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 } } }