PdfOcrLayer 0.5.1

dotnet add package PdfOcrLayer --version 0.5.1
                    
NuGet\Install-Package PdfOcrLayer -Version 0.5.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PdfOcrLayer" Version="0.5.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PdfOcrLayer" Version="0.5.1" />
                    
Directory.Packages.props
<PackageReference Include="PdfOcrLayer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PdfOcrLayer --version 0.5.1
                    
#r "nuget: PdfOcrLayer, 0.5.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PdfOcrLayer@0.5.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PdfOcrLayer&version=0.5.1
                    
Install as a Cake Addin
#tool nuget:?package=PdfOcrLayer&version=0.5.1
                    
Install as a Cake Tool

PdfOcrLayer

为扫描版 PDF 添加不可见文字图层的 .NET 类库。基于 PP-OCRv6 ONNX 模型实现纯 CPU OCR 推理,识别结果通过 PDF 文字渲染模式 3(Tr=3)写为不可见但可选中、可搜索的文字,并自动进行字体子集化以减小输出体积。

  • 目标框架:net8.0; net10.0(多目标编译,NuGet 包自动为消费者选取对应框架的 DLL)
  • 推理引擎:Microsoft.ML.OnnxRuntime 1.20.1(纯 CPU)
  • 模型:PP-OCRv6 tiny(文档方向分类 / 文本检测 / 文本行方向分类 / 文本识别)
  • PDF 处理:PdfPig 提取页面图像 + 写回文字图层
  • 模型资源:以 NuGet contentFiles 分发,所有目标框架共享一份,不嵌入 DLL(DLL 仅 ~88 KB)
  • 许可证:MulanPSL-2.0

平台支持

跨平台纯 CPU 推理,所有托管依赖(ImageSharp / PdfPig / YamlDotNet)均为纯托管代码;平台支持范围由 OnnxRuntime 1.20.1 原生库.NET 运行时 的交集决定。

支持的操作系统与 CPU 架构

平台 架构 RID 支持状态 说明
Windows x64 win-x64 完全支持
Windows x86 win-x86 完全支持
Windows arm64 win-arm64 完全支持
Linux (glibc) x64 linux-x64 要求 glibc ≥ 2.27(Ubuntu 18.04+ / Debian 10+ / RHEL 8+)
Linux (glibc) arm64 linux-arm64 要求 glibc ≥ 2.27(树莓派 4/5、RK3588、AWS Graviton 等 ARMv8 设备)
Linux (glibc) arm (32位) linux-arm 要求 glibc ≥ 2.27(ARMv7 hard-float)
Linux (musl/Alpine) x64 linux-musl-x64 Alpine Linux x64
Linux (musl/Alpine) arm64 linux-musl-arm64 Alpine Linux ARM64
Linux (musl/Alpine) arm (32位) linux-musl-arm Alpine Linux ARM32
Linux (glibc) loongarch64 linux-loongarch64 龙芯 LoongArch64(.NET 8+ 支持)
macOS x64 osx-x64 Intel 芯片
macOS arm64 osx-arm64 Apple Silicon(M1/M2/M3/M4)

不支持的平台

平台 架构 原因
Linux x86 (32位) .NET 7 起移除 32 位 x86 Linux 运行时支持,虽 OnnxRuntime 仍提供原生库但无对应 .NET 运行时
Linux riscv64 .NET 未正式支持 RISC-V 架构(仅有社区实验性支持)
CentOS 7 任意 glibc 2.17 < 2.27 要求,需升级到 RHEL 8 / Rocky 8 / AlmaLinux 8
Android arm64 / arm / x64 OnnxRuntime 提供 linux-bionic-* 原生库,但需 MAUI workload 与专用包(Microsoft.ML.OnnxRuntime.Android),非标准 .NET RID,未列入官方支持
iOS arm64 同上,需 MAUI workload 与 Microsoft.ML.OnnxRuntime.iOS,未列入官方支持

跨平台发布

