ARTICLE · 1070778
iOS PDF 工具怎么实现?从 PDFKit、标注、签名到扫描生成 PDF
iOS PDF 工具怎么实现?从 PDFKit、标注、签名到扫描生成 PDF
前面的系列,我们已经从文件浏览一路扩展到了压缩、图片、视频和音频处理:
01 iOS 文件浏览器架构02 GB 级大文件处理03 Wi-Fi 文件传输04 SMB / NAS05 WebDAV06 FTP07 ZIP / RAR / 7z08 图片转换与压缩09 视频转换与压缩10 音频格式转换
这一篇开始进入另外一个非常高频的文件类型:
对于普通用户来说,PDF 需求往往非常明确:
收到一份 PDF,怎么在 iPhone 上签字?
或者:
PDF 里怎么高亮、画线、写字?
又或者:
没有扫描仪,能不能直接拍照生成 PDF?
从产品角度看,这些都是一个 PDF 工具应该完成的事情。
但从技术实现上看,PDF 模块实际上包含多个不同能力:
PDF 浏览 页面渲染 缩略图 文本选择 Highlight Underline Strikeout Freehand Ink Signature 页面旋转 页面增删 图片转 PDF 相机扫描 文件保存 大型 PDF 内存控制 增量保存与文件替换
所以:
PDFService 不应该只是一个 PDFView 页面,而应该是一套完整的 Document Processing 模块。
1. PDF 为什么和图片不一样?
很多人第一反应是:
PDF 不就是很多张图片放在一起吗?
不完全是。
一个 PDF 页面里可能包含:
TextVector GraphicsImagesAnnotationsFormsLinksMetadata
例如:
Page 1├── Text├── Logo├── Vector Line├── Embedded Image└── Annotation
所以 PDF 更像:
一种完整的页面描述文档格式。
这也是为什么:
PDF可以在不同屏幕尺寸上缩放,而文字依然保持清晰。
2. iOS PDF 开发最核心的框架:PDFKit
在 Apple 平台上,PDF 处理经常会使用:
PDFKit几个最核心的类型包括:
PDFViewPDFDocumentPDFPagePDFAnnotation
可以简单理解为:
PDFDocument│├── PDFPage├── PDFPage└── PDFPage
然后:
PDFView↓Display PDFDocument
这是整个 PDF 模块最基础的一层。
3. PDFDocument 可以看作 PDF 文件模型
例如:
import PDFKitlet document = PDFDocument(url: fileURL)
拿到以后:
let count = document?.pageCount获取页面:
let page = document?.page(at: 0)整体:
FileItem↓PDFService↓PDFDocument↓PDFPage
因此不要让:
UIViewController直接承担 PDF 文件逻辑。
4. PDFView 负责什么?
PDFView 更接近:
PDF 阅读和交互 UI。
例如:
let pdfView = PDFView()pdfView.document = document
可以负责:
页面显示 缩放 滚动 页面切换 文本选择
所以架构最好是:
PDF Reader UI↓PDFView↓PDFDocument
而保存、标注、签名等业务逻辑继续放在:
PDFService里。
5. PDF 模块应该先有统一 PDFInfo
类似前面的:
ImageInfoVideoInfoAudioInfo
PDF 也应该先 Inspect。
例如:
struct PDFInfo {let pageCount: Intlet fileSize: Int64let title: String?let author: String?let isEncrypted: Boollet allowsCopying: Boollet allowsPrinting: Bool}
UI 可以先展示:
27 Pages4.8 MBEncrypted: No
以后也方便进行:
PDF Validation6. PDFAnnotation 是标注功能的核心
PDF 中很多常见编辑行为实际上不是:
修改原正文。
而是:
给页面增加 Annotation。
例如:
HighlightUnderlineStrikeoutInkFreeTextLinkStamp
都可以抽象为:
PDFAnnotation因此用户看到:
高亮一段文字底层可能只是:
PDFPage↓Add Annotation
而不是重新生成整页 PDF。
7. Highlight 怎么实现?
用户选择文字:
This is important text然后点击:
Highlight底层需要得到:
Selection↓Bounds↓PDFAnnotation
概念上:
let annotation = PDFAnnotation(bounds: bounds,forType: .highlight,withProperties: nil)page.addAnnotation(annotation)
于是:
Page Content+Highlight Annotation
共同组成最终页面。
8. Underline 和 Strikeout 也是类似思路
例如:
HighlightUnderlineStrikeout
UI 看起来是三种功能。
底层:
PDFAnnotation Type不同。
因此 PDF 编辑工具不应该写:
highlightText()underlineText()strikeoutText()
然后各自独立一整套架构。
更合理:
TextMarkupService↓Annotation Type
9. 手写画笔怎么实现?
自由手写:
Pen / Ink通常可以抽象为:
Touch Points↓Bezier Path↓PDF Ink Annotation
用户:
手指 / Apple Pencil在页面上画:
~~~~底层记录:
Point 1Point 2Point 3...
然后变成:
Path最终加入 PDF。
10. 不要每移动一个点都立即重写 PDF
如果用户正在连续书写:
Touch MoveTouch MoveTouch Move
如果每次:
Save PDF性能会非常差。
更合理:
Drawing Session↓Collect Points↓Render Overlay↓Gesture End↓Create Annotation↓Mark Document Dirty
最后再统一保存。
所以:
UI 实时反馈和 PDF 文件持久化应该分离。
11. Apple Pencil 怎么处理?
如果产品支持 Pencil,可以进一步区分:
FingerApple Pencil
并利用:
Pressure Tilt Precise Points
改善笔迹。
但 PDFService 本身不应该知道:
UITouch细节。
可以设计:
DrawingInput↓InkStroke↓PDF Annotation
这样输入层和文件层继续分离。
12. PDF 签名,本质上是什么?
普通用户看到:
Sign PDF以为 PDF 有一个特殊“签名按钮”。
但很多普通电子签名功能,本质上可能是:
用户手写签名↓Signature Drawing/Image↓Placed on PDF Page↓Annotation
例如:
John Smith用户先保存一个签名,然后拖到:
Signature: ___________位置。
这和:
数字证书签名
不是一回事。
13. 手写签名 ≠ 数字签名
这个区别非常重要。
Handwritten Signature
本质更接近:
Image / Vector Drawing放到 PDF 页面上。
主要解决:
我要在合同上签字。
Digital Signature
则涉及:
CertificatePrivate KeyCryptographic SignatureDocument Integrity
用于验证:
谁签的 文档有没有被篡改 证书是否可信
所以:
PDF 工具如果只是支持手写签字,不应该宣传成数字证书签名系统。
14. Signature 应该独立建模
例如:
struct SavedSignature {let id: UUIDlet createdAt: Datelet vectorData: Data?let imageData: Data?}
UI:
Signature Library│├── Signature A└── Signature B
用户选择:
Signature A然后:
DragResizePlace
最终转成:
PDF Annotation15. 签名保存在哪里?
这涉及用户隐私。
签名属于比较敏感的数据。
因此不建议:
随便放到公开 Documents更适合:
Application Support配合:
File Protection或者加密存储。
如果产品支持:
Face ID App Lock签名库也可以被整体应用锁保护。
16. PDF 签名最好支持矢量数据
如果只是:
PNG Signature放大后可能变模糊。
更好的方式是:
Stroke Points↓Vector Path
保存签名。
渲染时:
Vector↓Scale↓Still Sharp
当然实现成本更高。
第一版使用透明 PNG 也完全可以满足大部分需求。
17. Lasso 是什么?
如果用户画了多个:
Ink Annotation希望:
框选移动删除
就需要:
Selection Tool / Lasso架构上:
Touch Path↓Selection Bounds↓Intersect Annotations↓Selected Annotation Set
然后可以:
MoveDeleteResize
所以 PDF 编辑器一旦支持:
Ink后面很自然就会进入:
Annotation Editing体系。
18. Undo / Redo 很重要
用户在 PDF 上:
写错删错移动错
如果没有 Undo:
体验会非常差。
因此可以设计:
PDFEditCommand例如:
AddAnnotationCommandRemoveAnnotationCommandMoveAnnotationCommandResizeAnnotationCommand
然后:
Undo StackRedo Stack
这比到处手写:
undoLastThing()更容易维护。
19. Command Pattern 很适合 PDF 编辑
比如:
protocol PDFEditCommand {func execute()func undo()}
新增高亮:
Add Annotation执行:
page.addAnnotation(...)撤销:
page.removeAnnotation(...)于是:
HighlightInkSignatureDeleteMove
都可以进入统一编辑历史。
20. PDF 保存不能太随意
用户:
Add Highlight之后需要:
Save最危险的做法是:
直接破坏式覆盖原文件更稳妥:
Original PDF↓Write Temp↓Validate↓Replace Original
这和我们前几篇所有文件任务的原则完全一致。
21. 为什么还是要临时文件?
假设:
document.pdf保存到:
78%突然:
App 被终止 磁盘满了 写文件失败
如果直接写原文件,
就可能:
Original PDF↓Corrupted
所以:
.document.pdf.tmp↓Write↓Complete↓Validate↓Replace
更安全。
22. Auto Save 怎么设计?
PDF 标注很适合:
Auto Save但不要:
每画一个点就写磁盘可以:
Document Dirty↓Debounce↓Auto Save
例如:
用户停止编辑一段时间再保存。
同时保留:
Explicit Save以增强用户确定感。
23. 大型 PDF 最大的问题之一是渲染
例如:
1000 Pages或者:
每页都是超高清扫描图如果一次:
Render All Pages会造成:
Memory Pressure所以 PDF 阅读器应该始终围绕:
Visible Pages工作。
即:
Current Page+Nearby Pages
优先渲染。
24. PDF Thumbnail 不能一次全生成
假设:
2000 Pages缩略图栏如果一打开:
Generate 2000 Thumbnails非常浪费。
应该:
Visible Thumbnail↓Generate↓Cache
滚动到新的页面:
Generate On Demand这和前面的:
Image ThumbnailVideo ThumbnailNAS Thumbnail
完全一样。
25. PDF Thumbnail 也应该有 Cache
例如 Key:
PDF File ID+Modified Date+Page Index+Target Size
然后:
Memory Cache+Disk Cache
文件修改后:
Modified Date变化,
缓存失效。
26. Page Rendering 和 Thumbnail Rendering 应该区分
正文阅读:
Full Page Rendering要求:
清晰缩放
缩略图:
Small Preview要求:
速度低内存
所以不要:
Render Full Resolution↓Resize to Thumbnail
应该:
Render Directly at Target Size尽量减少无意义资源消耗。
27. PDF Search 是另一个非常有价值的能力
用户打开:
300 Page Manual想找:
authentication需要:
Text Search如果 PDF 包含真正的 Text Layer,
可以通过:
PDFDocument↓Find String
找到匹配位置。
UI:
23 Results然后跳转页面。
28. 扫描 PDF 可能没有文字层
例如:
Paper↓Camera↓
如果只是图片生成 PDF,
页面里面实际上是:
Image而不是:
Text于是:
SearchCopy TextSelect Text
都无法工作。
这就是:
OCR能力出现的原因。
29. OCR 和 PDF 是两个不同模块
PDFService 负责:
PDF StructurePagesAnnotationsSave
OCRService 负责:
Image↓Text Recognition
不要把所有能力塞进:
PDFManager更合理:
PDFServiceOCRServiceScanService
各自负责不同问题。
30. 扫描生成 PDF 的基本流程是什么?
用户操作:
打开相机↓拍摄纸张↓自动识别边缘↓透视矫正↓增强↓生成页面↓
整个 Pipeline:
Camera Image↓Document Detection↓Perspective Correction↓Image Enhancement↓Page Image↓PDF Generator
这并不是简单:
Camera Photo↓
31. VisionKit 很适合文档扫描场景
在 iOS 里,系统提供了和文档扫描相关的能力。
产品可以利用系统扫描体验完成:
Document Capture包括:
页面边缘检测 透视调整 多页扫描
然后拿到扫描结果:
Page Images再进入自己的:
PDFService生成 PDF。
32. ScanService 应该独立
例如:
protocol ScanService {func scanDocuments() async throws -> [ScannedPage]}
其中:
struct ScannedPage {let image: UIImage}
然后:
ScanService↓ScannedPage[]↓PDFService↓PDFDocument
这样:
Camera Scanning和:
PDF Generation职责清晰。
33. 图片转 PDF 其实也是同一条路径
例如用户已经有:
Photo 1Photo 2Photo 3
想:
Create PDF流程:
Images↓PDF Page Generator↓PDFDocument
所以:
Camera Scan只是图片来源不同。
统一后:
Image Source↓PDF Generator
可以同时支持:
CameraPhoto LibraryFilesScanner
34. 页面尺寸怎么选?
图片生成 PDF 时要决定:
Page Size例如:
A4LetterOriginal Image Ratio
如果用户扫描合同,
通常:
A4比较自然。
如果只是:
照片合集 PDF则:
Fit Image可能更合适。
因此 PDF Create Options 可以包含:
Page SizeMarginsOrientationImage Fit
35. 扫描 PDF 为什么很容易变得巨大?
假设一页扫描图:
4032 × 302410 页:
10 high-resolution images如果全部用高质量 PNG 写进 PDF,
最终可能:
100MB+所以扫描生成 PDF 时必须考虑:
Image Compression比如:
Downsample+JPEG Compression
36. PDF Scanner 应该考虑目标用途
例如:
Document
合同文字表格
可以采用:
高对比度适中分辨率
Photo
需要:
颜色更高质量
所以可以提供:
DocumentColorPhoto
不同模式。
不必暴露:
JPEG Quality = 0.72这种参数给普通用户。
37. 黑白扫描可以显著减少体积
很多纯文字文件:
黑字白纸没必要保存完整彩色信息。
可以:
Color↓Grayscale
甚至:
Black & White大幅降低数据量。
同时提高:
Text Contrast这也是扫描工具常见的模式。
38. OCR 可以在扫描后异步进行
例如:
Scan↓Create PDF↓User can immediately view
后台再:
OCR而不是强制:
等待 OCR 全部完成才能生成文件。
架构:
Scan↓PDF Ready↓OCR Operation
这能明显提升用户感知速度。
39. OCR 结果应该如何保存?
可以有几种思路。
方案 A
只用于:
Search Index不写回 PDF。
方案 B
生成:
Invisible Text Layer让扫描 PDF 可以:
搜索 复制文字
方案 C
单独保存:
OCR Text作为辅助数据。
不同实现复杂度差异很大。
40. PDF 页面管理也是重要功能
除了标注,
用户还经常需要:
Delete PageRotate PageReorder PageInsert Page
这些能力最好统一成:
PDFPageOperation而不是每个按钮自己直接操作 Document。
例如:
Move Page 8 → 2可以成为:
ReorderPageCommand同样进入:
Undo / Redo体系。
41. Merge PDF 怎么实现?
例如:
A.pdf+B.pdf+C.pdf
合并:
Merged.pdf逻辑:
New PDFDocument↓Insert Pages from A↓Insert Pages from B↓Insert Pages from C
但仍然需要考虑:
页面顺序 Metadata 加密 PDF 大文件 失败恢复
所以 Merge 本质也是:
FileOperation42. Split PDF 呢?
例如:
100-page.pdf用户选择:
Pages 1–10导出:
part1.pdf或者:
每页一个文件这就是:
Split Operation所以未来 PDFService 可以拥有:
merge()split()extractPages()
43. PDF 密码怎么办?
PDF 可能是:
Encrypted PDF打开时需要密码。
流程:
Open PDF↓Encrypted?↓ YesRequest Password↓Unlock
错误应该明确:
Wrong Password而不是:
Failed to open PDF继续延续整个系列的错误业务化原则。
44. PDF 权限也可能有限制
某些 PDF 会设置:
Allow CopyAllow Print
等权限信息。
所以 PDFInfo 可以记录:
allowsCopyingallowsPrinting
应用在实现:
Copy Text
时应尊重对应文档状态和系统能力。
45. PDF Password Store 也可以统一到安全存储
如果用户明确选择:
Remember Password可以:
PDF Credential↓Keychain
和之前:
SMBWebDAVFTPArchive Password
统一进:
CredentialStore这样安全层继续复用。
46. 大 PDF 也应该进入 FileOperationManager 吗?
不是所有浏览行为都需要。
例如:
Open PDFScroll PDF
属于交互。
但:
Save Edited PDFMergeSplitScan to PDFExportOCR
这些都可能是长任务。
所以可以定义:
OperationType.pdfSaveOperationType.pdfMergeOperationType.pdfSplitOperationType.pdfScanOperationType.ocr
统一进入任务系统。
47. PDF 保存进度可能比复制更难
例如:
Rewriting 500-page PDF底层不一定总能直接给你一个非常精确的:
0...100%所以进度可以分成:
PreparingProcessingWritingFinalizing
而不是假装给用户一个特别精确但实际不可靠的百分比。
有时候:
Stage Progress比:
Fake 73%更诚实。
48. PDF 错误应该统一业务化
例如:
enum PDFProcessingError: Error {case invalidDocumentcase encryptedcase wrongPasswordcase permissionDeniedcase unsupportedOperationcase insufficientStoragecase saveFailedcase cancelledcase unknown(Error)}
UI 显示:
PDF 密码错误而不是:
Error Domain=PDFKit...49. 远程 PDF 怎么办?
例如:
WebDAV↓contract.pdf
用户只是阅读,
可以:
Download / Cache↓PDFView
用户要:
签名则更合理:
Remote PDF↓Local Working Copy↓Edit↓Save↓Upload Back
因为编辑通常需要可靠的本地工作文件。
50. 为什么远程 PDF 编辑最好先做 Working Copy?
如果直接:
Remote File↓Edit In Place
网络中断时非常危险。
更稳妥:
Remote↓Download↓Local Working Copy↓Edit↓Save↓Upload Temp↓Replace Remote
这个流程其实就是:
Transactional Remote Edit51. 远程更新还要考虑冲突
假设:
10:00你下载 contract.pdf
你编辑了 10 分钟。
与此同时服务器上的:
contract.pdf已经被别人修改。
如果你直接上传覆盖:
Remote Changes Lost所以成熟实现可以比较:
ETagModified DateFile ID
判断:
Remote Changed?如果变化:
Conflict让用户决定:
ReplaceSave CopyCancel
这和前面的 Provider 架构完全一致。
52. PDFService 可以怎么抽象?
例如:
protocol PDFService {func inspect(_ item: FileItem) async throws -> PDFInfofunc addAnnotation(_ annotation: PDFAnnotationModel,to item: FileItem) async throwsfunc merge(_ items: [FileItem],destination: FileLocation) async throws -> FileItemfunc split(_ item: FileItem,pages: IndexSet,destination: FileLocation) async throws -> FileItemfunc create(from images: [FileItem],options: PDFCreateOptions) async throws -> FileItem}
这样:
PDFKit只是底层实现。
53. PDFAnnotationModel 最好独立于 PDFKit
如果业务层直接持有:
PDFAnnotation以后很难:
做 Undo 做持久化 做同步 做其他 PDF Engine
更好的方式:
struct PDFAnnotationModel {let id: UUIDlet pageIndex: Intlet type: AnnotationTypelet bounds: CGRect}
然后:
PDFAnnotationModel↓PDFKit Adapter↓PDFAnnotation
保持业务模型和框架解耦。
54. 一个完整 PDF 架构
最终可以整理成:
┌─────────────────────────────┐│ UI ││ ││ PDF Reader ││ Annotation Toolbar ││ Signature ││ Scan ││ Page Manager │└──────────────┬──────────────┘│▼┌─────────────────────────────┐│ PDFService ││ ││ Inspect ││ Annotation ││ Signature ││ Merge / Split ││ Create PDF ││ Save │└──────────────┬──────────────┘│┌──────┼───────────┐▼ ▼ ▼PDFKit ScanService OCRService│▼PDFDocument
旁边继续复用:
FileOperationManagerTemporaryFileManagerThumbnailServiceCredentialStoreConflictResolverStorageChecker
55. TS File Explorer 的 PDF 能力真正解决什么?
从开发者角度:
PDFDocumentPDFPagePDFAnnotationVisionKitOCR
是技术实现。
但用户真正的问题可能是:
合同突然发来,我现在就要签字。
或者:
我想把纸质文件直接扫描成 PDF。
或者:
PDF 里这几段文字需要高亮。
所以真正的使用路径是:
contract.pdf↓TS File Explorer↓Sign↓Save↓Share
或者:
Paper Document↓Scan↓↓Share
用户真正需要的不是:
一个 PDFKit Demo。
而是:
文件来了以后,直接在手机上把事情做完。
56. 文件处理平台已经进入 Document 层
到现在:
FileProvider│├── Local├── SMB├── WebDAV└── FTP
上层:
Services│├── ArchiveService├── ImageService├── VideoService├── AudioService├── PDFService├── ScanService├── OCRService├── ThumbnailService└── WaveformService
再由:
FileOperationManager统一处理:
CopyTransferCompressConvertScanMergeSplitSave
整个系统已经明显从:
File Browser变成:
File Processing Platform57. 开发 PDF 模块前建议先回答这 20 个问题
1. PDF 浏览使用什么架构?2. PDFDocument 生命周期如何管理?3. Annotation 如何建模?4. Highlight / Underline 如何实现?5. Ink 如何保存?6. 是否支持 Apple Pencil?7. Signature 如何保存?8. 手写签名与数字签名是否明确区分?9. Undo / Redo 如何设计?10. PDF 保存是否使用临时文件?11. 大 PDF 如何控制内存?12. Thumbnail 如何缓存?13. 是否支持 Search?14. Scan 与 PDFService 是否分层?15. 是否支持 OCR?16. OCR 结果如何保存?17. 是否支持 Merge / Split?18. PDF 密码如何处理?19. Remote PDF 编辑如何做冲突检测?20. PDF 长任务如何接入 FileOperationManager?
如果这些问题没有提前设计,
PDF 模块很容易从:
一个 PDFView逐渐变成:
各种手势+各种标注状态+保存 Bug+页面 Bug+远程文件 Bug
最后难以维护。
写在最后
PDF 功能表面上看只是:
ReadHighlightSignScan
但真正的工程架构其实涉及:
PDF Rendering+Annotation+Drawing+Document Editing+Scanning+OCR+File Persistence+Remote File Conflict
最重要的一点仍然是:
不要把 PDF 模块写成 PDFView 上不断叠加按钮,而应该把它设计成独立的 Document Processing Service。
整个系列现在已经来到:
01 File Browser↓02 Large File IO↓03 Wi-Fi Transfer↓04 SMB↓05 WebDAV↓06 FTP↓07 Archive↓08 Image↓09 Video↓10 Audio↓11 PDF
下一篇我建议继续进入:
《iOS EPUB / MOBI 阅读器怎么实现?从电子书解析、目录、分页到 TTS 阅读架构》
可以继续拆解:
EPUBMOBIHTMLCSSTable of ContentsChapterPaginationThemeFont SizeReading ProgressBookmarkTTS
这样就把 TS File Explorer 的 Reader 能力也完整接入这套架构。