Skip to content

Commit

Permalink
Text and Underline annotation support (#809)
Browse files Browse the repository at this point in the history
  • Loading branch information
michalrentka authored Mar 13, 2024
1 parent bc61215 commit 0802b12
Show file tree
Hide file tree
Showing 56 changed files with 1,514 additions and 653 deletions.
36 changes: 34 additions & 2 deletions Zotero.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Zotero/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@
"pdf.annotation_toolbar.note" = "Note";
"pdf.annotation_toolbar.image" = "Image";
"pdf.annotation_toolbar.ink" = "Ink";
"pdf.annotation_toolbar.underline" = "Underline";
"pdf.annotation_toolbar.text" = "Text";
"pdf.locked.locked" = "Locked";
"pdf.locked.enter_password" = "Please enter the password to open this PDF.";
"pdf.locked.failed" = "Incorrect password. Please try again.";
Expand All @@ -267,6 +269,7 @@
"pdf.export.export_annotated" = "Export Annotated PDF";
"pdf.sidebar.no_annotations" = "No Annotations";
"pdf.sidebar.no_outline" = "No Outline";
"pdf.delete_annotation" = "Do you really want to delete annotation?";

"settings.title" = "Settings";
"settings.logout" = "Sign Out";
Expand Down Expand Up @@ -520,11 +523,15 @@
"accessibility.pdf.note_annotation_tool" = "Create note annotation";
"accessibility.pdf.image_annotation_tool" = "Create image annotation";
"accessibility.pdf.ink_annotation_tool" = "Create ink annotation";
"accessibility.pdf.underline_annotation_tool" = "Create underline annotation";
"accessibility.pdf.text_annotation_tool" = "Create text annotation";
"accessibility.pdf.eraser_annotation_tool" = "Eraser";
"accessibility.pdf.highlight_annotation" = "Highlight annotation";
"accessibility.pdf.note_annotation" = "Note annotation";
"accessibility.pdf.image_annotation" = "Image annotation";
"accessibility.pdf.ink_annotation" = "Ink annotation";
"accessibility.pdf.underline_annotation" = "Underline annotation";
"accessibility.pdf.text_annotation" = "Text annotation";
"accessibility.pdf.edit_annotation" = "Edit annotation";
"accessibility.pdf.share_annotation" = "Share annotation";
"accessibility.pdf.share_annotation_image" = "Share annotation image";
Expand Down
89 changes: 75 additions & 14 deletions Zotero/Controllers/AnnotationConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,34 +81,41 @@ struct AnnotationConverter {

let type: AnnotationType
let rects: [CGRect]
let text: String?
var text: String?
let paths: [[CGPoint]]
let lineWidth: CGFloat?
var lineWidth: CGFloat?
var fontSize: UInt?
var rotation: UInt?

if let annotation = annotation as? PSPDFKit.NoteAnnotation {
type = .note
rects = self.rects(fromNoteAnnotation: annotation)
text = nil
paths = []
lineWidth = nil
} else if let annotation = annotation as? PSPDFKit.HighlightAnnotation {
type = .highlight
rects = self.rects(fromHighlightAnnotation: annotation)
rects = self.rects(fromHighlightAndUnderlineAnnotation: annotation)
text = TextConverter.convertTextForAnnotation(from: annotation.markedUpString)
paths = []
lineWidth = nil
} else if let annotation = annotation as? PSPDFKit.SquareAnnotation {
type = .image
rects = self.rects(fromSquareAnnotation: annotation)
text = nil
paths = []
lineWidth = nil
} else if let annotation = annotation as? PSPDFKit.InkAnnotation {
type = .ink
rects = []
text = nil
paths = self.paths(from: annotation)
lineWidth = annotation.lineWidth
} else if let annotation = annotation as? PSPDFKit.UnderlineAnnotation {
type = .underline
rects = self.rects(fromHighlightAndUnderlineAnnotation: annotation)
text = TextConverter.convertTextForAnnotation(from: annotation.markedUpString)
paths = []
} else if let annotation = annotation as? PSPDFKit.FreeTextAnnotation {
type = .freeText
fontSize = UInt(annotation.fontSize)
rotation = annotation.rotation
paths = []
rects = self.rects(fromTextAnnotation: annotation)
} else {
return nil
}
Expand All @@ -126,6 +133,8 @@ struct AnnotationConverter {
color: color,
comment: comment,
text: text,
fontSize: fontSize,
rotation: rotation,
sortIndex: sortIndex,
dateModified: date
)
Expand All @@ -143,27 +152,39 @@ struct AnnotationConverter {
if let annotation = annotation as? PSPDFKit.NoteAnnotation {
return self.rects(fromNoteAnnotation: annotation)
}
if let annotation = annotation as? PSPDFKit.HighlightAnnotation {
return self.rects(fromHighlightAnnotation: annotation)
if annotation is PSPDFKit.HighlightAnnotation || annotation is PSPDFKit.UnderlineAnnotation {
return self.rects(fromHighlightAndUnderlineAnnotation: annotation)
}
if let annotation = annotation as? PSPDFKit.SquareAnnotation {
return self.rects(fromSquareAnnotation: annotation)
}
if let annotation = annotation as? PSPDFKit.FreeTextAnnotation {
return self.rects(fromTextAnnotation: annotation)
}
return nil
}

private static func rects(fromNoteAnnotation annotation: PSPDFKit.NoteAnnotation) -> [CGRect] {
return [CGRect(origin: annotation.boundingBox.origin.rounded(to: 3), size: AnnotationsConfig.noteAnnotationSize)]
}

private static func rects(fromHighlightAnnotation annotation: PSPDFKit.HighlightAnnotation) -> [CGRect] {
private static func rects(fromHighlightAndUnderlineAnnotation annotation: PSPDFKit.Annotation) -> [CGRect] {
return (annotation.rects ?? [annotation.boundingBox]).map({ $0.rounded(to: 3) })
}

private static func rects(fromSquareAnnotation annotation: PSPDFKit.SquareAnnotation) -> [CGRect] {
return [annotation.boundingBox.rounded(to: 3)]
}

private static func rects(fromTextAnnotation annotation: PSPDFKit.FreeTextAnnotation) -> [CGRect] {
guard annotation.rotation > 0 else { return [annotation.boundingBox] }
let originalRotation = annotation.rotation
annotation.setRotation(0, updateBoundingBox: true)
let boundingBox = annotation.boundingBox.rounded(to: 3)
annotation.setRotation(originalRotation, updateBoundingBox: true)
return [boundingBox]
}

private static func createName(from displayName: String, username: String) -> String {
if !displayName.isEmpty {
return displayName
Expand All @@ -189,9 +210,10 @@ struct AnnotationConverter {
username: String,
boundingBoxConverter: AnnotationBoundingBoxConverter
) -> [PSPDFKit.Annotation] {
return items.map({ item in
return items.compactMap({ item in
guard let annotation = PDFDatabaseAnnotation(item: item) else { return nil }
return self.annotation(
from: PDFDatabaseAnnotation(item: item),
from: annotation,
type: type,
interfaceStyle: interfaceStyle,
currentUserId: currentUserId,
Expand Down Expand Up @@ -232,6 +254,12 @@ struct AnnotationConverter {

case .ink:
annotation = self.inkAnnotation(from: zoteroAnnotation, type: type, color: color, boundingBoxConverter: boundingBoxConverter)

case .underline:
annotation = self.underlineAnnotation(from: zoteroAnnotation, type: type, color: color, alpha: alpha, boundingBoxConverter: boundingBoxConverter)

case .freeText:
annotation = self.freeTextAnnotation(from: zoteroAnnotation, color: color, boundingBoxConverter: boundingBoxConverter)
}

switch type {
Expand Down Expand Up @@ -332,6 +360,39 @@ struct AnnotationConverter {
ink.lineWidth = annotation.lineWidth ?? 1
return ink
}

private static func underlineAnnotation(
from annotation: PDFAnnotation,
type: Kind,
color: UIColor,
alpha: CGFloat,
boundingBoxConverter: AnnotationBoundingBoxConverter
) -> PSPDFKit.UnderlineAnnotation {
let underline: PSPDFKit.UnderlineAnnotation
switch type {
case .export:
underline = PSPDFKit.UnderlineAnnotation()

case .zotero:
underline = UnderlineAnnotation()
}

underline.boundingBox = annotation.boundingBox(boundingBoxConverter: boundingBoxConverter).rounded(to: 3)
underline.rects = annotation.rects(boundingBoxConverter: boundingBoxConverter).map({ $0.rounded(to: 3) })
underline.color = color
underline.alpha = alpha

return underline
}

private static func freeTextAnnotation(from annotation: PDFAnnotation, color: UIColor, boundingBoxConverter: AnnotationBoundingBoxConverter) -> PSPDFKit.FreeTextAnnotation {
let text = PSPDFKit.FreeTextAnnotation(contents: annotation.comment)
text.color = color
text.fontSize = CGFloat(annotation.fontSize ?? 0)
text.setBoundingBox(annotation.boundingBox(boundingBoxConverter: boundingBoxConverter).rounded(to: 3), transformSize: true)
text.setRotation(annotation.rotation ?? 0, updateBoundingBox: true)
return text
}
}

extension RItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,11 @@ struct AnnotationPreviewBoundingBoxCalculator {
static func imagePreviewRect(from boundingBox: CGRect, lineWidth: CGFloat) -> CGRect {
return boundingBox.insetBy(dx: (lineWidth + 1), dy: (lineWidth + 1)).rounded(to: 3)
}

static func freeTextPreviewRect(from boundingBox: CGRect, rotation: UInt) -> CGRect {
let x = boundingBox.midX
let y = boundingBox.midY
let transform = CGAffineTransform(translationX: x, y: y).rotated(by: CGFloat(rotation) * .pi / 180).translatedBy(x: -x, y: -y)
return boundingBox.applying(transform)
}
}
3 changes: 2 additions & 1 deletion Zotero/Controllers/AnnotationPreviewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ extension AnnotationPreviewController {

// Cache and report original color
let rect = annotation.previewBoundingBox
let includeAnnotation = annotation is PSPDFKit.InkAnnotation || annotation is PSPDFKit.FreeTextAnnotation
self.enqueue(
key: annotation.previewId,
parentKey: parentKey,
Expand All @@ -111,7 +112,7 @@ extension AnnotationPreviewController {
rect: rect,
imageSize: previewSize,
imageScale: 0.0,
includeAnnotation: (annotation is PSPDFKit.InkAnnotation),
includeAnnotation: includeAnnotation,
invertColors: false,
isDark: isDark,
type: .cachedAndReported
Expand Down
10 changes: 9 additions & 1 deletion Zotero/Controllers/AttributedTagStringGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,16 @@ struct AttributedTagStringGenerator {
}

static func attributedString(from tags: [Tag], limit: Int? = nil) -> NSMutableAttributedString {
let sorted = tags.sorted { lTag, rTag in
if !rTag.color.isEmpty && lTag.color.isEmpty {
return false
} else if rTag.color.isEmpty && !lTag.color.isEmpty {
return true
}
return lTag.name.localizedCaseInsensitiveCompare(rTag.name) == .orderedAscending
}
let wholeString = NSMutableAttributedString()
for (index, tag) in tags.enumerated() {
for (index, tag) in sorted.enumerated() {
if let limit = limit, index == limit {
break
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ struct CreatePDFAnnotationsDbRequest: DbRequest {
var needsWrite: Bool { return true }

func process(in database: Realm) throws {
guard let parent = database.objects(RItem.self).filter(.key(self.attachmentKey, in: self.libraryId)).first else { return }
guard let parent = database.objects(RItem.self).filter(.key(attachmentKey, in: libraryId)).first else { return }

for annotation in self.annotations {
self.create(annotation: annotation, parent: parent, in: database)
for annotation in annotations {
create(annotation: annotation, parent: parent, in: database)
}
}

private func create(annotation: PDFDocumentAnnotation, parent: RItem, in database: Realm) {
let item: RItem

if let _item = database.objects(RItem.self).filter(.key(annotation.key, in: self.libraryId)).first {
if let _item = database.objects(RItem.self).filter(.key(annotation.key, in: libraryId)).first {
if !_item.deleted {
// If item exists and is not deleted locally, we can ignore this request
return
Expand All @@ -46,12 +46,13 @@ struct CreatePDFAnnotationsDbRequest: DbRequest {
item = RItem()
item.key = annotation.key
item.rawType = ItemTypes.annotation
item.localizedType = self.schemaController.localized(itemType: ItemTypes.annotation) ?? ""
item.libraryId = self.libraryId
item.localizedType = schemaController.localized(itemType: ItemTypes.annotation) ?? ""
item.libraryId = libraryId
item.dateAdded = annotation.dateModified
database.add(item)
}

item.annotationType = annotation.type.rawValue
item.syncState = .synced
item.changeType = .user
item.htmlFreeContent = annotation.comment.isEmpty ? nil : annotation.comment.strippedRichTextTags
Expand Down Expand Up @@ -89,8 +90,10 @@ struct CreatePDFAnnotationsDbRequest: DbRequest {

case FieldKeys.Item.Annotation.Position.pageIndex where field.baseKey == FieldKeys.Item.Annotation.position:
rField.value = "\(annotation.page)"

case FieldKeys.Item.Annotation.Position.lineWidth where field.baseKey == FieldKeys.Item.Annotation.position:
rField.value = annotation.lineWidth.flatMap({ "\(Decimal($0).rounded(to: 3))" }) ?? ""

case FieldKeys.Item.Annotation.pageLabel:
rField.value = annotation.pageLabel

Expand All @@ -100,6 +103,13 @@ struct CreatePDFAnnotationsDbRequest: DbRequest {

case FieldKeys.Item.Annotation.text:
rField.value = annotation.text ?? ""

case FieldKeys.Item.Annotation.Position.rotation where field.baseKey == FieldKeys.Item.Annotation.position:
rField.value = "\(annotation.rotation ?? 0)"

case FieldKeys.Item.Annotation.Position.fontSize where field.baseKey == FieldKeys.Item.Annotation.position:
rField.value = "\(annotation.fontSize ?? 0)"

default: break
}

Expand All @@ -108,12 +118,12 @@ struct CreatePDFAnnotationsDbRequest: DbRequest {
}

private func add(rects: [CGRect], to item: RItem, changes: inout RItemChanges, database: Realm) {
guard !rects.isEmpty else { return }
guard !rects.isEmpty, let annotation = PDFDatabaseAnnotation(item: item) else { return }

let page = UInt(PDFDatabaseAnnotation(item: item).page)
let page = UInt(annotation.page)

for rect in rects {
let dbRect = self.boundingBoxConverter.convertToDb(rect: rect, page: page) ?? rect
let dbRect = boundingBoxConverter.convertToDb(rect: rect, page: page) ?? rect

let rRect = RRect()
rRect.minX = Double(dbRect.minX)
Expand All @@ -126,16 +136,16 @@ struct CreatePDFAnnotationsDbRequest: DbRequest {
}

private func add(paths: [[CGPoint]], to item: RItem, changes: inout RItemChanges, database: Realm) {
guard !paths.isEmpty else { return }
guard !paths.isEmpty, let annotation = PDFDatabaseAnnotation(item: item) else { return }

let page = UInt(PDFDatabaseAnnotation(item: item).page)
let page = UInt(annotation.page)

for (idx, path) in paths.enumerated() {
let rPath = RPath()
rPath.sortIndex = idx

for (idy, point) in path.enumerated() {
let dbPoint = self.boundingBoxConverter.convertToDb(point: point, page: page) ?? point
let dbPoint = boundingBoxConverter.convertToDb(point: point, page: page) ?? point

let rXCoordinate = RPathCoordinate()
rXCoordinate.value = Double(dbPoint.x)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// EditAnnotationFontSizeDbRequest.swift
// Zotero
//
// Created by Michal Rentka on 01.08.2023.
// Copyright © 2023 Corporation for Digital Scholarship. All rights reserved.
//

import Foundation

import RealmSwift

struct EditAnnotationFontSizeDbRequest: DbRequest {
let key: String
let libraryId: LibraryIdentifier
let size: UInt

var needsWrite: Bool { return true }

func process(in database: Realm) throws {
guard let item = database.objects(RItem.self).filter(.key(self.key, in: self.libraryId)).first else { return }

let field: RItemField
if let _field = item.fields.filter(.key(FieldKeys.Item.Annotation.Position.fontSize)).first {
field = _field
} else {
field = RItemField()
field.key = FieldKeys.Item.Annotation.Position.fontSize
field.baseKey = FieldKeys.Item.Annotation.position
item.fields.append(field)
}

field.value = "\(self.size)"
item.changeType = .user
item.changes.append(RObjectChange.create(changes: RItemChanges.fields))
}
}
Loading

0 comments on commit 0802b12

Please sign in to comment.