Skip to content

Commit

Permalink
Improve underline annotation creation (#1010)
Browse files Browse the repository at this point in the history
* Fix underline annotation from text selection

* Improve PDF reader text selection menu generation

* Add support for blending underline annotations

* Fix missing annotation cases

* Improve annotation selection on PDF document

* Apply applicable annotation transformation to underline ones
  • Loading branch information
mvasilak authored Sep 16, 2024
1 parent 82fd581 commit 23b256e
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 85 deletions.
52 changes: 43 additions & 9 deletions Zotero/Controllers/AnnotationColorGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,35 @@ import UIKit
struct AnnotationColorGenerator {
private static let highlightOpacity: CGFloat = 0.5
private static let highlightDarkOpacity: CGFloat = 0.5
private static let underlineOpacity: CGFloat = 1
private static let underlineDarkOpacity: CGFloat = 1

static func color(from color: UIColor, isHighlight: Bool, userInterfaceStyle: UIUserInterfaceStyle) -> (color: UIColor, alpha: CGFloat, blendMode: CGBlendMode?) {
if !isHighlight {
static func color(from color: UIColor, type: AnnotationType?, userInterfaceStyle: UIUserInterfaceStyle) -> (color: UIColor, alpha: CGFloat, blendMode: CGBlendMode?) {
let opacity: CGFloat
switch type {
case .none, .note, .image, .ink, .freeText:
return (color, 1, nil)

case .highlight:
switch userInterfaceStyle {
case .dark:
opacity = Self.highlightDarkOpacity

default:
opacity = Self.highlightOpacity
}

case .underline:
switch userInterfaceStyle {
case .dark:
opacity = Self.underlineDarkOpacity

default:
opacity = Self.underlineOpacity
}
}

let adjustedColor: UIColor
switch userInterfaceStyle {
case .dark:
var hue: CGFloat = 0
Expand All @@ -27,17 +50,28 @@ struct AnnotationColorGenerator {
color.getHue(&hue, saturation: &sat, brightness: &brg, alpha: &alpha)

let adjustedSat = min(1, (sat * 1.2))
let adjustedColor = UIColor(hue: hue, saturation: adjustedSat, brightness: brg, alpha: AnnotationColorGenerator.highlightDarkOpacity)
return (adjustedColor, AnnotationColorGenerator.highlightDarkOpacity, .lighten)
adjustedColor = UIColor(hue: hue, saturation: adjustedSat, brightness: brg, alpha: opacity)

default:
let adjustedColor = color.withAlphaComponent(AnnotationColorGenerator.highlightOpacity)
return (adjustedColor, AnnotationColorGenerator.highlightOpacity, .multiply)
adjustedColor = color.withAlphaComponent(opacity)
}

return (adjustedColor, opacity, Self.blendMode(for: userInterfaceStyle, type: type))
}

static func blendMode(for userInterfaceStyle: UIUserInterfaceStyle, isHighlight: Bool) -> CGBlendMode? {
guard isHighlight else { return nil }
return userInterfaceStyle == .dark ? .lighten : .multiply
static func blendMode(for userInterfaceStyle: UIUserInterfaceStyle, type: AnnotationType?) -> CGBlendMode? {
switch type {
case .none, .note, .image, .ink, .freeText:
return nil

case .highlight, .underline:
switch userInterfaceStyle {
case .dark:
return .lighten

default:
return .multiply
}
}
}
}
4 changes: 2 additions & 2 deletions Zotero/Controllers/AnnotationConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct AnnotationConverter {
/// - returns: Sort index (5 places for page, 6 places for character offset, 5 places for y position)
static func sortIndex(from annotation: PSPDFKit.Annotation, boundingBoxConverter: AnnotationBoundingBoxConverter?) -> String {
let rect: CGRect
if annotation is PSPDFKit.HighlightAnnotation {
if annotation is PSPDFKit.HighlightAnnotation || annotation is PSPDFKit.UnderlineAnnotation {
rect = annotation.rects?.first ?? annotation.boundingBox
} else {
rect = annotation.boundingBox
Expand Down Expand Up @@ -243,7 +243,7 @@ struct AnnotationConverter {
) -> PSPDFKit.Annotation {
let (color, alpha, blendMode) = AnnotationColorGenerator.color(
from: UIColor(hex: zoteroAnnotation.color),
isHighlight: (zoteroAnnotation.type == .highlight),
type: zoteroAnnotation.type,
userInterfaceStyle: interfaceStyle
)
let annotation: PSPDFKit.Annotation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ struct SplitAnnotationsDbRequest: DbRequest {
guard let annotationType = item.fields.filter(.key(FieldKeys.Item.Annotation.type)).first.flatMap({ AnnotationType(rawValue: $0.value) }) else { return }

switch annotationType {
case .highlight:
case .highlight, .underline:
let rects = item.rects.map({ CGRect(x: $0.minX, y: $0.minY, width: ($0.maxX - $0.minY), height: ($0.maxY - $0.minY)) })

guard let splitRects = AnnotationSplitter.splitRectsIfNeeded(rects: Array(rects)) else { return }
Expand Down
18 changes: 9 additions & 9 deletions Zotero/Models/AnnotationsConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import PSPDFKit
struct AnnotationsConfig {
static let defaultActiveColor = "#ffd400"
static let allColors: [String] = ["#ffd400", "#ff6666", "#5fb236", "#2ea8e5", "#a28ae5", "#e56eee", "#f19837", "#aaaaaa", "#000000"]
static let typesWithColorVariation: [AnnotationType?] = [.none, .highlight, .underline]
static let userInterfaceStylesWithVarition: [UIUserInterfaceStyle] = [.light, .dark]
static let colorNames: [String: String] = [
"#ffd400": "Yellow",
"#ff6666": "Red",
Expand Down Expand Up @@ -53,16 +55,14 @@ struct AnnotationsConfig {

private static func createColorVariationMap() -> [String: String] {
var map: [String: String] = [:]
for hexBaseColor in self.allColors {
for hexBaseColor in allColors {
let baseColor = UIColor(hex: hexBaseColor)
let color1 = AnnotationColorGenerator.color(from: baseColor, isHighlight: false, userInterfaceStyle: .light).color
map[color1.hexString] = hexBaseColor
let color2 = AnnotationColorGenerator.color(from: baseColor, isHighlight: false, userInterfaceStyle: .dark).color
map[color2.hexString] = hexBaseColor
let color3 = AnnotationColorGenerator.color(from: baseColor, isHighlight: true, userInterfaceStyle: .light).color
map[color3.hexString] = hexBaseColor
let color4 = AnnotationColorGenerator.color(from: baseColor, isHighlight: true, userInterfaceStyle: .dark).color
map[color4.hexString] = hexBaseColor
for type in typesWithColorVariation {
for userInterfaceStyle in userInterfaceStylesWithVarition {
let variation = AnnotationColorGenerator.color(from: baseColor, type: type, userInterfaceStyle: userInterfaceStyle).color
map[variation.hexString] = hexBaseColor
}
}
}
return map
}
Expand Down
2 changes: 2 additions & 0 deletions Zotero/Models/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ final class Defaults {
self.squareColorHex = AnnotationsConfig.defaultActiveColor
self.noteColorHex = AnnotationsConfig.defaultActiveColor
self.highlightColorHex = AnnotationsConfig.defaultActiveColor
self.underlineColorHex = AnnotationsConfig.defaultActiveColor
self.textColorHex = AnnotationsConfig.defaultActiveColor
self.pdfSettings = PDFSettings.default
#endif
}
Expand Down
64 changes: 34 additions & 30 deletions Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
let baseColor = annotation.baseColor
let (color, alpha, blendMode) = AnnotationColorGenerator.color(
from: UIColor(hex: baseColor),
isHighlight: (annotation is PSPDFKit.HighlightAnnotation),
type: annotation.type.annotationType,
userInterfaceStyle: interfaceStyle
)
annotation.color = color
Expand Down Expand Up @@ -1155,7 +1155,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi

private func addImage(onPage pageIndex: PageIndex, origin: CGPoint, in viewModel: ViewModel<PDFReaderActionHandler>) {
guard let activeColor = viewModel.state.toolColors[tool(from: .image)] else { return }
let color = AnnotationColorGenerator.color(from: activeColor, isHighlight: false, userInterfaceStyle: viewModel.state.interfaceStyle).color
let color = AnnotationColorGenerator.color(from: activeColor, type: .image, userInterfaceStyle: viewModel.state.interfaceStyle).color
let rect = CGRect(origin: origin, size: CGSize(width: 100, height: 100))

let square = SquareAnnotation()
Expand All @@ -1171,7 +1171,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi

private func addNote(onPage pageIndex: PageIndex, origin: CGPoint, in viewModel: ViewModel<PDFReaderActionHandler>) {
guard let activeColor = viewModel.state.toolColors[tool(from: .note)] else { return }
let color = AnnotationColorGenerator.color(from: activeColor, isHighlight: false, userInterfaceStyle: viewModel.state.interfaceStyle).color
let color = AnnotationColorGenerator.color(from: activeColor, type: .note, userInterfaceStyle: viewModel.state.interfaceStyle).color
let rect = CGRect(origin: origin, size: AnnotationsConfig.noteAnnotationSize)

let note = NoteAnnotation(contents: "")
Expand All @@ -1187,7 +1187,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi

private func addHighlightOrUnderline(isHighlight: Bool, onPage pageIndex: PageIndex, rects: [CGRect], in viewModel: ViewModel<PDFReaderActionHandler>) {
guard let activeColor = viewModel.state.toolColors[tool(from: isHighlight ? .highlight : .underline)] else { return }
let (color, alpha, blendMode) = AnnotationColorGenerator.color(from: activeColor, isHighlight: true, userInterfaceStyle: viewModel.state.interfaceStyle)
let (color, alpha, blendMode) = AnnotationColorGenerator.color(from: activeColor, type: isHighlight ? .highlight : .underline, userInterfaceStyle: viewModel.state.interfaceStyle)

let annotation = isHighlight ? HighlightAnnotation() : UnderlineAnnotation()
annotation.rects = rects
Expand Down Expand Up @@ -1350,7 +1350,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
}

if changes.contains(.color), let (color, interfaceStyle) = color {
let (_color, alpha, blendMode) = AnnotationColorGenerator.color(from: UIColor(hex: color), isHighlight: (annotation.type == .highlight), userInterfaceStyle: interfaceStyle)
let (_color, alpha, blendMode) = AnnotationColorGenerator.color(from: UIColor(hex: color), type: annotation.type, userInterfaceStyle: interfaceStyle)
pdfAnnotation.color = _color
pdfAnnotation.alpha = alpha
if let blendMode {
Expand Down Expand Up @@ -1458,7 +1458,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
for annotation in annotations {
guard let tool = tool(from: annotation), let activeColor = state.toolColors[tool] else { continue }
// `AnnotationStateManager` doesn't apply the `blendMode` to created annotations, so it needs to be applied to newly created annotations here.
let (_, _, blendMode) = AnnotationColorGenerator.color(from: activeColor, isHighlight: (annotation is PSPDFKit.HighlightAnnotation), userInterfaceStyle: state.interfaceStyle)
let (_, _, blendMode) = AnnotationColorGenerator.color(from: activeColor, type: annotation.type.annotationType, userInterfaceStyle: state.interfaceStyle)
annotation.blendMode = blendMode ?? .normal

// Either annotation is new (key not assigned) or the user used undo/redo and we check whether the annotation exists in DB
Expand All @@ -1468,11 +1468,11 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
}
var workingAnnotation = annotation

if let transformedHighlightAnnotation = transformHighlightRectsIfNeeded(annotation: annotation) {
DDLogInfo("PDFReaderActionHandler: did transform highlight annotation rects")
if let transformedAnnotation = transformHighlightOrUnderlineRectsIfNeeded(annotation: annotation) {
DDLogInfo("PDFReaderActionHandler: did transform highlight/underline annotation rects")
toRemove.append(annotation)
toAdd.append(transformedHighlightAnnotation)
workingAnnotation = transformedHighlightAnnotation
toAdd.append(transformedAnnotation)
workingAnnotation = transformedAnnotation
}

let splitAnnotations = splitIfNeeded(annotation: workingAnnotation)
Expand All @@ -1495,22 +1495,25 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
return (keptAsIs, toRemove, toAdd)

// TODO: Remove if issues are fixed in PSPDFKit
/// Transforms highlight annotation if needed.
/// Transforms highlight/underline annotation if needed.
/// (a) Merges rects that are in the same text line.
/// (b) Trims different line rects that overlap.
/// If not a higlight annotation, or transformations are not needed, it returns nil.
/// (b) Trims different line rects that overlap. (only for highlight annotations)
/// If not a higlight/underline annotation, or transformations are not needed, it returns nil.
/// Issue appeared in PSPDFKit 13.5.0
/// - parameter annotation: Annotation to be transformed if needed
func transformHighlightRectsIfNeeded(annotation: PSPDFKit.Annotation) -> PSPDFKit.Annotation? {
guard annotation is HighlightAnnotation, let rects = annotation.rects, rects.count > 1 else { return nil }
func transformHighlightOrUnderlineRectsIfNeeded(annotation: PSPDFKit.Annotation) -> PSPDFKit.Annotation? {
guard annotation is HighlightAnnotation || annotation is UnderlineAnnotation, let rects = annotation.rects, rects.count > 1 else { return nil }
let isHighlight = annotation is HighlightAnnotation
var workingRects = rects
workingRects = mergeHighlightRectsIfNeeded(workingRects)
workingRects = trimOverlappingHighlightRectsIfNeeded(workingRects)
workingRects = mergeHighlightOrUnderlineRectsIfNeeded(workingRects)
if isHighlight {
workingRects = trimOverlappingHighlightRectsIfNeeded(workingRects)
}
guard workingRects != rects else { return nil }
return copyHighlightAnnotation(from: annotation, with: workingRects)
return copyHighlightOrUnderlineAnnotation(isHighlight: isHighlight, from: annotation, with: workingRects)

func mergeHighlightRectsIfNeeded(_ rects: [CGRect]) -> [CGRect] {
// Check if there are gaps for sequential highlight rects on the same line, and if so transform the annotation to eliminate them.
func mergeHighlightOrUnderlineRectsIfNeeded(_ rects: [CGRect]) -> [CGRect] {
// Check if there are gaps for sequential highlight/underline rects on the same line, and if so transform the annotation to eliminate them.
var mergedRects: [CGRect] = []
for rect in rects {
guard let previousRect = mergedRects.last, rect.minY == previousRect.minY, rect.height == previousRect.height else {
Expand Down Expand Up @@ -1547,8 +1550,8 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
return trimmedRects
}

func copyHighlightAnnotation(from annotation: PSPDFKit.Annotation, with rects: [CGRect]) -> HighlightAnnotation {
let newAnnotation = HighlightAnnotation()
func copyHighlightOrUnderlineAnnotation(isHighlight: Bool, from annotation: PSPDFKit.Annotation, with rects: [CGRect]) -> Annotation {
let newAnnotation = isHighlight ? HighlightAnnotation() : UnderlineAnnotation()
newAnnotation.rects = rects
newAnnotation.boundingBox = AnnotationBoundingBoxCalculator.boundingBox(from: rects)
newAnnotation.alpha = annotation.alpha
Expand All @@ -1564,20 +1567,21 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
/// - parameter annotation: Annotation to split
/// - returns: Array with original annotation if limit was not exceeded. Otherwise array of new split annotations.
func splitIfNeeded(annotation: PSPDFKit.Annotation) -> [PSPDFKit.Annotation] {
if let annotation = annotation as? HighlightAnnotation, let rects = annotation.rects, let splitRects = AnnotationSplitter.splitRectsIfNeeded(rects: rects) {
return createAnnotations(from: splitRects, original: annotation)
if annotation is HighlightAnnotation || annotation is UnderlineAnnotation, let rects = annotation.rects, let splitRects = AnnotationSplitter.splitRectsIfNeeded(rects: rects) {
let isHighlight = annotation is HighlightAnnotation
return createHighlightOrUnderlineAnnotations(isHighlight: isHighlight, from: splitRects, original: annotation)
}

if let annotation = annotation as? InkAnnotation, let paths = annotation.lines, let splitPaths = AnnotationSplitter.splitPathsIfNeeded(paths: paths) {
return createAnnotations(from: splitPaths, original: annotation)
return createInkAnnotations(from: splitPaths, original: annotation)
}

return [annotation]

func createAnnotations(from splitRects: [[CGRect]], original: HighlightAnnotation) -> [HighlightAnnotation] {
func createHighlightOrUnderlineAnnotations(isHighlight: Bool, from splitRects: [[CGRect]], original: Annotation) -> [Annotation] {
guard splitRects.count > 1 else { return [original] }
return splitRects.map { rects -> HighlightAnnotation in
let new = HighlightAnnotation()
return splitRects.map { rects -> Annotation in
let new = isHighlight ? HighlightAnnotation() : UnderlineAnnotation()
new.rects = rects
new.boundingBox = AnnotationBoundingBoxCalculator.boundingBox(from: rects)
new.alpha = original.alpha
Expand All @@ -1589,7 +1593,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
}
}

func createAnnotations(from splitPaths: [[[DrawingPoint]]], original: InkAnnotation) -> [InkAnnotation] {
func createInkAnnotations(from splitPaths: [[[DrawingPoint]]], original: InkAnnotation) -> [InkAnnotation] {
guard splitPaths.count > 1 else { return [original] }
return splitPaths.map { paths in
let new = InkAnnotation(lines: paths)
Expand Down Expand Up @@ -2352,7 +2356,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
if pdfAnnotation.baseColor != annotation.color {
let hexColor = annotation.color

let (color, alpha, blendMode) = AnnotationColorGenerator.color(from: UIColor(hex: hexColor), isHighlight: (annotation.type == .highlight), userInterfaceStyle: interfaceStyle)
let (color, alpha, blendMode) = AnnotationColorGenerator.color(from: UIColor(hex: hexColor), type: annotation.type, userInterfaceStyle: interfaceStyle)
pdfAnnotation.color = color
pdfAnnotation.alpha = alpha
if let blendMode {
Expand Down
Loading

0 comments on commit 23b256e

Please sign in to comment.