v0.1 Comments and basic functionality work
This commit is contained in:
117
scripts/build-app.sh
Executable file
117
scripts/build-app.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_NAME="I Hate PDFs"
|
||||
EXECUTABLE_NAME="IHatePDFs"
|
||||
CONFIGURATION="${CONFIGURATION:-release}"
|
||||
if [[ -z "${ARCHS+x}" && "$CONFIGURATION" == "release" ]]; then
|
||||
ARCHS="arm64 x86_64"
|
||||
else
|
||||
ARCHS="${ARCHS:-}"
|
||||
fi
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DIST_DIR="$ROOT_DIR/dist"
|
||||
APP_DIR="$DIST_DIR/$APP_NAME.app"
|
||||
CONTENTS_DIR="$APP_DIR/Contents"
|
||||
MACOS_DIR="$CONTENTS_DIR/MacOS"
|
||||
RESOURCES_DIR="$CONTENTS_DIR/Resources"
|
||||
ICON_SOURCE="$ROOT_DIR/ihatepdf.png"
|
||||
ICON_NAME="AppIcon"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
SWIFT_BUILD_ARGS=(-c "$CONFIGURATION")
|
||||
for ARCH in $ARCHS; do
|
||||
SWIFT_BUILD_ARGS+=(--arch "$ARCH")
|
||||
done
|
||||
|
||||
swift build "${SWIFT_BUILD_ARGS[@]}"
|
||||
BUILD_DIR="$(swift build "${SWIFT_BUILD_ARGS[@]}" --show-bin-path)"
|
||||
|
||||
rm -rf "$APP_DIR"
|
||||
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
|
||||
cp "$BUILD_DIR/$EXECUTABLE_NAME" "$MACOS_DIR/$EXECUTABLE_NAME"
|
||||
|
||||
if [[ ! -f "$ICON_SOURCE" ]]; then
|
||||
echo "Missing app icon source: $ICON_SOURCE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ICONSET_DIR="$DIST_DIR/$ICON_NAME.iconset"
|
||||
rm -rf "$ICONSET_DIR"
|
||||
mkdir -p "$ICONSET_DIR"
|
||||
|
||||
make_icon() {
|
||||
local pixels="$1"
|
||||
local output="$2"
|
||||
sips -s format png --resampleHeightWidth "$pixels" "$pixels" "$ICON_SOURCE" --out "$ICONSET_DIR/$output" >/dev/null
|
||||
}
|
||||
|
||||
make_icon 16 "icon_16x16.png"
|
||||
make_icon 32 "icon_16x16@2x.png"
|
||||
make_icon 32 "icon_32x32.png"
|
||||
make_icon 64 "icon_32x32@2x.png"
|
||||
make_icon 128 "icon_128x128.png"
|
||||
make_icon 256 "icon_128x128@2x.png"
|
||||
make_icon 256 "icon_256x256.png"
|
||||
make_icon 512 "icon_256x256@2x.png"
|
||||
make_icon 512 "icon_512x512.png"
|
||||
make_icon 1024 "icon_512x512@2x.png"
|
||||
|
||||
iconutil -c icns "$ICONSET_DIR" -o "$RESOURCES_DIR/$ICON_NAME.icns"
|
||||
rm -rf "$ICONSET_DIR"
|
||||
|
||||
cat > "$CONTENTS_DIR/Info.plist" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$EXECUTABLE_NAME</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.ihatepdfs.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$APP_NAME</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$APP_NAME</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>$ICON_NAME</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>PDF Document</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.adobe.pdf</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>MIT License</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
echo "Built $APP_DIR"
|
||||
22
scripts/make-dmg.sh
Executable file
22
scripts/make-dmg.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
APP_NAME="I Hate PDFs"
|
||||
DIST_DIR="$ROOT_DIR/dist"
|
||||
APP_DIR="$DIST_DIR/$APP_NAME.app"
|
||||
DMG_PATH="$DIST_DIR/IHatePDFs.dmg"
|
||||
|
||||
if [[ ! -d "$APP_DIR" ]]; then
|
||||
"$ROOT_DIR/scripts/build-app.sh"
|
||||
fi
|
||||
|
||||
rm -f "$DMG_PATH"
|
||||
hdiutil create \
|
||||
-volname "$APP_NAME" \
|
||||
-srcfolder "$APP_DIR" \
|
||||
-ov \
|
||||
-format UDZO \
|
||||
"$DMG_PATH"
|
||||
|
||||
echo "Created $DMG_PATH"
|
||||
283
scripts/verify-pdf-annotations.swift
Normal file
283
scripts/verify-pdf-annotations.swift
Normal file
@@ -0,0 +1,283 @@
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
struct AnnotationSummary {
|
||||
var highlights = 0
|
||||
var selectedTextComments = 0
|
||||
var underlines = 0
|
||||
var textNotes = 0
|
||||
var replies = 0
|
||||
var freeText = 0
|
||||
var popups = 0
|
||||
}
|
||||
|
||||
enum VerificationError: Error, CustomStringConvertible {
|
||||
case unreadablePDF(String)
|
||||
case missingPageAnnotations(Int)
|
||||
case missingDictionary(page: Int, index: Int)
|
||||
case missingName(page: Int, index: Int, key: String)
|
||||
case missingString(page: Int, index: Int, key: String)
|
||||
case missingArray(page: Int, index: Int, key: String)
|
||||
case missingPopup(page: Int, index: Int, subtype: String)
|
||||
case missingPopupParent(page: Int, index: Int)
|
||||
case missingExpectedSubtype(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .unreadablePDF(let path):
|
||||
return "Unable to read PDF at \(path)"
|
||||
case .missingPageAnnotations(let page):
|
||||
return "Page \(page) has no /Annots array"
|
||||
case .missingDictionary(let page, let index):
|
||||
return "Annotation \(index) on page \(page) is not a dictionary"
|
||||
case .missingName(let page, let index, let key):
|
||||
return "Annotation \(index) on page \(page) is missing name key /\(key)"
|
||||
case .missingString(let page, let index, let key):
|
||||
return "Annotation \(index) on page \(page) is missing string key /\(key)"
|
||||
case .missingArray(let page, let index, let key):
|
||||
return "Annotation \(index) on page \(page) is missing array key /\(key)"
|
||||
case .missingPopup(let page, let index, let subtype):
|
||||
return "\(subtype) annotation \(index) on page \(page) is missing a /Popup dictionary"
|
||||
case .missingPopupParent(let page, let index):
|
||||
return "Popup annotation \(index) on page \(page) is missing a /Parent dictionary"
|
||||
case .missingExpectedSubtype(let subtype):
|
||||
return "Expected at least one /\(subtype) annotation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let inputPath = CommandLine.arguments.dropFirst().first ?? "dist/annotation-verification.pdf"
|
||||
let inputURL = URL(fileURLWithPath: inputPath)
|
||||
|
||||
guard let document = CGPDFDocument(inputURL as CFURL) else {
|
||||
throw VerificationError.unreadablePDF(inputURL.path)
|
||||
}
|
||||
|
||||
var summary = AnnotationSummary()
|
||||
|
||||
for pageNumber in 1...document.numberOfPages {
|
||||
guard let page = document.page(at: pageNumber),
|
||||
let pageDictionary = page.dictionary
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
var annotationsArray: CGPDFArrayRef?
|
||||
guard CGPDFDictionaryGetArray(pageDictionary, "Annots", &annotationsArray),
|
||||
let annotationsArray
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
for annotationIndex in 0..<CGPDFArrayGetCount(annotationsArray) {
|
||||
let annotation = try annotationDictionary(
|
||||
in: annotationsArray,
|
||||
at: annotationIndex,
|
||||
page: pageNumber
|
||||
)
|
||||
let subtype = try nameValue(
|
||||
in: annotation,
|
||||
key: "Subtype",
|
||||
page: pageNumber,
|
||||
index: annotationIndex
|
||||
)
|
||||
|
||||
switch subtype {
|
||||
case "Highlight":
|
||||
try requireMarkupKeys(in: annotation, page: pageNumber, index: annotationIndex)
|
||||
try requirePopup(in: annotation, page: pageNumber, index: annotationIndex, subtype: "Highlight")
|
||||
if hasString(in: annotation, key: "IHatePDFsKind") {
|
||||
try requireString(in: annotation, key: "IHatePDFsKind", page: pageNumber, index: annotationIndex)
|
||||
summary.selectedTextComments += 1
|
||||
} else {
|
||||
summary.highlights += 1
|
||||
}
|
||||
case "Underline":
|
||||
summary.underlines += 1
|
||||
try requireMarkupKeys(in: annotation, page: pageNumber, index: annotationIndex)
|
||||
try requirePopup(in: annotation, page: pageNumber, index: annotationIndex, subtype: "Underline")
|
||||
case "Text":
|
||||
try requireTextKeys(in: annotation, page: pageNumber, index: annotationIndex)
|
||||
try requirePopup(in: annotation, page: pageNumber, index: annotationIndex, subtype: "Text")
|
||||
|
||||
if hasString(in: annotation, key: "IRT") || hasString(in: annotation, key: "RT") {
|
||||
summary.replies += 1
|
||||
try requireString(in: annotation, key: "IRT", page: pageNumber, index: annotationIndex)
|
||||
try requireString(in: annotation, key: "RT", page: pageNumber, index: annotationIndex)
|
||||
} else {
|
||||
summary.textNotes += 1
|
||||
}
|
||||
case "FreeText":
|
||||
summary.freeText += 1
|
||||
try requireString(in: annotation, key: "Contents", page: pageNumber, index: annotationIndex)
|
||||
try requireString(in: annotation, key: "T", page: pageNumber, index: annotationIndex)
|
||||
try requireString(in: annotation, key: "M", page: pageNumber, index: annotationIndex)
|
||||
try requireString(in: annotation, key: "DA", page: pageNumber, index: annotationIndex)
|
||||
try requireArray(in: annotation, key: "C", page: pageNumber, index: annotationIndex)
|
||||
try requireArray(in: annotation, key: "Rect", page: pageNumber, index: annotationIndex)
|
||||
case "Popup":
|
||||
summary.popups += 1
|
||||
var parentDictionary: CGPDFDictionaryRef?
|
||||
guard CGPDFDictionaryGetDictionary(annotation, "Parent", &parentDictionary) else {
|
||||
throw VerificationError.missingPopupParent(page: pageNumber, index: annotationIndex)
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard summary.highlights > 0 else {
|
||||
throw VerificationError.missingExpectedSubtype("Highlight")
|
||||
}
|
||||
guard summary.selectedTextComments > 0 else {
|
||||
throw VerificationError.missingExpectedSubtype("selected-text Comment")
|
||||
}
|
||||
guard summary.underlines > 0 else {
|
||||
throw VerificationError.missingExpectedSubtype("Underline")
|
||||
}
|
||||
guard summary.textNotes > 0 else {
|
||||
throw VerificationError.missingExpectedSubtype("Text")
|
||||
}
|
||||
guard summary.replies > 0 else {
|
||||
throw VerificationError.missingExpectedSubtype("Text reply")
|
||||
}
|
||||
guard summary.freeText > 0 else {
|
||||
throw VerificationError.missingExpectedSubtype("FreeText")
|
||||
}
|
||||
guard summary.popups >= 5 else {
|
||||
throw VerificationError.missingExpectedSubtype("Popup")
|
||||
}
|
||||
|
||||
print("Verified raw PDF annotation dictionaries in \(inputURL.path): \(summary.highlights) highlight, \(summary.selectedTextComments) selected-text comment, \(summary.underlines) underline, \(summary.textNotes) text note, \(summary.replies) reply, \(summary.freeText) free-text, \(summary.popups) popups.")
|
||||
|
||||
func annotationDictionary(
|
||||
in array: CGPDFArrayRef,
|
||||
at index: Int,
|
||||
page: Int
|
||||
) throws -> CGPDFDictionaryRef {
|
||||
var object: CGPDFObjectRef?
|
||||
guard CGPDFArrayGetObject(array, index, &object),
|
||||
let object
|
||||
else {
|
||||
throw VerificationError.missingDictionary(page: page, index: index)
|
||||
}
|
||||
|
||||
var dictionary: CGPDFDictionaryRef?
|
||||
guard CGPDFObjectGetValue(object, .dictionary, &dictionary),
|
||||
let dictionary
|
||||
else {
|
||||
throw VerificationError.missingDictionary(page: page, index: index)
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
|
||||
func nameValue(
|
||||
in dictionary: CGPDFDictionaryRef,
|
||||
key: String,
|
||||
page: Int,
|
||||
index: Int
|
||||
) throws -> String {
|
||||
var name: UnsafePointer<Int8>?
|
||||
guard CGPDFDictionaryGetName(dictionary, key, &name),
|
||||
let name
|
||||
else {
|
||||
throw VerificationError.missingName(page: page, index: index, key: key)
|
||||
}
|
||||
|
||||
return String(cString: name)
|
||||
}
|
||||
|
||||
func requireString(
|
||||
in dictionary: CGPDFDictionaryRef,
|
||||
key: String,
|
||||
page: Int,
|
||||
index: Int
|
||||
) throws {
|
||||
var value: CGPDFStringRef?
|
||||
guard CGPDFDictionaryGetString(dictionary, key, &value),
|
||||
value != nil
|
||||
else {
|
||||
throw VerificationError.missingString(page: page, index: index, key: key)
|
||||
}
|
||||
}
|
||||
|
||||
func requireName(
|
||||
in dictionary: CGPDFDictionaryRef,
|
||||
key: String,
|
||||
page: Int,
|
||||
index: Int
|
||||
) throws {
|
||||
var value: UnsafePointer<Int8>?
|
||||
guard CGPDFDictionaryGetName(dictionary, key, &value),
|
||||
value != nil
|
||||
else {
|
||||
throw VerificationError.missingName(page: page, index: index, key: key)
|
||||
}
|
||||
}
|
||||
|
||||
func hasString(
|
||||
in dictionary: CGPDFDictionaryRef,
|
||||
key: String
|
||||
) -> Bool {
|
||||
var value: CGPDFStringRef?
|
||||
return CGPDFDictionaryGetString(dictionary, key, &value) && value != nil
|
||||
}
|
||||
|
||||
func requireArray(
|
||||
in dictionary: CGPDFDictionaryRef,
|
||||
key: String,
|
||||
page: Int,
|
||||
index: Int
|
||||
) throws {
|
||||
var value: CGPDFArrayRef?
|
||||
guard CGPDFDictionaryGetArray(dictionary, key, &value),
|
||||
value != nil
|
||||
else {
|
||||
throw VerificationError.missingArray(page: page, index: index, key: key)
|
||||
}
|
||||
}
|
||||
|
||||
func requireMarkupKeys(
|
||||
in dictionary: CGPDFDictionaryRef,
|
||||
page: Int,
|
||||
index: Int
|
||||
) throws {
|
||||
try requireString(in: dictionary, key: "Contents", page: page, index: index)
|
||||
try requireArray(in: dictionary, key: "QuadPoints", page: page, index: index)
|
||||
try requireArray(in: dictionary, key: "C", page: page, index: index)
|
||||
try requireString(in: dictionary, key: "T", page: page, index: index)
|
||||
try requireString(in: dictionary, key: "M", page: page, index: index)
|
||||
}
|
||||
|
||||
func requireTextKeys(
|
||||
in dictionary: CGPDFDictionaryRef,
|
||||
page: Int,
|
||||
index: Int
|
||||
) throws {
|
||||
try requireString(in: dictionary, key: "Contents", page: page, index: index)
|
||||
try requireName(in: dictionary, key: "Name", page: page, index: index)
|
||||
try requireArray(in: dictionary, key: "C", page: page, index: index)
|
||||
try requireString(in: dictionary, key: "T", page: page, index: index)
|
||||
try requireString(in: dictionary, key: "M", page: page, index: index)
|
||||
}
|
||||
|
||||
func requirePopup(
|
||||
in dictionary: CGPDFDictionaryRef,
|
||||
page: Int,
|
||||
index: Int,
|
||||
subtype: String
|
||||
) throws {
|
||||
var popupDictionary: CGPDFDictionaryRef?
|
||||
guard CGPDFDictionaryGetDictionary(dictionary, "Popup", &popupDictionary),
|
||||
let popupDictionary
|
||||
else {
|
||||
throw VerificationError.missingPopup(page: page, index: index, subtype: subtype)
|
||||
}
|
||||
|
||||
let popupSubtype = try nameValue(in: popupDictionary, key: "Subtype", page: page, index: index)
|
||||
guard popupSubtype == "Popup" else {
|
||||
throw VerificationError.missingPopup(page: page, index: index, subtype: subtype)
|
||||
}
|
||||
}
|
||||
180
scripts/verify-sample-pdf.swift
Normal file
180
scripts/verify-sample-pdf.swift
Normal file
@@ -0,0 +1,180 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import PDFKit
|
||||
|
||||
let outputURL = URL(fileURLWithPath: CommandLine.arguments.dropFirst().first ?? "dist/annotation-verification.pdf")
|
||||
try FileManager.default.createDirectory(
|
||||
at: outputURL.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true
|
||||
)
|
||||
|
||||
let page = PDFPage()
|
||||
let document = PDFDocument()
|
||||
document.insert(page, at: 0)
|
||||
|
||||
let verificationDate = Date(timeIntervalSince1970: 1_797_000_000)
|
||||
|
||||
let highlight = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 620, width: 260, height: 24),
|
||||
forType: .highlight,
|
||||
withProperties: nil
|
||||
)
|
||||
highlight.markupType = .highlight
|
||||
highlight.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.24)
|
||||
highlight.quadrilateralPoints = quadPoints(width: 260, height: 24)
|
||||
standardize(
|
||||
highlight,
|
||||
name: "verify-highlight",
|
||||
contents: "This is a standards-compliant PDF highlight comment.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(highlight)
|
||||
addPopup(for: highlight, bounds: CGRect(x: 352, y: 592, width: 240, height: 110))
|
||||
|
||||
let selectedTextComment = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 594, width: 260, height: 22),
|
||||
forType: .highlight,
|
||||
withProperties: nil
|
||||
)
|
||||
selectedTextComment.markupType = .highlight
|
||||
selectedTextComment.color = NSColor(calibratedRed: 0.88, green: 0.72, blue: 0.46, alpha: 0.10)
|
||||
selectedTextComment.quadrilateralPoints = quadPoints(width: 260, height: 22)
|
||||
standardize(
|
||||
selectedTextComment,
|
||||
name: "verify-selected-text-comment",
|
||||
contents: "This selected-text comment is saved as standard PDF markup with popup contents.",
|
||||
author: "Professor"
|
||||
)
|
||||
_ = selectedTextComment.setValue("Comment", forAnnotationKey: PDFAnnotationKey(rawValue: "IHatePDFsKind"))
|
||||
page.addAnnotation(selectedTextComment)
|
||||
addPopup(for: selectedTextComment, bounds: CGRect(x: 352, y: 472, width: 240, height: 110))
|
||||
|
||||
let underline = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 570, width: 260, height: 24),
|
||||
forType: .underline,
|
||||
withProperties: nil
|
||||
)
|
||||
underline.markupType = .underline
|
||||
underline.color = NSColor(calibratedRed: 0.48, green: 0.53, blue: 0.62, alpha: 0.56)
|
||||
underline.quadrilateralPoints = quadPoints(width: 260, height: 24)
|
||||
standardize(
|
||||
underline,
|
||||
name: "verify-underline",
|
||||
contents: "This underline comment should remain openable.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(underline)
|
||||
addPopup(for: underline, bounds: CGRect(x: 352, y: 540, width: 240, height: 110))
|
||||
|
||||
let textNote = PDFAnnotation(
|
||||
bounds: CGRect(x: 360, y: 620, width: 28, height: 28),
|
||||
forType: .text,
|
||||
withProperties: nil
|
||||
)
|
||||
textNote.iconType = .note
|
||||
textNote.color = NSColor(calibratedRed: 0.64, green: 0.59, blue: 0.49, alpha: 0.90)
|
||||
standardize(
|
||||
textNote,
|
||||
name: "verify-text-note",
|
||||
contents: "This standard PDF text annotation remains visible in common PDF readers.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(textNote)
|
||||
|
||||
let reply = PDFAnnotation(
|
||||
bounds: CGRect(x: 402, y: 586, width: 24, height: 24),
|
||||
forType: .text,
|
||||
withProperties: nil
|
||||
)
|
||||
reply.iconType = .comment
|
||||
reply.color = NSColor(calibratedRed: 0.52, green: 0.58, blue: 0.60, alpha: 0.88)
|
||||
standardize(
|
||||
reply,
|
||||
name: "verify-reply",
|
||||
contents: "This reply is saved as a visible PDF text annotation.",
|
||||
author: "Reader"
|
||||
)
|
||||
_ = reply.setValue("verify-text-note", forAnnotationKey: PDFAnnotationKey(rawValue: "IRT"))
|
||||
_ = reply.setValue("R", forAnnotationKey: PDFAnnotationKey(rawValue: "RT"))
|
||||
page.addAnnotation(reply)
|
||||
|
||||
let freeText = PDFAnnotation(
|
||||
bounds: CGRect(x: 72, y: 500, width: 260, height: 50),
|
||||
forType: .freeText,
|
||||
withProperties: nil
|
||||
)
|
||||
freeText.font = NSFont.systemFont(ofSize: 13)
|
||||
freeText.fontColor = NSColor(calibratedWhite: 0.22, alpha: 1)
|
||||
freeText.color = NSColor(calibratedRed: 0.91, green: 0.86, blue: 0.75, alpha: 0.32)
|
||||
freeText.alignment = .left
|
||||
let border = PDFBorder()
|
||||
border.lineWidth = 0.75
|
||||
freeText.border = border
|
||||
standardize(
|
||||
freeText,
|
||||
name: "verify-free-text",
|
||||
contents: "Free text remains visible on the PDF page.",
|
||||
author: "Professor"
|
||||
)
|
||||
page.addAnnotation(freeText)
|
||||
|
||||
guard document.write(to: outputURL) else {
|
||||
fatalError("Unable to write \(outputURL.path)")
|
||||
}
|
||||
|
||||
let reopened = PDFDocument(url: outputURL)!
|
||||
let annotations = reopened.page(at: 0)!.annotations
|
||||
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("highlight") == true })
|
||||
precondition(annotations.contains { matches($0, .highlight) && $0.contents?.contains("selected-text comment") == true })
|
||||
precondition(annotations.contains { matches($0, .underline) && $0.contents?.contains("underline") == true })
|
||||
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("text annotation") == true })
|
||||
precondition(annotations.contains { matches($0, .text) && $0.contents?.contains("reply") == true })
|
||||
precondition(annotations.contains { matches($0, .freeText) && $0.contents?.contains("Free text") == true })
|
||||
|
||||
print("Verified standard PDF annotations in \(outputURL.path)")
|
||||
|
||||
func standardize(
|
||||
_ annotation: PDFAnnotation,
|
||||
name: String,
|
||||
contents: String,
|
||||
author: String
|
||||
) {
|
||||
annotation.contents = contents
|
||||
annotation.userName = author
|
||||
annotation.modificationDate = verificationDate
|
||||
annotation.shouldDisplay = true
|
||||
annotation.shouldPrint = true
|
||||
_ = annotation.setValue(name, forAnnotationKey: .name)
|
||||
_ = annotation.setValue(author, forAnnotationKey: .textLabel)
|
||||
_ = annotation.setValue(verificationDate, forAnnotationKey: .date)
|
||||
_ = annotation.setValue("D:20261215132000Z00'00'", forAnnotationKey: PDFAnnotationKey(rawValue: "CreationDate"))
|
||||
_ = annotation.setValue("Unmarked", forAnnotationKey: PDFAnnotationKey(rawValue: "State"))
|
||||
}
|
||||
|
||||
func addPopup(for annotation: PDFAnnotation, bounds: CGRect) {
|
||||
let popup = PDFAnnotation(bounds: bounds, forType: .popup, withProperties: nil)
|
||||
popup.contents = annotation.contents
|
||||
popup.userName = annotation.userName
|
||||
popup.modificationDate = annotation.modificationDate
|
||||
popup.isOpen = false
|
||||
popup.shouldDisplay = true
|
||||
popup.shouldPrint = true
|
||||
annotation.popup = popup
|
||||
page.addAnnotation(popup)
|
||||
}
|
||||
|
||||
func quadPoints(width: CGFloat, height: CGFloat) -> [NSValue] {
|
||||
[
|
||||
NSValue(point: CGPoint(x: 0, y: height)),
|
||||
NSValue(point: CGPoint(x: width, y: height)),
|
||||
NSValue(point: CGPoint(x: 0, y: 0)),
|
||||
NSValue(point: CGPoint(x: width, y: 0))
|
||||
]
|
||||
}
|
||||
|
||||
func matches(_ annotation: PDFAnnotation, _ subtype: PDFAnnotationSubtype) -> Bool {
|
||||
guard let type = annotation.type else { return false }
|
||||
let raw = subtype.rawValue
|
||||
let normalized = raw.hasPrefix("/") ? String(raw.dropFirst()) : raw
|
||||
return type == raw || type == normalized
|
||||
}
|
||||
Reference in New Issue
Block a user