77 lines
3.4 KiB
Swift
77 lines
3.4 KiB
Swift
import Foundation
|
|
import PDFKit
|
|
import PencilKit
|
|
import UIKit
|
|
|
|
@MainActor
|
|
struct DefaultPDFSigningService: PDFSigningService {
|
|
var outputDirectory: URL = URL.applicationSupportDirectory
|
|
.appending(path: "SignedLeadDocuments", directoryHint: .isDirectory)
|
|
|
|
func sign(source: URL, drawing: PKDrawing, placement: PDFSignaturePlacement) async throws -> URL {
|
|
try Task.checkCancellation()
|
|
guard !drawing.strokes.isEmpty else { throw PDFSigningError.emptySignature }
|
|
guard let document = PDFDocument(url: source), !document.isLocked else {
|
|
throw PDFSigningError.invalidDocument
|
|
}
|
|
guard placement.pageIndex >= 0,
|
|
let page = document.page(at: placement.pageIndex),
|
|
placement.bounds.width > 0, placement.bounds.height > 0,
|
|
page.bounds(for: .mediaBox).contains(placement.bounds) else {
|
|
throw PDFSigningError.invalidPlacement
|
|
}
|
|
|
|
// Fit the handwriting into the configured field without changing its aspect ratio.
|
|
// PencilKit uses a top-left origin; PDF page coordinates use a bottom-left origin.
|
|
let sourceBounds = drawing.bounds.insetBy(dx: -2, dy: -2)
|
|
let target = placement.bounds.insetBy(dx: 4, dy: 4)
|
|
guard target.width > 0, target.height > 0,
|
|
sourceBounds.width.isFinite, sourceBounds.height.isFinite else {
|
|
throw PDFSigningError.invalidPlacement
|
|
}
|
|
let scale = min(target.width / sourceBounds.width, target.height / sourceBounds.height)
|
|
let origin = CGPoint(x: target.midX - sourceBounds.width * scale / 2,
|
|
y: target.midY - sourceBounds.height * scale / 2)
|
|
let annotation = PDFAnnotation(bounds: placement.bounds, forType: .ink, withProperties: nil)
|
|
annotation.color = .black
|
|
let border = PDFBorder()
|
|
border.lineWidth = max(0.7, 2 * scale)
|
|
annotation.border = border
|
|
annotation.shouldPrint = true
|
|
|
|
for stroke in drawing.strokes {
|
|
let path = UIBezierPath()
|
|
var isFirst = true
|
|
for point in stroke.path.interpolatedPoints(by: .distance(1)) {
|
|
let location = point.location.applying(stroke.transform)
|
|
let converted = CGPoint(
|
|
x: origin.x + (location.x - sourceBounds.minX) * scale,
|
|
y: origin.y + (sourceBounds.maxY - location.y) * scale
|
|
)
|
|
if isFirst {
|
|
path.move(to: converted)
|
|
isFirst = false
|
|
} else {
|
|
path.addLine(to: converted)
|
|
}
|
|
}
|
|
// A tap is also a valid ink mark.
|
|
if path.isEmpty == false {
|
|
path.addLine(to: CGPoint(x: path.currentPoint.x + 0.01, y: path.currentPoint.y))
|
|
annotation.add(path)
|
|
}
|
|
}
|
|
page.addAnnotation(annotation)
|
|
guard let data = document.dataRepresentation() else { throw PDFSigningError.saveFailed }
|
|
try Task.checkCancellation()
|
|
do {
|
|
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
|
let destination = outputDirectory.appending(path: UUID().uuidString + ".pdf")
|
|
try data.write(to: destination, options: .atomic)
|
|
return destination
|
|
} catch {
|
|
throw PDFSigningError.saveFailed
|
|
}
|
|
}
|
|
}
|