You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于OverlayView将PencilKit批注烧录进PDF的技术问询

解决方案:将PencilKit批注烧录进PDF+S3外部PDF优化方案

一、核心需求实现:把PencilKit批注合并到PDF文件中

你当前的代码是将批注单独保存为.dat文件,现在需要修改为直接把PKDrawing渲染到PDF页面中,让批注和PDF合为一体。

关键代码修改

在OverlayCoordinator类中替换原来的saveDrawings方法,新增合并批注到PDF的逻辑:

class OverlayCoordinator: NSObject, PDFPageOverlayViewProvider, PDFViewDelegate {
    var pageToViewMapping = [PDFPage: PKCanvasView]()
    var toolPicker = PKToolPicker()
    var targetS3URL: URL? // 用于后续上传回S3的目标地址

    // 将批注合并到PDF并保存
    func mergeDrawingsIntoPDF(pdfView: PDFView) {
        guard let document = pdfView.document else { return }
        
        // 先清除页面上已有的墨水批注(避免重复添加)
        for page in document.pages {
            let inkAnnotations = page.annotations.filter { $0.type == .ink }
            inkAnnotations.forEach { page.removeAnnotation($0) }
        }

        // 将每个页面的PKDrawing转换为PDF批注
        for (page, canvasView) in pageToViewMapping {
            let pageBounds = page.bounds(for: .mediaBox)
            let scale = pdfView.scaleFactorForSizeToFit
            
            // 创建PDF墨水批注
            let inkAnnotation = PDFAnnotation(bounds: pageBounds, forType: .ink, withProperties: nil)
            inkAnnotation.color = .black // 可根据Pencil工具动态调整颜色
            
            // 把PKStroke转换为PDF路径
            let paths = canvasView.drawing.strokes.map { stroke -> PDFBezierPath in
                let path = PDFBezierPath()
                guard let firstPoint = stroke.points.first else { return path }
                path.move(to: firstPoint.location.applying(CGAffineTransform(scaleX: scale, y: scale)))
                
                for point in stroke.points.dropFirst() {
                    path.addLine(to: point.location.applying(CGAffineTransform(scaleX: scale, y: scale)))
                }
                path.lineWidth = CGFloat(stroke.toolWidth) * scale
                return path
            }
            inkAnnotation.addPaths(paths)
            page.addAnnotation(inkAnnotation)
        }

        // 保存到临时目录(避免占用永久存储)
        guard let pdfData = document.dataRepresentation() else { return }
        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("annotated_\(UUID().uuidString).pdf")
        
        do {
            try pdfData.write(to: tempURL)
            print("合并后的PDF已保存到临时目录")
            
            // 上传回S3(后续实现)
            if let s3URL = targetS3URL {
                uploadToS3(localURL: tempURL, targetURL: s3URL)
            }
            
            // 上传成功后删除临时文件
            try FileManager.default.removeItem(at: tempURL)
        } catch {
            print("PDF处理失败: \(error)")
        }
    }

    // S3上传逻辑(可结合AWS SDK优化)
    private func uploadToS3(localURL: URL, targetURL: URL) {
        var request = URLRequest(url: targetURL)
        request.httpMethod = "PUT"
        // 根据S3配置添加必要的请求头(如签名、Content-Type等)
        
        let task = URLSession.shared.uploadTask(with: request, fromFile: localURL) { _, response, error in
            if let error = error {
                print("S3上传失败: \(error)")
                return
            }
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                print("上传返回异常状态码")
                return
            }
            print("批注PDF已成功上传回S3")
        }
        task.resume()
    }

    // 其余原有代码...
}

二、外部S3 PDF优化方案:减少本地存储占用

1. 用URLSession流式下载PDF到临时目录

替代原来的Data(contentsOf:)同步下载,改用URLSession异步流式下载到系统临时目录,避免占用永久存储:

struct PDFDocumentView: View {
    let s3PDFURL = URL(string: "你的S3文件地址")!
    @State private var pdfTempURL: URL?
    @State private var saveAnnotations = false

    var body: some View {
        VStack {
            Button(action: {
                saveAnnotations.toggle()
            }) {
                Text("保存批注")
            }

            if let tempURL = pdfTempURL {
                PDFViewWrapper(pdfTempURL: tempURL, targetS3URL: s3PDFURL, saveAnnotations: $saveAnnotations)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .edgesIgnoringSafeArea(.all)
            } else {
                ProgressView("加载PDF中...")
                    .onAppear {
                        downloadPDFToTemp(from: s3PDFURL) { tempURL in
                            pdfTempURL = tempURL
                        }
                    }
            }
        }
    }

    // URLSession流式下载到临时目录
    private func downloadPDFToTemp(from url: URL, completion: @escaping (URL?) -> Void) {
        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("temp_\(UUID().uuidString).pdf")
        
        let task = URLSession.shared.downloadTask(with: url) { location, _, error in
            guard let location = location, error == nil else {
                completion(nil)
                return
            }
            do {
                try FileManager.default.moveItem(at: location, to: tempURL)
                completion(tempURL)
            } catch {
                print("临时文件移动失败: \(error)")
                completion(nil)
            }
        }
        task.resume()
    }
}

2. 优化PDFViewWrapper传递参数

修改PDFViewWrapper以接收临时PDF地址和S3目标地址,并在保存触发时调用合并逻辑:

struct PDFViewWrapper: UIViewControllerRepresentable {
    let pdfTempURL: URL
    let targetS3URL: URL
    @Binding var saveAnnotations: Bool
    let coordinator = OverlayCoordinator()

    func makeUIViewController(context: Context) -> UIViewController {
        coordinator.targetS3URL = targetS3URL
        
        let pdfView = PDFView()
        pdfView.pageOverlayViewProvider = coordinator
        pdfView.delegate = coordinator
        pdfView.isInMarkupMode = true
        pdfView.displayMode = .singlePageContinuous
        pdfView.displayDirection = .vertical
        
        if let document = PDFDocument(url: pdfTempURL) {
            pdfView.document = document
        }

        let viewController = UIViewController()
        viewController.view.addSubview(pdfView)
        pdfView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            pdfView.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
            pdfView.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
            pdfView.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor),
            pdfView.topAnchor.constraint(equalTo: viewController.view.topAnchor)
        ])

        // 视图消失时自动保存批注
        viewController.viewDidDisappear = { _ in
            if let pdfView = viewController.view.subviews.first as? PDFView {
                coordinator.mergeDrawingsIntoPDF(pdfView: pdfView)
            }
        }
        
        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        if saveAnnotations {
            if let pdfView = uiViewController.view.subviews.first as? PDFView {
                coordinator.mergeDrawingsIntoPDF(pdfView: pdfView)
            }
            saveAnnotations = false
        }
    }
}

三、核心优势

  1. 批注永久合并:直接将批注写入PDF文件,下次打开无需加载额外批注文件
  2. 减少本地存储:所有PDF操作都在临时目录完成,上传后自动清理,不占用用户永久存储空间
  3. 高效流式处理:用URLSession异步下载/上传,避免同步操作阻塞UI,同时降低内存占用

内容的提问来源于stack exchange,提问作者Joseph Garcia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 06:51:08