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

如何将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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 07:22:33