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文件是一本书,这体量就大多了,需要做拆解处理,否则程序就会内存溢出。

我们再细节的规划下:

  1. PDF → 图:PdfPageRasterizer 用 Docnet(PDFium)把一页渲成 Mat
  2. 图 → 字:OcrService 把这张 Mat 直接传给原来的 TextSystem.Run
  3. 多页 → 队列:桌面把 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):渲一页,返回 BGR Mat,调用方 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 页              │                       │
└──────────┴────────────────────┴───────────────────────┘

使用方法:

  1. 模式保持 文字识别(表格模式会明确提示不支持 PDF)
  2. 添加一个或多个 PDF(也可和图片混着加)
  3. 队列里会按页拆开;目前支持识别前 10 页。时间关系,先验证路径。后续做强大的书籍PDF识别支持。
  4. 点 开始识别——按需渲页,认完一页释放一页
  5. 导出 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 表格识别》
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