ONNXOCRSharp 开源手记 02|v0.4.0 PDF识别
1、什么是PDF
PDF全称是Portable Document Format,是一种电子文档格式,由 Adobe 公司在 1993 年推出。它最牛的特点是:无论在什么设备、什么操作系统上打开,文件的排版和内容都保持不变。
举个例子,也是之前踩过的坑:标书的打印,最开始呢,我们是给打印社word文档。好,打印出来的页数跟我们自己看的不一样。写过标书的同学都知道,这个页数是要手动填写的,页数都变了,我们还要做调整。而且,不同的word版本,打开后显示的也不一样。也就是说,这一致性保持的不好。那么经验就来了,直接提供PDF文件,打印出来就没有惊喜了。
PDF按内容来源和可编辑性,常被分成下面两种:
扫描型 / 图片型 PDF:本质是图片,没有文字层。
数码型 / 文本型 PDF:由 Word、LaTeX、排版软件等生成,自带文字层。
2、本期目标
主要是针对第一种类型,就是没有文字层的PDF做OCR识别。

在整体界面都不变的情况下,选择文件时支持添加 PDF 文件,点击识别,可以导出识别后的文字到 TXT 文件。嗯,目前先做这个基础功能。
图中的PDF是用小程序Linc识别扫描而成,它主打一个扫描永久免费,无水印,导出方便。
4、技术实现
核心问题是将 PDF 转换为图片,再用 OCR 识别。PDF文件与图片不同点在于,有可能选择的PDF文件是一本书,这体量就大多了,需要做拆解处理,否则程序就会内存溢出。
我们再细节的规划下:
- PDF → 图:
PdfPageRasterizer用 Docnet(PDFium)把一页渲成Mat - 图 → 字:
OcrService把这张Mat直接传给原来的TextSystem.Run - 多页 → 队列:桌面把 PDF 拆成「第 n 页」队列项,识别/预览时再按需渲
对应关系大概是:
┌─────────────────────────────────────────────────────────────┐
│ OnnxOcr.App │
│ Pdf/PdfPageRasterizer.cs ← 负责读页数、渲单页 │
│ Services/OcrService.cs ← RecognizePdf* 串行调上面 │
│ Models/PdfOcr*.cs ← 按页结果 / 进度 / 截断标记 │
├─────────────────────────────────────────────────────────────┤
│ OnnxOcr.Desktop │
│ QueueItemViewModel ← FilePath + PdfPageIndex │
│ MainViewModel ← 入队截断、按页识别、导出 │
├─────────────────────────────────────────────────────────────┤
│ OnnxOcr.Core(不变) │
│ TextSystem.Run(Mat) ← 还是原来的检测 + 识别 │
└─────────────────────────────────────────────────────────────┘
4.1 渲页:PdfPageRasterizer
**Docnet(PDFium)**封装了 Google Chromium 浏览器使用的 PDFium C++ 渲染引擎,用于在 .NET 应用中快速读取、解析和渲染 PDF 文档。
两个入口:
GetPageCount(path):打开文档拿到PDF文件的页数RenderPage(path, pageIndex, dpi):渲一页,返回 BGRMat,调用方using释放
一些编程上的细节:
PDF 里长度单位是 point(1/72 英寸)。Docnet 的 PageDimensions(scalingFactor) 要的是 pixels-per-point,所以:
scalingFactor = dpi / 72
默认 DPI=200 → scaling ≈ 2.778。A4 大概会渲到一千多像素宽。
GetImage() 拿到的是 BGRA 原始像素,不是 PNG。很多 PDF 页带透明通道,如果丢掉 alpha,透明区会变黑底,这样检测模型就没办法识别了。我们可以先铺一张白底,再按 alpha 把彩色贴上去,最后交给 OCR 就可以了:
// 伪代码:白底 + alpha 合成
white.CopyTo(result);
bgr.CopyTo(result, alphaChannel);
4.2 串行识别:RecognizePdfAsync
库 API 如下:
Task<PdfOcrResult> RecognizePdfAsync(
string pdfPath,
PdfRecognizeOptions? options = null, // Dpi、MaxPages
IProgress<PdfOcrProgress>? progress = null,
CancellationToken cancellationToken = default);
核心循环可以这样设计:
totalPages = GetPageCount
toProcess = MaxPages.HasValue ? Min(MaxPages, totalPages) : totalPages
WasTruncated = toProcess < totalPages
for i in 0 .. toProcess-1:
progress.Report(当前页 / 本次页数 / 文档总页数)
try:
mat = RenderPage(pdf, i, dpi) // using,出作用域就释放
result = TextSystem.Run(mat) // 与认 jpg 同一条路
pages.Add(成功)
catch (不是取消):
pages.Add(失败 + ErrorMessage) // 单页挂了不中断整本
单页核心的三行逻辑:
using var image = PdfPageRasterizer.RenderPage(pdfPath, pageIndex, dpi);
var result = _textSystem.Run(image);
return OcrRunResult.From(result, $"{pdfPath}#page={pageIndex + 1}");
4.3 桌面:拆页入队 + 识别时再渲
桌面没有在添加文件时把 PDF 都导出jpg。现在的做法是扩展队列项:
QueueItemViewModel
FilePath → 那个 .pdf 路径
PdfPageIndex → 0-based;普通图片为 null
QueueKey → "path#page=3",用来去重
FileName → "书名.pdf · 第 4 页"
添加时:GetPageCount → 生成前 CommunityPdfMaxPages(10)个队列项;超了就弹窗/状态栏说明。
识别时:若 item.IsPdfPage,走 RecognizePdfPageAsync(path, pageIndex),否则走原来的 RecognizeAsync(图片路径)。
预览时:同样按 PdfPageIndex 现渲一页再 ToBitmapSource,点哪页渲哪页。
导出 TXT:PDF 页用 --- 第 n 页 --- 分隔。
4.4 库侧怎么调
整本(可截断):
using OnnxOcr.App.Services;
using OnnxOcr.App.Pdf;
using OnnxOcr.Core.Configuration;
using var service = new OcrService(OcrModelPreset.PpOcrV6Tiny);
var pdf = await service.RecognizePdfAsync(
"scan.pdf",
new PdfRecognizeOptions { Dpi = 200, MaxPages = null },
new Progress<PdfOcrProgress>(p =>
Console.WriteLine($"[{p.CurrentPage}/{p.TotalToProcess}] 文档共 {p.TotalInDocument} 页")));
foreach (var page in pdf.Pages.Where(p => p.Success))
{
Console.WriteLine($"--- 第 {page.PageIndex + 1} 页 ---");
foreach (var line in page.Result!.Lines)
Console.WriteLine(line.Text);
}
if (pdf.WasTruncated)
Console.WriteLine($"已截断:{pdf.ProcessedPages}/{pdf.TotalPages}");
只要某一页:
var page = await service.RecognizePdfPageAsync("scan.pdf", pageIndex: 0, dpi: 200);
5、用法一览
还是熟悉的三栏。文字模式下,添加文件的过滤器里多了 PDF:
┌─ 文字识别 模型 / 设备 / 添加 / 开始识别 / 导出 TXT ─┐
├──────────┬────────────────────┬───────────────────────┤
│ 文件队列 │ 当前页预览 + 检测框 │ 当前页识别结果 │
│ ····pdf · 第 1 页 │ │
│ ····pdf · 第 2 页 │ │
└──────────┴────────────────────┴───────────────────────┘
使用方法:
- 模式保持 文字识别(表格模式会明确提示不支持 PDF)
- 添加一个或多个 PDF(也可和图片混着加)
- 队列里会按页拆开;目前支持识别前 10 页。时间关系,先验证路径。后续做强大的书籍PDF识别支持。
- 点 开始识别——按需渲页,认完一页释放一页
- 导出 TXT 时,页与页之间用
--- 第 n 页 ---隔开

6、小结
PDF识别要做的东西挺多的,今天先开了一个头,将基础的识别转文字完成了。
而业界也有很多难题没有完美的解决,比如表格结构的识别、手写体与公式的识别、导出DOC并恢复文档结构等等。
接下来我会从简单着手,慢慢完善这些功能。
7、获取地址
# 源码
https://github.com/lincyang/OnnxOCRSharp
# NuGet
dotnet add package OnnxOCRSharp --version 0.4.0
# 桌面应用 Release
https://github.com/lincyang/OnnxOCRSharp/releases
系列导航(持续更新)
- 00|从今天起,记录它的每一步
- 01|v0.3.0 表格识别
- 02|v0.4.0 PDF 识别
- 前传|C# 移植 MVP、《PP-OCRv6 升级》、《GPU 与竖排》、《多文件识别》、《Python 表格识别》
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)