如何将VNDocumentCameraScan提取的文本整合到可搜索PDFDocument中?
实现可搜索PDF的方案(基于VNDocumentCameraScan + PDFKit)
我刚好做过类似的需求,核心思路是给PDF的每一页添加透明的文本层——也就是把Vision识别出的文本内容,以不可见的形式绘制到PDF页面上,这样PDF阅读器就能检索到这些文本,同时不影响原扫描图像的显示。
下面是完整的实现步骤和代码:
1. 核心步骤拆解
- 对每一页扫描图,先创建基础的图片PDF页
- 用Vision框架识别该页的所有文本及对应的位置信息
- 转换Vision的坐标系到PDF的坐标系(两者差异很大,必须处理)
- 把识别到的文本以透明样式绘制到PDF页上,作为可搜索的文本层
- 替换原PDF页为带文本层的新页面
2. 完整代码实现
首先,添加文本识别的辅助方法:
import Vision import PDFKit import UIKit fileprivate func recognizeText(in image: UIImage) -> [VNRecognizedText] { // 创建文本识别请求,可选择精度(accurate/fast) let request = VNRecognizeTextRequest() request.recognitionLevel = .accurate // 追求精度用这个,要速度可以换成.fast request.usesLanguageCorrection = true // 开启语言校正,提升识别准确率 guard let cgImage = image.cgImage else { return [] } let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) do { try handler.perform([request]) // 取每个识别结果的最可能候选文本 return request.results?.compactMap { $0.topCandidates(1).first } ?? [] } catch { print("文本识别失败:\(error.localizedDescription)") return [] } }
然后是生成可搜索PDF的核心方法:
fileprivate func createSearchablePDF(from scan: VNDocumentCameraScan) -> PDFDocument { let pdfDocument = PDFDocument() for pageIndex in 0 ..< scan.pageCount { // 1. 获取当前页的扫描图像,创建基础PDF页 guard let pageImage = scan.imageOfPage(at: pageIndex), let basePDFPage = PDFPage(image: pageImage) else { continue } // 2. 识别当前页的文本内容 let recognizedTexts = recognizeText(in: pageImage) guard !recognizedTexts.isEmpty else { // 如果没识别到文本,直接添加原页面 pdfDocument.insert(basePDFPage, at: pageIndex) continue } // 3. 获取PDF页面的媒体框(坐标系统,单位为pt) guard let pageBounds = basePDFPage.bounds(for: .mediaBox) else { pdfDocument.insert(basePDFPage, at: pageIndex) continue } let imageSize = pageImage.size // 4. 创建PDF绘制上下文,用来合并图像和文本层 let pdfContext = CGContext(data: nil, width: Int(pageBounds.width), height: Int(pageBounds.height), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! // 开始绘制新的PDF页 pdfContext.beginPDFPage(nil) // 先绘制原扫描图像(作为底层) pdfContext.draw(pageImage.cgImage!, in: pageBounds) // 设置文本填充色为透明——关键!这样文本不会显示,但能被搜索到 pdfContext.setFillColor(UIColor.clear.cgColor) // 遍历所有识别到的文本块,逐个绘制 for textBlock in recognizedTexts { guard let text = textBlock.string as NSString?, // 获取文本块的边界框(归一化坐标) let textBoundingBox = textBlock.boundingBoxForRange(NSRange(location: 0, length: text.length)) else { continue } // ==== 坐标转换:Vision坐标系 → PDF坐标系 ==== // Vision的坐标系:原点在图像左下角,值为0~1的归一化值 // UIImage的坐标系:原点在左上角,y轴向下 // PDF的坐标系:原点在左下角,y轴向上,单位为pt let imageX = textBoundingBox.origin.x * imageSize.width // 转换y坐标:从Vision的左下角原点转为UIImage的左上角原点 let imageY = imageSize.height - (textBoundingBox.origin.y + textBoundingBox.height) * imageSize.height let textRectInImage = CGRect(x: imageX, y: imageY, width: textBoundingBox.width * imageSize.width, height: textBoundingBox.height * imageSize.height) // 转成PDF的坐标(因为PDF页的尺寸和图像一致,直接复用即可) let textRectInPDF = textRectInImage // 根据文本块高度设置字体大小(适配不同字号的文本) let fontSize = textRectInPDF.height * 0.8 // 取块高度的80%,保证文本居中 // 创建字体(用系统字体即可,不影响搜索) guard let cgFont = UIFont.systemFont(ofSize: fontSize).cgFont else { continue } // 配置文本绘制参数 pdfContext.selectFont(cgFont.name as String, size: fontSize, matrix: CGAffineTransform.identity) pdfContext.setTextDrawingMode(.fill) // 调整文本的垂直位置,让文本居中在识别块内 let textPositionY = textRectInPDF.origin.y + textRectInPDF.height - fontSize * 0.2 pdfContext.setTextPosition(textRectInPDF.origin.x, textPositionY) // 绘制文本 text.draw(with: textRectInPDF, options: .usesLineFragmentOrigin, attributes: [.font: UIFont.systemFont(ofSize: fontSize)], context: pdfContext) } // 结束PDF页绘制 pdfContext.endPDFPage() // 从上下文生成新的PDF页,替换原页面 if let searchablePDFPage = PDFPage(cgPDFPage: pdfContext.pdfPage!) { pdfDocument.insert(searchablePDFPage, at: pageIndex) } else { pdfDocument.insert(basePDFPage, at: pageIndex) } } return pdfDocument }
最后在相机回调里调用并保存PDF:
func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { let searchablePDF = createSearchablePDF(from: scan) // 保存PDF到Documents目录 let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let saveURL = documentsDir.appendingPathComponent("扫描文档_\(Date().timeIntervalSince1970).pdf") if searchablePDF.write(to: saveURL) { print("可搜索PDF已保存到:\(saveURL.path)") // 这里可以添加用户提示,比如弹框告知保存成功 } else { print("PDF保存失败") } controller.dismiss(animated: true) }
3. 关键注意事项
- 坐标转换:这是最容易出错的地方,一定要注意Vision、UIImage、PDF三者的坐标系差异,否则文本位置会完全偏离
- 文本透明度:必须设置文本为透明,否则会覆盖原扫描图像
- 识别精度:如果对识别速度有要求,可以把
recognitionLevel改成.fast,但准确率会稍低 - 畸变处理:如果扫描的文档有倾斜或轻微畸变,Vision的文本识别依然能正常工作,但如果畸变严重,建议先做图像校正
内容的提问来源于stack exchange,提问作者grahan
相关产品推荐
相关产品推荐

