Skip to content

Commit

Permalink
Added sepia mode to PDF reader (#1070)
Browse files Browse the repository at this point in the history
  • Loading branch information
michalrentka authored Feb 19, 2025
1 parent 0e9c0e0 commit dd9701d
Show file tree
Hide file tree
Showing 23 changed files with 290 additions and 174 deletions.
10 changes: 10 additions & 0 deletions Zotero.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,10 @@
B36FD9AF2A78FB13002D77E8 /* EditAnnotationFontSizeDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36FD9AE2A78FB13002D77E8 /* EditAnnotationFontSizeDbRequest.swift */; };
B36FD9B12A7924CB002D77E8 /* FontSizePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36FD9B02A7924CB002D77E8 /* FontSizePickerViewController.swift */; };
B36FD9B32A7929C8002D77E8 /* FontSizeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B36FD9B22A7929C8002D77E8 /* FontSizeCell.swift */; };
B370625E2D5F407100E1722B /* ReaderSettingsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F9A4C22B04D0D800684030 /* ReaderSettingsState.swift */; };
B370625F2D5F595600E1722B /* PDFSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35B473B25F11BCD0023B6F4 /* PDFSettings.swift */; };
B37062612D5F75E200E1722B /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37062602D5F75DF00E1722B /* Appearance.swift */; };
B37062622D5F75E200E1722B /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37062602D5F75DF00E1722B /* Appearance.swift */; };
B37080532AA7216E006F56B9 /* Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B37080512AA72135006F56B9 /* Localizable.swift */; };
B371494C25D585EA00D6391E /* RestoreDeletionsSyncAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B340868825D574D6000F4446 /* RestoreDeletionsSyncAction.swift */; };
B371494F25D585EF00D6391E /* MarkObjectsAsChangedByUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B340868C25D579C9000F4446 /* MarkObjectsAsChangedByUser.swift */; };
Expand Down Expand Up @@ -1805,6 +1809,7 @@
B36FD9AE2A78FB13002D77E8 /* EditAnnotationFontSizeDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAnnotationFontSizeDbRequest.swift; sourceTree = "<group>"; };
B36FD9B02A7924CB002D77E8 /* FontSizePickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizePickerViewController.swift; sourceTree = "<group>"; };
B36FD9B22A7929C8002D77E8 /* FontSizeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizeCell.swift; sourceTree = "<group>"; };
B37062602D5F75DF00E1722B /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = "<group>"; };
B37080512AA72135006F56B9 /* Localizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizable.swift; sourceTree = "<group>"; };
B372CEDF2486504600B423AE /* GroupVersionsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupVersionsRequest.swift; sourceTree = "<group>"; };
B372CEE22486512500B423AE /* GroupRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupRequest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2692,6 +2697,7 @@
B3B557122C884B7D00BD6325 /* AnnotationPreview.swift */,
B3F6AA392AB30663005BC22E /* AnnotationTool.swift */,
B311737A2AFCF004007D9741 /* AnnotationType.swift */,
B37062602D5F75DF00E1722B /* Appearance.swift */,
B305656123FC051E003304F2 /* AppGroup.swift */,
B305653A23FC051E003304F2 /* Attachment.swift */,
B339459026DE6C2900E59A02 /* AttachmentFileDeletedNotification.swift */,
Expand Down Expand Up @@ -5180,6 +5186,7 @@
B3D427D62CB67EFC0058453A /* AutoEmptyTrashController.swift in Sources */,
B30565EB23FC051E003304F2 /* Controllers.swift in Sources */,
B3A351E12715784A002E597A /* WebDavDownloadRequest.swift in Sources */,
B37062612D5F75E200E1722B /* Appearance.swift in Sources */,
B305CEBB29E6E67600B9E2B4 /* AssignItemsToTagDbRequest.swift in Sources */,
B39AF554290033CD001F400F /* TableOfContentsViewController.swift in Sources */,
B3593F78241A76E600760E20 /* CollectionEditError.swift in Sources */,
Expand Down Expand Up @@ -5736,6 +5743,7 @@
B3868541270DC3AA0068A022 /* WebDavScheme.swift in Sources */,
B352FFCC2CBE944400D0887B /* AutoEmptyTrashDbRequest.swift in Sources */,
B331F9AF2653CEA00099F6A6 /* ReadGroupDbRequest.swift in Sources */,
B370625F2D5F595600E1722B /* PDFSettings.swift in Sources */,
B33E8A4B27B6A39100CBC7DE /* CollectionCell.swift in Sources */,
B3DDC0CC2667825E00B2DFD1 /* RegularExpression+Extensions.swift in Sources */,
B30566EB23FC082A003304F2 /* ObjectsRequest.swift in Sources */,
Expand All @@ -5754,6 +5762,7 @@
B30566F123FC0845003304F2 /* CheckItemIsChangedDbRequest.swift in Sources */,
B3AFB83D2A0CF1EE008C2374 /* EmptyDecodable.swift in Sources */,
B36459E42644120D00A0C2C0 /* Color+Extension.swift in Sources */,
B370625E2D5F407100E1722B /* ReaderSettingsState.swift in Sources */,
B34ACC7F2514EB6100040C17 /* AnnotationColorGenerator.swift in Sources */,
B3FE79E42B333030009FBDBD /* DeleteDownloadDbRequest.swift in Sources */,
B305670423FC08A6003304F2 /* ReadAllCustomLibrariesDbRequest.swift in Sources */,
Expand Down Expand Up @@ -5902,6 +5911,7 @@
B3B557182C8860E600BD6325 /* CitationMetadata.swift in Sources */,
B305677023FC0A58003304F2 /* Alamofire+RxSwift.swift in Sources */,
B330261225DEAD9A00742025 /* RPageIndex.swift in Sources */,
B37062622D5F75E200E1722B /* Appearance.swift in Sources */,
B305670023FC086B003304F2 /* MarkObjectsAsDeletedDbRequest.swift in Sources */,
B34341B7260A49C300093E63 /* ReadCollectionAndLibraryDbRequest.swift in Sources */,
B305671023FC08C3003304F2 /* ReadSearchesDbRequest.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Zotero/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@
"pdf.settings.appearance.auto" = "Automatic";
"pdf.settings.appearance.light_mode" = "Light";
"pdf.settings.appearance.dark_mode" = "Dark";
"pdf.settings.appearance.sepia_mode" = "Sepia";
"pdf.settings.page_mode.title" = "Page Mode";
"pdf.settings.page_mode.single" = "Single";
"pdf.settings.page_mode.double" = "Double";
Expand Down
22 changes: 11 additions & 11 deletions Zotero/Controllers/AnnotationColorGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,33 @@ struct AnnotationColorGenerator {
private static let underlineOpacity: CGFloat = 1
private static let underlineDarkOpacity: CGFloat = 1

static func color(from color: UIColor, type: AnnotationType?, userInterfaceStyle: UIUserInterfaceStyle) -> (color: UIColor, alpha: CGFloat, blendMode: CGBlendMode?) {
static func color(from color: UIColor, type: AnnotationType?, appearance: Appearance) -> (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 {
switch appearance {
case .dark:
opacity = Self.highlightDarkOpacity

default:
case .light, .sepia:
opacity = Self.highlightOpacity
}

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

default:
case .light, .sepia:
opacity = Self.underlineOpacity
}
}

let adjustedColor: UIColor
switch userInterfaceStyle {
switch appearance {
case .dark:
var hue: CGFloat = 0
var sat: CGFloat = 0
Expand All @@ -52,24 +52,24 @@ struct AnnotationColorGenerator {
let adjustedSat = min(1, (sat * 1.2))
adjustedColor = UIColor(hue: hue, saturation: adjustedSat, brightness: brg, alpha: opacity)

default:
case .light, .sepia:
adjustedColor = color.withAlphaComponent(opacity)
}

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

static func blendMode(for userInterfaceStyle: UIUserInterfaceStyle, type: AnnotationType?) -> CGBlendMode? {
static func blendMode(for appearance: Appearance, type: AnnotationType?) -> CGBlendMode? {
switch type {
case .none, .note, .image, .ink, .freeText:
return nil

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

default:
case .light, .sepia:
return .multiply
}
}
Expand Down
8 changes: 4 additions & 4 deletions Zotero/Controllers/AnnotationConverter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ struct AnnotationConverter {
static func annotations(
from items: Results<RItem>,
type: Kind = .zotero,
interfaceStyle: UIUserInterfaceStyle,
appearance: Appearance,
currentUserId: Int,
library: Library,
displayName: String,
Expand All @@ -221,7 +221,7 @@ struct AnnotationConverter {
return annotation(
from: dbAnnotation,
type: type,
interfaceStyle: interfaceStyle,
appearance: appearance,
currentUserId: currentUserId,
library: library,
displayName: displayName,
Expand All @@ -234,7 +234,7 @@ struct AnnotationConverter {
static func annotation(
from zoteroAnnotation: PDFDatabaseAnnotation,
type: Kind,
interfaceStyle: UIUserInterfaceStyle,
appearance: Appearance,
currentUserId: Int,
library: Library,
displayName: String,
Expand All @@ -244,7 +244,7 @@ struct AnnotationConverter {
let (color, alpha, blendMode) = AnnotationColorGenerator.color(
from: UIColor(hex: zoteroAnnotation.color),
type: zoteroAnnotation.type,
userInterfaceStyle: interfaceStyle
appearance: appearance
)
let annotation: PSPDFKit.Annotation

Expand Down
53 changes: 31 additions & 22 deletions Zotero/Controllers/AnnotationPreviewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class AnnotationPreviewController: NSObject {
let imageSize: CGSize
let imageScale: CGFloat
let includeAnnotation: Bool
let isDark: Bool
let appearance: Appearance
let type: PreviewType

init(
Expand All @@ -37,7 +37,7 @@ final class AnnotationPreviewController: NSObject {
parentKey: String,
libraryId: LibraryIdentifier,
includeAnnotation: Bool = false,
isDark: Bool = false,
appearance: Appearance,
type: PreviewType
) {
self.key = key
Expand All @@ -49,11 +49,11 @@ final class AnnotationPreviewController: NSObject {
self.imageSize = imageSize
self.imageScale = imageScale
self.includeAnnotation = includeAnnotation
self.isDark = isDark
self.appearance = appearance
self.type = type
}

init?(annotation: PSPDFKit.Annotation, parentKey: String, libraryId: LibraryIdentifier, imageSize: CGSize, imageScale: CGFloat, isDark: Bool, type: PreviewType) {
init?(annotation: PSPDFKit.Annotation, parentKey: String, libraryId: LibraryIdentifier, imageSize: CGSize, imageScale: CGFloat, appearance: Appearance, type: PreviewType) {
guard annotation.shouldRenderPreview, let document = annotation.document else { return nil }
key = annotation.previewId
self.parentKey = parentKey
Expand All @@ -64,7 +64,7 @@ final class AnnotationPreviewController: NSObject {
self.imageSize = imageSize
self.imageScale = imageScale
includeAnnotation = annotation is PSPDFKit.InkAnnotation || annotation is PSPDFKit.FreeTextAnnotation
self.isDark = isDark
self.appearance = appearance
self.type = type
}
}
Expand Down Expand Up @@ -141,6 +141,7 @@ extension AnnotationPreviewController {
key: key,
parentKey: parentKey,
libraryId: libraryId,
appearance: .light,
type: .temporary(subscriberKey: subscriberKey)
)
)
Expand All @@ -155,22 +156,22 @@ extension AnnotationPreviewController {
/// - parameter parentKey: Key of PDF item.
/// - parameter libraryId: Library identifier of item.
/// - parameter isDark: `true` if dark mode is on, `false` otherwise.
func store(for annotation: PSPDFKit.Annotation, parentKey: String, libraryId: LibraryIdentifier, isDark: Bool) {
func store(for annotation: PSPDFKit.Annotation, parentKey: String, libraryId: LibraryIdentifier, appearance: Appearance) {
queue.async { [weak self] in
guard
let self,
let data = EnqueuedData(annotation: annotation, parentKey: parentKey, libraryId: libraryId, imageSize: previewSize, imageScale: 0, isDark: isDark, type: .cachedAndReported)
let data = EnqueuedData(annotation: annotation, parentKey: parentKey, libraryId: libraryId, imageSize: previewSize, imageScale: 0, appearance: appearance, type: .cachedAndReported)
else { return }
enqueue(data: data)
}
}

func store(annotations: [PSPDFKit.Annotation], parentKey: String, libraryId: LibraryIdentifier, isDark: Bool) {
func store(annotations: [PSPDFKit.Annotation], parentKey: String, libraryId: LibraryIdentifier, appearance: Appearance) {
queue.async { [weak self] in
guard let self else { return }
for annotation in annotations {
guard
let data = EnqueuedData(annotation: annotation, parentKey: parentKey, libraryId: libraryId, imageSize: previewSize, imageScale: 0, isDark: isDark, type: .cachedOnly)
let data = EnqueuedData(annotation: annotation, parentKey: parentKey, libraryId: libraryId, imageSize: previewSize, imageScale: 0, appearance: appearance, type: .cachedOnly)
else { continue }
enqueue(data: data)
}
Expand All @@ -193,8 +194,9 @@ extension AnnotationPreviewController {
queue.async { [weak self] in
guard let self else { return }
for key in keys {
try? fileStorage.remove(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, isDark: true))
try? fileStorage.remove(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, isDark: false))
try? fileStorage.remove(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, appearance: .dark))
try? fileStorage.remove(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, appearance: .light))
try? fileStorage.remove(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, appearance: .sepia))
}
}
}
Expand All @@ -211,8 +213,8 @@ extension AnnotationPreviewController {
/// - parameter libraryId: Library identifier of item.
/// - parameter isDark: `true` if dark mode is on, `false` otherwise.
/// - returns: `true` if preview is available, `false` otherwise.
func hasPreview(for key: String, parentKey: String, libraryId: LibraryIdentifier, isDark: Bool) -> Bool {
return fileStorage.has(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, isDark: isDark))
func hasPreview(for key: String, parentKey: String, libraryId: LibraryIdentifier, appearance: Appearance) -> Bool {
return fileStorage.has(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, appearance: appearance))
}

/// Loads cached preview for given annotation.
Expand All @@ -221,12 +223,12 @@ extension AnnotationPreviewController {
/// - parameter libraryId: Library identifier of item.
/// - parameter isDark: `true` if dark mode is on, `false` otherwise.
/// - parameter completed: Completion handler which contains loaded preview or `nil` if loading wasn't successful.
func preview(for key: String, parentKey: String, libraryId: LibraryIdentifier, isDark: Bool, completed: @escaping (UIImage?) -> Void) {
func preview(for key: String, parentKey: String, libraryId: LibraryIdentifier, appearance: Appearance, completed: @escaping (UIImage?) -> Void) {
queue.async { [weak self] in
guard let self else { return }

do {
let data = try fileStorage.read(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, isDark: isDark))
let data = try fileStorage.read(Files.annotationPreview(annotationKey: key, pdfKey: parentKey, libraryId: libraryId, appearance: appearance))
let image = UIImage(data: data)
DispatchQueue.main.async {
completed(image)
Expand All @@ -242,9 +244,16 @@ extension AnnotationPreviewController {
/// Creates and enqueues a render request for PSPDFKit rendering engine.
private func enqueue(data: EnqueuedData) {
let options = RenderOptions()
if data.isDark {
switch data.appearance {
case .dark:
options.invertRenderColor = true
options.filters = [.colorCorrectInverted]

case .sepia:
options.filters = [.sepia]

case .light:
break
}

let request = MutableRenderRequest(document: data.document)
Expand All @@ -261,7 +270,7 @@ extension AnnotationPreviewController {
task.completionHandler = { [weak self] image, error in
let result: Result<UIImage, Swift.Error> = image.flatMap({ .success($0) }) ?? .failure(error ?? Error.imageNotAvailable)
self?.queue.async {
self?.completeRequest(with: result, key: data.key, parentKey: data.parentKey, libraryId: data.libraryId, isDark: data.isDark, type: data.type)
self?.completeRequest(with: result, key: data.key, parentKey: data.parentKey, libraryId: data.libraryId, appearance: data.appearance, type: data.type)
}
}

Expand All @@ -271,18 +280,18 @@ extension AnnotationPreviewController {
}
}

private func completeRequest(with result: Result<UIImage, Swift.Error>, key: String, parentKey: String, libraryId: LibraryIdentifier, isDark: Bool, type: PreviewType) {
private func completeRequest(with result: Result<UIImage, Swift.Error>, key: String, parentKey: String, libraryId: LibraryIdentifier, appearance: Appearance, type: PreviewType) {
switch result {
case .success(let image):
switch type {
case .temporary(let subscriberKey):
perform(event: .success(image), subscriberKey: subscriberKey)

case .cachedOnly:
cache(image: image, key: key, pdfKey: parentKey, libraryId: libraryId, isDark: isDark)
cache(image: image, key: key, pdfKey: parentKey, libraryId: libraryId, appearance: appearance)

case .cachedAndReported:
cache(image: image, key: key, pdfKey: parentKey, libraryId: libraryId, isDark: isDark)
cache(image: image, key: key, pdfKey: parentKey, libraryId: libraryId, appearance: appearance)
observable.on(.next((key, parentKey, image)))
}

Expand All @@ -305,15 +314,15 @@ extension AnnotationPreviewController {
subscribers[subscriberKey] = nil
}

private func cache(image: UIImage, key: String, pdfKey: String, libraryId: LibraryIdentifier, isDark: Bool) {
private func cache(image: UIImage, key: String, pdfKey: String, libraryId: LibraryIdentifier, appearance: Appearance) {
autoreleasepool {
guard let data = image.pngData() else {
DDLogError("AnnotationPreviewController: can't create data from image")
return
}

do {
try fileStorage.write(data, to: Files.annotationPreview(annotationKey: key, pdfKey: pdfKey, libraryId: libraryId, isDark: isDark), options: .atomicWrite)
try fileStorage.write(data, to: Files.annotationPreview(annotationKey: key, pdfKey: pdfKey, libraryId: libraryId, appearance: appearance), options: .atomicWrite)
} catch let error {
DDLogError("AnnotationPreviewController: can't store preview - \(error)")
}
Expand Down
Loading

0 comments on commit dd9701d

Please sign in to comment.