Highlight
iOS 16 和 macOS Ventura 的 PDFKit 支持扫描 PDF 的按需 Live Text、从 CGImage 创建 PDF 页面、为每页安装交互式 overlay view,并能把 PencilKit 绘图保存为 PDF annotation。
核心内容
很多文档 App 都会走到同一个问题:PDF 负责显示和翻页,但用户想做的事越来越像编辑器。
扫描件里的文字要能选中。表格要能填。照片要能变成 PDF 页面。用户用 Apple Pencil 在页面上画出的标注,关闭文件后还要保留下来。
过去,额外绘制 PDF 内容通常要子类化 PDFPage 并重写绘制方法,或者直接创建自定义 PDFAnnotation。这类做法更接近一次性绘制。它不适合放一个可缩放、可响应手势、可实时编辑的视图到页面上。
(04:18)iOS 16 和 macOS Ventura 给 PDFKit 加了 overlay view。PDFKit 在页面进入视野前向 App 请求视图,负责把它约束到页面大小,并处理页面旋转。App 只需要返回一个 UIView 或 NSView。
这让 PencilKit 的使用方式变得直接:每个 PDF 页面上放一个 PKCanvasView。用户画线时,画的是活的视图。页面滚出屏幕时,把 PKDrawing 存回自定义 PDFPage。保存文件时,再把这些绘图转成 annotation。
详细内容
PDFKit 的四个核心对象
(00:54)这场 session 先回顾了 PDFKit 的对象模型。PDFView 放在界面里,负责显示、导航、缩放和复制文本。PDFDocument 表示整个 PDF 文件。PDFPage 表示每一页。PDFAnnotation 是页面上的可交互附加内容。
// 概念示例,非完整 API 签名
let pdfView = PDFView()
let document = PDFDocument(url: fileURL)
pdfView.document = document
let page = document?.page(at: 0)
let annotations = page?.annotations
关键点:
PDFView是放进布局的视图对象,SwiftUI 中可以通过UIViewRepresentable接入。PDFDocument是根对象,一份文档包含多个页面。page(at:)取出单页,页面负责渲染内容并保存该页资源。annotations是页面上的可编辑附加对象,后面的保存流程会用它承载 PencilKit 绘图。
Live Text 和表单识别
(02:08)PDFKit 现在支持 Live Text。扫描 PDF 即使只是位图、没有文本层,用户也可以选择和搜索其中的文字。
PDF 可以有很多页。PDFKit 不会在打开文档时对所有页面跑 OCR。它会在用户与某一页交互时按需处理该页。OCR 在原文档上就地完成,不需要复制文件。保存时也可以选择为整个文档保存识别出的文本。
(02:58)表单处理也有更新。PDFKit 会自动识别文档里的表单字段,即使原始 PDF 没有内置文本字段。用户可以用 Tab 在字段之间跳转并输入文字。
// 使用 PDFView 打开文档后,Live Text 与表单识别由 PDFKit 提供。
// 这里没有额外启用开关;示例只展示 PDFKit 的接入位置。
let pdfView = PDFView()
pdfView.document = PDFDocument(url: scannedPDFURL)
关键点:
PDFView是用户与 PDF 页面交互的位置,Live Text 的选择和搜索发生在这里。PDFDocument(url:)载入的文件可以是扫描 PDF;session 明确说明示例文档没有文本层。- Live Text 的 OCR 按页、按需执行,避免打开大文档时一次性处理全部页面。
- 表单字段识别同样由 PDFKit 处理,用户获得接近网页表单的键盘跳转体验。
从图片创建 PDF 页面
(03:14)iOS 16 和 macOS Ventura 提供了从图片创建 PDF 页面的新 API。输入图片使用 CGImageRef,PDFKit 会用高质量 JPEG 编码压缩图片。
这条路径适合扫描、拍照归档、把图片整理成 PDF 的 App。因为 CGImageRef 是 CoreGraphics 原生类型,App 不需要先把图片转成中间格式。
// 概念示例,非 API 签名:演示 session 提到的输入和选项。
let sourceImage: CGImage = scannedImage
let pageOptions = [
"MediaBox": pageSize,
"Rotation": pageRotation,
"UpscaleIfSmaller": true,
]
let pdfPage = PDFPage(image: sourceImage, options: pageOptions)
关键点:
sourceImage使用CGImage,对应 transcript 中的CGImageRef输入。MediaBox决定页面大小,可以贴合图片,也可以选择 Letter 这类纸张尺寸。Rotation用来指定纵向或横向。UpscaleIfSmaller表示小图也放大填充页面;大图仍会缩小以适配页面。- 这段是概念示例,不是官方 snippet;session 没有给出完整 Swift 签名。
用 PDFPageOverlayViewProvider 安装交互式视图
(05:47)overlay view 的入口是 PDFPageOverlayViewProvider。最重要的方法是 overlayViewFor page。PDFKit 需要显示某页时,会调用这个方法请求视图。
class Coordinator: NSObject, PDFPageOverlayViewProvider {
var pageToViewMapping = [PDFPage: UIView]()
func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
var resultView: PKCanvasView? = nil
if let overlayView = pageToViewMapping[page] {
resultView = overlayView
} else {
let canvasView = PKCanvasView(frame: .zero)
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .systemYellow, width: 20)
canvasView.backgroundColor = UIColor.clear
pageToViewMapping[page] = canvasView
resultView = canvasView
}
// If we have stored a drawing on the page, set it on the canvas
let page = page as! MyPDFPage
if let drawing = page.drawing {
resultView?.drawing = drawing;
}
return resultView
}
func pdfView(_ pdfView: PDFView, willEndDisplayingOverlayView
overlayView: UIView, for page: PDFPage) {
let overlayView = overlayView as! PKCanvasView
let page = page as! MyPDFPage
page.drawing = overlayView.drawing
pageToViewMapping.removeValue(forKey: page)
}
}
关键点:
Coordinator实现PDFPageOverlayViewProvider,它是 PDFKit 请求 overlay view 的对象。pageToViewMapping把PDFPage映射到已经创建的UIView,避免同一页重复创建 canvas。overlayViewFor page先查缓存,没有缓存时创建新的PKCanvasView。drawingPolicy = .anyInput允许 Pencil 或手指输入。PKInkingTool(.pen, color: .systemYellow, width: 20)设置示例中的黄色画笔。backgroundColor = UIColor.clear让 PDF 页面内容透出来。MyPDFPage是示例里的PDFPage子类,只增加了一个drawing属性。willEndDisplayingOverlayView在页面滚出视野时调用,把当前PKCanvasView.drawing存回页面,并移除缓存视图。
把 PencilKit 绘图保存成 PDF annotation
(09:28)overlay view 只解决屏幕上的交互。保存文件时,还要把绘图写回 PDF。
PDF annotation 有两个对这件事有用的特性。第一,它可以有 appearance stream,也就是 PDF 绘制命令流,用来保证在 Adobe Reader、Chrome 等阅读器里显示一致。第二,它以字典形式存储,可以放私有 key/value 数据,供下次打开时恢复编辑状态。
// Implement a subclass with a drawing override
class MyPDFAnnotation: PDFAnnotation {
override func draw(with box: PDFDisplayBox, in context: CGContext) {
UIGraphicsPushContext(context)
context.saveGState()
let page = self.page as! MyPDFPage
if let drawing = page.drawing {
let image = drawing.image(from: drawing.bounds, scale: 1)
image.draw(in: drawing.bounds)
}
context.restoreGState()
UIGraphicsPopContext()
}
}
关键点:
MyPDFAnnotation继承PDFAnnotation,目的是重写绘制行为。draw(with:in:)会在 PDFKit 保存 annotation 的 appearance stream 时被调用。UIGraphicsPushContext(context)把 Core Graphics context 接到 UIKit 绘制环境。context.saveGState()和restoreGState()包住自定义绘制,避免污染外层绘制状态。self.page as! MyPDFPage取回带drawing属性的自定义页面。drawing.image(from:scale:)把PKDrawing渲染成图片。image.draw(in:)把图片画进 annotation 的 bounds。
保存 annotation,并选择是否 burn in
(10:35)示例在 UIDocument 的 contents(forType:) 中遍历所有页面。每个有绘图的页面都会获得一个自定义 annotation。绘图本身用 NSKeyedArchiver 编码后,写入 annotation 的私有 key。
override func contents(forType typeName: String) throws -> Any {
if let pdfDocument = pdfDocument {
// Go through all pages in the document
for i in 0...pdfDocument.pageCount-1 {
if let page = pdfDocument.page(at: i) {
if let drawing = (page as! MyPDFPage).drawing {
// Create an annotation of our custom subclass
let newAnnotation = MyPDFAnnotation(bounds: drawing.bounds,
forType: .stamp, withProperties: nil)
// Add our custom data
let codedData = try! NSKeyedArchiver.archivedData(withRootObject: drawing,
requiringSecureCoding: true)
newAnnotation.setValue(codedData,
forAnnotationKey: PDFAnnotationKey(rawValue: "drawingData"))
// Add our annotation to the page
page.addAnnotation(newAnnotation)
}
}
}
// -- Option #1: Save the document to a data representation
if let resultData = pdfDocument.dataRepresentation() {
return resultData
}
// -- Option #2: Save the document to a data representation and "burn in" annotations
let options = [PDFDocumentWriteOption.burnInAnnotationsOption: true]
if let resultData = pdfDocument.dataRepresentation(options: options) {
return resultData
}
}
// Fall through to returning empty data
return Data()
}
关键点:
contents(forType:)是UIDocument写出内容的位置。0...pdfDocument.pageCount-1遍历所有 PDF 页面。(page as! MyPDFPage).drawing只处理已经有 PencilKit 绘图的页面。MyPDFAnnotation(bounds:forType:withProperties:)创建自定义 annotation,示例类型是.stamp。NSKeyedArchiver.archivedData把PKDrawing编码成数据,供下次打开后恢复 round-trip editing。setValue(_:forAnnotationKey:)用私有 keydrawingData保存绘图数据。page.addAnnotation(newAnnotation)把 annotation 挂到页面上。dataRepresentation()返回可写出的 PDF 数据;annotation 仍可被接收方移动、缩放或删除。burnInAnnotationsOption: true会把 annotation 烧录到页面内容里,适合不希望接收方继续编辑标注的文件。
控制 PDF 写出体积和网络加载
(11:50)iOS 16 和 macOS Ventura 还开放了更多 PDF 写出选项。
默认情况下,CoreGraphics 会尽量保存图片质量,使用全分辨率和无损压缩。这适合大幅打印,但对屏幕阅读可能导致文件过大。saveAllImagesAsJPEG 会把图片用 JPEG 编码保存。optimizeImagesForScreen 会把图片降采样到 HiDPI 屏幕分辨率上限。两者可以一起使用。
(12:41)createLinearizedPDF 用来创建适合网络加载的 linearized PDF。它把显示第一页所需的数据放到文件开头,浏览器可以先显示第一页,再继续加载剩余内容。
// 概念示例,非完整 API 签名:session 说明这些选项传给 dataRepresentation 或 writeToURL。
let options = [
"saveAllImagesAsJPEG": true,
"optimizeImagesForScreen": true,
"createLinearizedPDF": true,
]
let data = pdfDocument.dataRepresentation(options: options)
关键点:
saveAllImagesAsJPEG适合降低包含大量图片的 PDF 文件体积。optimizeImagesForScreen面向屏幕阅读,会降低过高分辨率图片的成本。createLinearizedPDF改善网络分发场景下的首屏显示速度。- 这些写出选项可以传给
PDFDocument的dataRepresentation或writeToURL。 - 这段是概念示例;session 只给出了
burnInAnnotationsOption的完整 Swift 写法。
核心启发
-
做一个扫描 PDF 的文字检索器
- 做什么:打开扫描件后,让用户直接搜索其中的文字。
- 为什么值得做:PDFKit 对无文本层扫描 PDF 提供按需 Live Text,不需要 App 自己在打开时 OCR 全文档。
- 怎么开始:用
PDFView加载PDFDocument,把交互入口放在页面视图上,保存时提供保存识别文本的路径。
-
做一个图片转 PDF 的归档工具
- 做什么:把拍照或导入的图片整理成固定纸张尺寸的 PDF。
- 为什么值得做:新 API 直接吃
CGImageRef,PDFKit 负责 JPEG 压缩,并提供页面尺寸、方向、小图放大等选项。 - 怎么开始:把扫描图片保留为
CGImage,为每页设置 MediaBox、Rotation 和 UpscaleIfSmaller,再组合成PDFDocument。
-
做一个带 Apple Pencil 标注的 PDF 阅读器
- 做什么:用户翻阅 PDF 时可以直接在页面上手写批注。
- 为什么值得做:
PDFPageOverlayViewProvider允许每页叠加PKCanvasView,PDFKit 负责按需创建、布局和旋转。 - 怎么开始:实现
overlayViewFor page返回PKCanvasView,在willEndDisplayingOverlayView中把PKDrawing存回自定义PDFPage。
-
做一个可继续编辑的 PDF 批注保存流程
- 做什么:保存手写标注后,下次打开还能继续编辑。
- 为什么值得做:PDF annotation 能保存 appearance stream,也能用私有 key/value 存原始
PKDrawing数据。 - 怎么开始:子类化
PDFAnnotation重写draw(with:in:),保存时把PKDrawing编码到drawingData,打开时再读回 overlay view。
-
做一个适合网页分发的 PDF 导出选项
- 做什么:给用户提供“小文件”“适合屏幕”“适合网页预览”等导出开关。
- 为什么值得做:PDFKit 的 JPEG、屏幕优化和 linearized PDF 写出选项分别对应体积、屏幕阅读和首屏加载。
- 怎么开始:在导出面板里把这些选项映射到
PDFDocument的dataRepresentation或writeToURL写出参数。
关联 Session
- Add Live Text interaction to your app — 讲解如何把 Live Text 接入静态图片和暂停视频帧,和 PDFKit 的扫描 PDF 文字交互互补。
- Capture machine-readable codes and text with VisionKit — 展示 VisionKit 如何从实时相机画面捕获文字和码,适合理解系统文字识别能力的另一条入口。
- What’s new in Vision — 介绍 Vision 的文字识别、条码检测和任务调试更新,是 Live Text 背后识别能力的相关背景。
- Use SwiftUI with UIKit — 说明 UIKit 视图和 SwiftUI 的组合方式,适合把
PDFView接入 SwiftUI App 时参考。
评论
GitHub Issues · utterances