PdfOcrLayer 0.5.1
dotnet add package PdfOcrLayer --version 0.5.1
NuGet\Install-Package PdfOcrLayer -Version 0.5.1
<PackageReference Include="PdfOcrLayer" Version="0.5.1" />
<PackageVersion Include="PdfOcrLayer" Version="0.5.1" />
<PackageReference Include="PdfOcrLayer" />
paket add PdfOcrLayer --version 0.5.1
#r "nuget: PdfOcrLayer, 0.5.1"
#:package PdfOcrLayer@0.5.1
#addin nuget:?package=PdfOcrLayer&version=0.5.1
#tool nuget:?package=PdfOcrLayer&version=0.5.1
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.Recognize 与 PdfOcrProcessor.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 的所有重载均接受两个可选参数 pagesToProcess 与 excludePages(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 流水线
- 文档方向分类:将图像分类为 0°/90°/180°/270°,必要时旋转校正
- 文本检测(DBNet):基于可微二值化的文本检测,输出四边形文本框
- 文本行方向分类:检测倒置(180°)的文本行,必要时翻转
- 文本识别(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 | Versions 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. |
-
net10.0
- Microsoft.ML.OnnxRuntime (>= 1.20.1)
- PdfPig (>= 0.1.15)
- SixLabors.Fonts (>= 2.0.8)
- SixLabors.ImageSharp (>= 3.1.7)
- YamlDotNet (>= 16.3.0)
-
net8.0
- Microsoft.ML.OnnxRuntime (>= 1.20.1)
- PdfPig (>= 0.1.15)
- SixLabors.Fonts (>= 2.0.8)
- SixLabors.ImageSharp (>= 3.1.7)
- YamlDotNet (>= 16.3.0)
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