夜雨聆风学习资料网

ARTICLE · 1070778

iOS PDF 工具怎么实现?从 PDFKit、标注、签名到扫描生成 PDF

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 需求往往非常明确:

收到一份 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(at0)

整体:

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 pageCountInt    let fileSizeInt64    let titleString?    let authorString?    let isEncryptedBool    let allowsCopyingBool    let allowsPrintingBool}

UI 可以先展示:

27 Pages4.8 MBEncrypted: No

以后也方便进行:

PDF Validation

6. 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 idUUID    let createdAtDate    let vectorDataData?    let imageDataData?}

UI:

Signature Library├── Signature A└── Signature B

用户选择:

Signature A

然后:

DragResizePlace

最终转成:

PDF Annotation

15. 签名保存在哪里?

这涉及用户隐私。

签名属于比较敏感的数据。

因此不建议:

随便放到公开 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

如果只是图片生成 PDF,

页面里面实际上是:

Image

而不是:

Text

于是:

SearchCopy TextSelect Text

都无法工作。

这就是:

OCR

能力出现的原因。


29. OCR 和 PDF 是两个不同模块

PDFService 负责:

PDF StructurePagesAnnotationsSave

OCRService 负责:

Image ↓Text Recognition

不要把所有能力塞进:

PDFManager

更合理:

PDFServiceOCRServiceScanService

各自负责不同问题。


30. 扫描生成 PDF 的基本流程是什么?

用户操作:

打开相机 ↓拍摄纸张 ↓自动识别边缘 ↓透视矫正 ↓增强 ↓生成页面 ↓PDF

整个 Pipeline:

Camera Image    ↓Document Detection    ↓Perspective Correction    ↓Image Enhancement    ↓Page Image    ↓PDF Generator

这并不是简单:

Camera Photo ↓PDF

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 × 3024

10 页:

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 本质也是:

FileOperation

42. Split PDF 呢?

例如:

100-page.pdf

用户选择:

Pages 110

导出:

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 TextPrint

时应尊重对应文档状态和系统能力。


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 PDFProcessingErrorError {    case invalidDocument    case encrypted    case wrongPassword    case permissionDenied    case unsupportedOperation    case insufficientStorage    case saveFailed    case cancelled    case 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 Edit

51. 远程更新还要考虑冲突

假设:

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(        _ itemFileItem    ) async throws -> PDFInfo    func addAnnotation(        _ annotationPDFAnnotationModel,        to itemFileItem    ) async throws    func merge(        _ items: [FileItem],        destinationFileLocation    ) async throws -> FileItem    func split(        _ itemFileItem,        pagesIndexSet,        destinationFileLocation    ) async throws -> FileItem    func create(        from images: [FileItem],        optionsPDFCreateOptions    ) async throws -> FileItem}

这样:

PDFKit

只是底层实现。


53. PDFAnnotationModel 最好独立于 PDFKit

如果业务层直接持有:

PDFAnnotation

以后很难:

  • 做 Undo
  • 做持久化
  • 做同步
  • 做其他 PDF Engine

更好的方式:

struct PDFAnnotationModel {    let id: UUID    let pageIndex: Int    let type: AnnotationType    let 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   ↓PDF   ↓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 Platform

57. 开发 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 能力也完整接入这套架构。

相关学习资料