:双层 PDF 生成实战——图像层 + 隐形文本层的完整实现)
专栏第 2 篇。上一篇讲了 DA/T 31-2017 的九环节全流程其中图像处理和OCR/目录建库之间有一个被很多同行忽略的衔接产物双层 PDF。这篇把我们的实现方案、完整代码和踩过的坑都放出来。一、什么是双层 PDF为什么档案场景必须用它把一张扫描图直接存成 PDF你得到的是一层皮——只有图像层看起来是文档但里面没有一个字搜不到、复制不了、选不中。档案数字化的利用场景恰恰全是要字的查档系统里搜某某项目要能命中这份文件律师调阅时想复制一段判决书原文用户按页打印时又要保持原貌不能是重排的文本。原貌 可检索两个要求同时满足解决方案就是双层 PDF图像层保留扫描原貌隐形文本层承载 OCR 识别结果。人眼看到的永远是图像搜索引擎和文本工具拿到的永远是文字层。行业里管这个叫OCR 全文本 PDF或检索 PDF档案验收和档案局接收系统对它的接受度已经非常高。对我们的系统来说双层 PDF 是 OCR 能力的交付形态——识别结果不落成文字层OCR 就等于白做。二、三条技术路线对比我们调研并实测过三条路线工程取舍如下路线实现方式优点缺点我们的结论A. ocrmypdf开源命令行工具tesseract ghostscript开箱即用、参数齐全中文识别依赖 tesseract 模型坐标可控性弱快速原型可用生产不采用B. PaddleOCR reportlab自研管线OCR 出坐标reportlab 写隐形文本层坐标完全可控、中文识别质量最好要自己处理坐标系、字体、批量并发生产主方案C. PyMuPDFfitz直接在 PDF 上叠文本对象免重绘图像、速度快隐形文本属性设置繁琐和 OCR 输出对接要自己写适合给已有 PDF 补文字层下面给出主方案 B 的完整实现以及方案 C 的补充代码。三、主方案PaddleOCR reportlab3.1 整体思路扫描图 → PaddleOCR出文本 坐标框 → 坐标换算图像像素 → PDF 点 → reportlab 画图像层 → 叠加隐形文本层 → 输出双层 PDF核心难点只有一个坐标系换算。图像坐标系原点在左上角、Y 轴向下PDF 坐标系原点在左下角、Y 轴向上。OCR 返回的每个文本框坐标都要翻转到 PDF 坐标否则文字层会跑到镜像位置复制出来的文字顺序还是反的。3.2 完整代码双层 PDF 生成器PaddleOCR 识别 reportlab 隐形文本层 运行环境Python 3.9pip install paddleocr reportlab pillow frompaddleocrimportPaddleOCRfromPILimportImagefromreportlab.pdfgenimportcanvasfromreportlab.pdfbaseimportpdfmetricsfromreportlab.pdfbase.cidfontsimportUnicodeCIDFont# 关键点中文必须注册 CID 字体否则隐形文本层无法嵌入中文字形# 用 Helvetica 画中文会全部变成空白占位复制出来是乱码。pdfmetrics.registerFont(UnicodeCIDFont(STSong-Light))ocrPaddleOCR(use_angle_clsTrue,langch,show_logFalse)# 默认识别结果结构# result[0] [ [[x1,y1,x2,y2...4点坐标], (文本, 置信度)], ... ]# 每个文本框是 4 个角点的坐标列表 [左上, 右上, 右下, 左下]def_image_dpi(img_path:str)-int:读取扫描图的元数据 dpi取不到就按 300 兜底PDF 点换算用。withImage.open(img_path)asim:dpiim.info.get(dpi)ifdpianddpi[0]0:returnint(dpi[0])return300defbuild_double_layer_pdf(img_path:str,pdf_path:str)-None:resultocr.ocr(img_path,clsTrue)linesresult[0]ifresultelse[]withImage.open(img_path)asim:w_px,h_pxim.size dpi_image_dpi(img_path)# 像素 → PDF 点1 英寸 72 点w_ptw_px*72.0/dpi h_pth_px*72.0/dpi ccanvas.Canvas(pdf_path,pagesize(w_pt,h_pt))# 1) 图像层原貌c.drawImage(img_path,0,0,widthw_pt,heighth_pt)# 2) 隐形文本层render mode 3 不渲染可见字形仅用于提取forlineinlines:box,(text,conf)line[0],line[1]xs[p[0]forpinbox]ys[p[1]forpinbox]x_ptmin(xs)*72.0/dpi# 图像原点在左上 → PDF 原点在左下Y 轴翻转y_pth_pt-max(ys)*72.0/dpi font_size(max(ys)-min(ys))*72.0/dpiiffont_size0orconf0.6:# 低置信度不落文字层避免污染检索continuec.setFont(STSong-Light,font_size)tc.beginText(x_pt,y_pt)t.setTextRenderMode(3)# 隐形t.textOut(text)c.drawText(t)c.showPage()c.save()print(f[ok]{img_path}-{pdf_path}{len(lines)}个文本块{w_px}x{h_px}{dpi}dpi)if__name____main__:importsys build_double_layer_pdf(sys.argv[1],sys.argv[2])用法python double_layer_pdf.py 扫描件.png 双层结果.pdf3.3 批量流水线单页跑通后上批量我们直接按页做多进程图片处理是 CPU 密集进程池比线程池合适frommultiprocessingimportPoolfrompathlibimportPathdefprocess_one(page:Path,out_dir:Path)-None:build_double_layer_pdf(str(page),str(out_dir/(page.stem.pdf)))defbatch(src_dir:Path,out_dir:Path,workers:int4)-None:out_dir.mkdir(parentsTrue,exist_okTrue)pageslist(src_dir.glob(*.png))list(src_dir.glob(*.jpg))withPool(workers)aspool:pool.map(lambdap:process_one(p,out_dir),pages)四、三个必须处理的坑代码能跑只是开始这三点不处理生产环境必翻车坑 1中文隐形文字层是乱码不注册UnicodeCIDFont直接用 Helvetica 画中文视觉上没问题反正隐形但复制出来全乱码等于文字层白做。这也是为什么很多看起来做了双层 PDF的产品验收时一复制就露馅。务必用pdfmetrics.registerFont(UnicodeCIDFont(STSong-Light))。坑 2坐标 Y 轴翻转漏掉图像左上原点 → PDF 左下原点Y 必须翻转。漏翻转的症状是检索能命中但复制出来文字顺序是反的、选中区域错位。测试时用 PDF 阅读器的选择文本功能逐页抽查选中的矩形区域应该和文字位置对得上。坑 3低置信度文本全塞进去把 OCR 的幻觉文本置信度 0.3 的噪声也写进文字层检索时会搜出一堆根本不存在的内容——档案检索出错误结果比搜不到更严重。我们的策略置信度低于 0.6 不落文字层宁可少搜到不可搜错。五、性能与成本参考我们的实测口径具体数据因机器和页面密度而异仅供参考配置单页耗时300dpi A4说明CPU8 核PaddleOCR约 2~4 秒/页并发 4 路后约 1 秒/页折算GPU单卡PaddleOCR约 0.3~0.8 秒/页适合 30 万页以上大项目存储上双层 PDF 相比纯图像 PDF 增量通常在 5%~15%文字层是矢量文本几乎不占空间完全可接受。六、方案 C 补充给已有 PDF 补文字层如果你手头已经是单层图像 PDF比如从扫描仪直出的 PDF用 PyMuPDF 补文字层更划算不用重新渲染图像importfitz# PyMuPDFdefadd_text_layer(pdf_path:str,out_path:str,text_items:list)-None:text_items: [(x_pt, y_pt, font_size, text), ...]坐标已是 PDF 左下原点docfitz.open(pdf_path)pagedoc[0]forx,y,size,textintext_items:page.insert_text(fitz.Point(x,y),text,fontnamechina-s,fontsizesize,render_mode3,# 隐形overlayTrue,)doc.save(out_path)注意 PyMuPDF 的china-s内置字体同样是为了中文提取正确性。七、小结双层 PDF 是个小功能、大讲究的活儿代码量不大但坐标系、字体、置信度三个细节决定成败。总结成一句话口诀原图铺底坐标翻转中文字体低置信度丢弃。下一篇讲我们做过的一个 30 万页高校学籍档案数字化项目复盘——那里有一堆比技术更疼的坑。