# 框架依赖发布(体积小,需目标机器安装 .NET 运行时)
dotnet publish -c Release

# 自包含发布到 linux-arm64(无需目标机器安装 .NET)
dotnet publish -c Release -r linux-arm64 --self-contained

# 自包含发布到 win-x64
dotnet publish -c Release -r win-x64 --self-contained

Linux 容器部署推荐基础镜像mcr.microsoft.com/dotnet/runtime:8.0(基于 Debian,glibc 2.31,满足要求)。Alpine 部署请使用 mcr.microsoft.com/dotnet/runtime:8.0-alpine 并指定 RID linux-musl-x64

功能特性

  • 完整 OCR 流水线:文档方向分类 → 文本检测(DBNet)→ 文本行方向分类 → CTC 识别
  • 不可见文字图层:OCR 结果写回 PDF,文字不可见但可复制、可搜索
  • 纯 CPU 推理:无需 GPU,开箱即用
  • 资源文件共享:ONNX 模型、YAML 配置、TTF 字体通过 NuGet contentFiles 分发,所有目标框架共享一份,不重复嵌入 DLL(单 DLL 仅 ~88 KB)
  • 可配置:检测阈值、批大小、线程数、是否跳过方向分类等均可调整
  • 可观测:7 阶段进度回调(IOcrProgressCallback)+ 文本行识别细粒度进度(IProgress<LineRecognitionProgress>)+ 日志回调(ILogCallback)+ 结构化阶段耗时(OcrTimings
  • 可中止:支持请求当前推理立即中止(OcrEngine.Abort()

安装

dotnet add package PdfOcrLayer

支持目标框架 net8.0、net10.0,NuGet 包自动为消费者项目匹配对应的 DLL。

安装后首次编译时,NuGet 会通过 buildTransitive/PdfOcrLayer.targets 自动将 ONNX 模型、字体等资源文件复制到输出目录的 PdfOcrLayer/ 子目录下。

快速开始

处理 PDF 文件

using PdfOcrLayer;

var processor = new PdfOcrProcessor();

// 处理 PDF 文件,返回带不可见文字图层的新 PDF 字节
byte[] outputPdf = processor.Process("input.pdf");

File.WriteAllBytes("output.pdf", outputPdf);

对单张图像执行 OCR

using PdfOcrLayer;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

using var engine = new OcrEngine();

using var image = Image.Load<Rgb24>("page.png");
PageOcrResult result = engine.Recognize(image, pageNumber: 1);

Console.WriteLine($"识别到 {result.Regions.Count} 个文本行");
foreach (var region in result.Regions)
{
    Console.WriteLine($"  [{region.Confidence:F3}] {region.Text}");
}

// 结构化耗时统计
if (result.Timings is { } t)
{
    Console.WriteLine(t.ToSummary());
    // total=28576.0ms | docOri=400.7 | det=1716.3 | cls=3258.7 | rec=22100.0
}

核心 API

PipelineOptions — 流水线参数配置

// 使用默认配置(每次返回新实例,可自由修改)
var opt = PipelineOptions.Default;

// 自定义配置
var opt = new PipelineOptions
{
    LimitSideLen = 1280,       // 检测模型长边限制(像素)
    BoxThresh = 0.45f,         // DB 框置信度阈值
    Thresh = 0.2f,             // DB 像素二值化阈值
    UnclipRatio = 1.1f,        // DB 框扩张系数(默认 1.1)
    MaxCandidates = 3000,      // 最大候选框数量
    RecBatchSize = 12,         // 识别批量大小(默认 12,可调 1~32)
    NumThreads = 4,            // ONNX Runtime 线程数
    SkipDocOrientation = false,    // 是否跳过文档方向分类
    SkipDocUnwarping = true,       // 是否跳过图像矫正(本期未实现 UVDoc)
    SkipTextlineOrientation = false, // 是否跳过文本行方向分类
    TextlineClsBatchSize = 8,  // 文本行方向分类批大小
    RecDropThreshold = 0.5f,    // 识别最低置信度
};

OcrEngine — 引擎生命周期

// 创建引擎
using var engine = new OcrEngine(opt, logCallback);

// 执行 OCR,返回结果(Regions.Count 即识别条数)
var result = engine.Recognize(image, pageNumber: 1, progressCallback);

// 带 CancellationToken 的重载(与 Abort() 取"或"关系,任一触发即取消)
var result = engine.Recognize(image, cancellationToken, pageNumber: 1, progressCallback);

// 同时传入行级细粒度进度回调(在⑤识别阶段每完成一个批次触发一次)
var lineProgress = new Progress<LineRecognitionProgress>(p =>
    Console.WriteLine($"  [页 {p.PageNumber}] ⑤ 文字识别 {p.CurrentLine}/{p.TotalLines}"));
var result = engine.Recognize(image, pageNumber: 1, progress: progressCallback, lineProgress: lineProgress);

// 请求中止(实际停止发生在下一个阶段边界)
engine.Abort();

// 销毁引擎(也可用 using/Dispose)
engine.Destroy();

回调接口

// 7 阶段进度回调(带时长重载默认转发到无时长重载)
public interface IOcrProgressCallback
{
    void OnCheckpoint(OcrCheckpoint checkpoint, int pageNumber, int totalPages);

    // 带截至当前累计耗时的重载;最后阶段(Done)的 TotalMs 为完整耗时
    void OnCheckpoint(OcrCheckpoint checkpoint, int pageNumber, int totalPages, OcrTimings timings)
        => OnCheckpoint(checkpoint, pageNumber, totalPages);
}

// OcrCheckpoint 枚举:Idle(0) → DocOrientation(1) → Unwarp(2)
//   → Detection(3) → TextlineClassification(4) → Recognition(5) → Done(6)

// 日志回调
public interface ILogCallback
{
    void Log(OcrLogLevel level, string message);
}
OcrLogLevel — 日志级别枚举
public enum OcrLogLevel
{
    Debug = 0,
    Info = 1,
    Warning = 2,
    Error = 3,
}
NullLogCallback / NullProgressCallback — 空实现

当不需要日志或进度回调时,可使用内置的空实现(也可直接传 null,引擎内部会自动回退到 NullLogCallback):

public sealed class NullLogCallback : ILogCallback
{
    public static readonly NullLogCallback Instance = new();
    public void Log(OcrLogLevel level, string message) { }
}

public sealed class NullProgressCallback : IOcrProgressCallback
{
    public static readonly NullProgressCallback Instance = new();
    public void OnCheckpoint(OcrCheckpoint checkpoint, int pageNumber, int totalPages) { }
}

OcrTimings — 结构化阶段耗时

PageOcrResult.Timings 始终非 null,记录各阶段累计耗时(毫秒):

public sealed class OcrTimings
{
    public double TotalMs { get; set; }                    // 流水线总耗时
    public double DocOrientationMs { get; set; }           // ① 文档方向分类
    public double UnwarpMs { get; set; }                   // ② 图像矫正(本期未实现,始终为 0)
    public double DetectionMs { get; set; }                // ③ 文本检测(DBNet)
    public double TextlineClassificationMs { get; set; }   // ④ 文本行方向分类
    public double RecognitionMs { get; set; }              // ⑤ 文本识别(CTC,含透视校正与批量推理)

    public string ToSummary();  // 可读摘要:"total=...ms | docOri=... | det=... | cls=... | rec=..."
}

跳过的阶段对应字段为 0;TotalMs >= 各阶段耗时之和(差值为中间处理开销)。

LineRecognitionProgress — 文本行识别细粒度进度

OcrEngine.RecognizePdfOcrProcessor.Process 均接受可选的 IProgress<LineRecognitionProgress>? lineProgress。在⑤识别阶段,每完成一个识别批次(PipelineOptions.RecBatchSize 行/批)触发一次,用于显示类似 "⑤ 文字识别 8/12" 的实时进度:

public sealed record LineRecognitionProgress(int PageNumber, int CurrentLine, int TotalLines);
  • PageNumber:当前页码(1-indexed)
  • CurrentLine:已识别的文本行数(0..TotalLines;0 表示刚进入识别阶段)
  • TotalLines:本页待识别的文本行总数(已剔除透视校正失败的退化框)

控制台程序建议使用同步 IProgress<T> 实现(如 cli_demo 中的 SyncProgress<T>),避免 Progress<T> 在无 SynchronizationContext 时走线程池导致输出顺序错乱。

PdfOcrProcessor — PDF 处理

var processor = new PdfOcrProcessor(opt);

// 处理 PDF 文件路径
byte[] pdf = processor.Process("input.pdf", progress, checkpointCallback, ct);

// 处理 PDF 字节数组
byte[] pdf = processor.Process(inputPdfBytes, progress, checkpointCallback, ct);

// 同时传入行级细粒度进度回调
byte[] pdf = processor.Process("input.pdf", progress, checkpointCallback, ct, lineProgress);

// 调试模式:可见文字图层(默认蓝色),用于排查文字位置
using var debugProcessor = new PdfOcrProcessor()
{
    DebugVisibleText = true,
    DebugVisibleColor = (1.0, 0, 0), // 红色
};
debugProcessor.Process("input.pdf", "output.pdf");

指定/排除页码(页码过滤)

Process 的所有重载均接受两个可选参数 pagesToProcessexcludePages(1-indexed),用于仅对部分页执行 OCR 或排除某些页:

  • pagesToProcess:仅对这些页执行 OCR;为 null 时处理全部页。
  • excludePages:排除这些页,不对其执行 OCR;为 null 时不排除任何页。
  • 排除优先于包含:若某页同时出现在两者中,该页会被排除。
  • 被跳过的页在输出 PDF 中保留原始内容,仅不添加文字图层。
  • 超出 [1, totalPages] 范围的页码会被忽略并记录一条 Warning 日志。
// 仅 OCR 第 1、3、5-8 页
byte[] pdf = processor.Process("input.pdf",
    pagesToProcess: new[] { 1, 3, 5, 6, 7, 8 });

// 排除第 2、4 页
byte[] pdf = processor.Process("input.pdf",
    excludePages: new[] { 2, 4 });

// 同时使用:仅处理 1-10 页,但排除其中的 3、7 页
byte[] pdf = processor.Process("input.pdf",
    pagesToProcess: Enumerable.Range(1, 10),
    excludePages: new[] { 3, 7 });
PageRange — 页码范围字符串解析

PageRange.Parse 可把 "1,3,5-8" 这类字符串解析为页码集合,便于从命令行参数或配置文件中接收页码范围:

// 解析字符串
PageRange range = PageRange.Parse("1,3,5-8");  // {1, 3, 5, 6, 7, 8}

// PageRange 实现 IEnumerable<int>,可直接传入 Process
byte[] pdf = processor.Process("input.pdf",
    pagesToProcess: PageRange.Parse("1,3,5-8"),
    excludePages: PageRange.Parse("2,4"));

// 安全解析(失败返回 false)
if (PageRange.TryParse(userInput, out var parsed))
    processor.Process("input.pdf", pagesToProcess: parsed);

// 单独判断某页是否在集合内
bool has5 = parsed.Contains(5);
int[] sorted = parsed.ToSortedArray();

支持的格式:

  • 单个页码:"5"
  • 闭区间:"3-7"(包含 3 与 7)
  • 逗号分隔多项:"1,3,5-8,10"
  • 空白字符会被忽略,重复或乱序的页码会被自动去重排序

完整示例

参见 cli_demo/Program.cs,提供以下子命令(运行 pdfocr-cli help 查看完整帮助,pdfocr-cli help <命令> 查看指定命令的详细用法):

命令 说明
defaults 显示 PipelineOptions.Default 全部默认配置
config 显示自定义配置示例
sample <image.png> 生成带文字的测试样图
recognize <image> [thr] 创建引擎 → 执行 OCR → 打印条数与文本 → 销毁引擎
timings <image> 演示结构化时长接口(PageOcrResult.Timings + 带时长的进度回调)
abort <image> 启动 OCR 后立即 Abort,演示中止
progress <image> 演示 7 阶段 Checkpoint 回调 + 行级细粒度进度
log <image> 演示日志回调
pdf <input> <output> [--pages 1,3,5-8] [--exclude 2,4] [--debug-visible] [--color R,G,B] 使用 PdfOcrProcessor 处理 PDF(可选指定/排除页码、调试可见文字、自定义颜色)
pdfpages <input> [--pages 3] [--exclude 2,4] [--out <path>] 端到端测试页码过滤参数,处理后自动校验
verify <pdf> [page] 验证 OCR 文本框坐标与 PDF 文字层实际位置是否一致
extract <pdf> <page> <out.png> 从 PDF 提取指定页图像
inspect <pdf> [page] [ops] 检查 PDF 结构(页数、字体、内容流、文字提取)
diagnose <image> 分阶段诊断识别问题
diagmodel [image] 检查 rec 模型 I/O 元数据与字典,对比归一化策略
help [命令] 显示总览或指定命令的详细帮助
--version, -v 显示版本号
# 构建并运行
dotnet build cli_demo/cli_demo.csproj -c Release

# 查看帮助
dotnet cli_demo/bin/Release/net10.0/pdfocr-cli.dll help
dotnet cli_demo/bin/Release/net10.0/pdfocr-cli.dll help pdf

# 处理 PDF(仅 OCR 第 1、3、5-8 页)
dotnet cli_demo/bin/Release/net10.0/pdfocr-cli.dll pdf input.pdf output.pdf --pages 1,3,5-8

# 端到端测试页码过滤(默认仅处理第 3 页并自动校验)
dotnet cli_demo/bin/Release/net10.0/pdfocr-cli.dll pdfpages input.pdf

# 使用可见文字图层排查位置(默认蓝色)
dotnet cli_demo/bin/Release/net10.0/pdfocr-cli.dll pdf input.pdf output.pdf --debug-visible

# 使用红色可见文字图层排查位置
dotnet cli_demo/bin/Release/net10.0/pdfocr-cli.dll pdf input.pdf output.pdf --debug-visible --color 255,0,0

项目结构

PdfOcrLayer/
├── src/PdfOcrLayer/              # NuGet 类库
│   ├── OcrEngine.cs              # 引擎生命周期(构造/Recognize/Abort/Destroy)
│   ├── PdfOcrProcessor.cs        # PDF 主编排器(提图 → OCR → 写文字图层,支持页码过滤)
│   ├── PageRange.cs              # 页码范围解析器("1,3,5-8" → 页码集合)
│   ├── PipelineOptions.cs        # 流水线可调参数
│   ├── OcrCallbacks.cs           # 进度/日志回调接口 + OcrCheckpoint 枚举
│   ├── OcrResult.cs              # PageOcrResult / TextRegion
│   ├── Pipeline/PpOcrPipeline.cs # OCR 完整流水线编排
│   ├── Onnx/                     # ONNX 模型封装
│   │   ├── OnnxModelBase.cs
│   │   ├── DocOrientationClassifier.cs
│   │   ├── TextDetector.cs
│   │   ├── TextlineOrientationClassifier.cs
│   │   └── TextRecognizer.cs
│   ├── Image/                    # 图像预处理(NCHW 转换、透视变换等)
│   ├── Pdf/                      # PDF 图像提取与文字图层写入
│   ├── Models/                   # 文件系统资源加载 + YAML 配置解析
│   └── buildTransitive/          # NuGet 包使用的 .targets 文件(自动复制资源到输出目录)
├── cli_demo/                     # 命令行示例程序
├── Assets/
│   ├── Models/                   # ONNX 模型 + YAML 配置
│   └── Fonts/                    # HarmonyOS Sans SC 字体
└── artifacts/
    └── nupkg/                    # 构建产物(NuGet 包)

技术细节

OCR 流水线

  1. 文档方向分类:将图像分类为 0°/90°/180°/270°,必要时旋转校正
  2. 文本检测(DBNet):基于可微二值化的文本检测,输出四边形文本框
  3. 文本行方向分类:检测倒置(180°)的文本行,必要时翻转
  4. 文本识别(CTC):透视校正文本框为水平条带,CTC 解码输出字符序列

文字图层写入

OCR 结果通过 PDF 文字渲染模式 3(Tr=3)写为不可见文字:

  • 文字不可见(不绘制字形),但可选中、可复制、可被搜索引擎索引
  • 使用嵌入的 HarmonyOS Sans SC 字体进行字体子集化,仅包含实际用到的字符
  • 文字坐标精确对齐到原图像中的文本框位置
  • 文字方向自动跟随文本框方向:根据文档旋转(0°/90°/180°/270°)和行内 180° 翻转,计算文字矩阵 [a b c d e f],使不可见文字与图像中的视觉文字方向一致,选区与字符精确对齐
  • 字号按文本框短边估算:取文本框左右两条短边(TL→BL、TR→BR)的长度均值,避免在文档旋转 90°/270° 时误用长边(行宽)导致字号被放大
  • 文字宽度自适应文本框:利用嵌入字体的 glyph 宽度度量,计算渲染总宽度与文本框宽度的差值,自动应用 Tw(词间距)、Tc(字符间距)或 Tz(水平缩放)使文字层精确填充文本框,提升选中区域与视觉文字的贴合度
  • 字体子集化采用 GID 重编号方案:重建 glyf/loca/hmtx/cmap 等所有相关表,丢弃 PDF 文字层不需要的 vmtx/vhea/GPOS/GSUB/GDEF/DSIG,实测子集字体大小可从 629KB 缩小到 5KB(缩小 126 倍)
  • 使用 Type0 / CIDFontType2 复合字体 + Identity CIDToGIDMap + 2 字节 GID 字符码 + ToUnicode CMap,支持任意字符集(无 255 字符限制)

资源文件加载机制

ONNX 模型、YAML 配置和 TTF 字体不再嵌入 DLL,而是作为 NuGet 包的 contentFiles 分发:

  • NuGet 消费者buildTransitive/PdfOcrLayer.targets 在首次编译时自动将资源文件复制到输出目录的 PdfOcrLayer/ 子目录下
  • ProjectReference 开发PdfOcrLayer.csproj 中的 Content 项(CopyToOutputDirectory="PreserveNewest")将资源同步到输出目录
  • 运行时加载EmbeddedAssets{BasePath}/PdfOcrLayer/{subDir}/{file} → 递归向上查找 Assets/ 目录的顺序搜索资源文件
  • DLL 体积:每个目标框架的 DLL 仅 ~88 KB(纯代码,不含资源),两个框架合计 ~176 KB(原为 66 MB)
字体文件重复说明

本包自带的 HarmonyOS_Sans_SC_Regular.ttf(约 7.9 MB)存放在 NuGet 包 contentFiles 目录中,Link 路径为 PdfOcrLayer/Fonts/HarmonyOS_Sans_SC_Regular.ttf

如果你在自己的项目中也引用了同一份字体文件(放在其他路径),编译和运行均不会产生冲突,但输出目录中会多出一份 7.9 MB 的副本。

避免字体包重复的方案:

直接通过 PdfOcrLayer.Models.EmbeddedAssets.HarmonyOsSansScFont 读取本包自带的字体字节,无需自己再带一份

许可证

MulanPSL-2.0,详见 LICENSE

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

详见 CHANGELOG.md