冠军

导航

使用 PdfPig 处理 PDF 文档

使用 AI 应用模板扩展创建支持使用自定义数据进行 chat 的 .NET AI 应用 中,看到里面这个示例中使用了 PdfPig 这个 Pdf 处理库,在该示例中, 使用 PdfPig 来提取 Pdf 文档中的文字内容. 与 Word 不同, Pdf 用于排版输出, 导致我们看到的段落在 Pdf 内部并不一定是一个段落单位, PdfPig 对这些问题专门进行了处理.

以前使用过 PDFSharp,对 PdfPig 不了解,特地找时间学习了一下。

PdfPig 的 GitHub 地址:https://github.com/UglyToad/PdfPig

PdfPig 的 Wiki 地址: https://github.com/UglyToad/PdfPig/wiki

从说明中可以看到,他是从 PDFBox 移植到 .NET 中的。而 PDFBox 是 apache 旗下著名的基于 Java 技术的开源 PDF 文档处理库。PDFBox 的文档位于:https://pdfbox.apache.org/ ,记得它有一个特殊的功能是可以用来调试 PDF 文档,让我们看到 PDF 文档内部的详细结构,这个功能在学习 PDF 结构的时候,非常方便。

注意:从文档看, 当前它只支持 ASCII 文本, 但已经看到一个创建中文 pdf 的例子, 见最后部分的示例.

NuGet

PdfPig 可以通过 NuGet 获得: https://www.nuget.org/packages/PdfPig, 当前版本是 0.1.11-alpha-20250626-d1d79

实际上,它还没有正式发布 1.0 版本。当前 (2025/3/8) 发布的是 v0.1.10

使用 dotnet 的 add package 可以将它引用到项目中。

dotnet add package PdfPig --version 0.1.11-alpha-20250626-d1d79

快速入门

Wiki 中可以找到详细说明。

读取页面中的文本

在这个阶段,最简单的用法就是打开一份文档,逐页阅读文字:

using (PdfDocument document = PdfDocument.Open(@"C:\Documents\document.pdf"))
{
	foreach (Page page in document.GetPages())
	{
		string pageText = page.Text;

		foreach (Word word in page.GetWords())
		{
			Console.WriteLine(word.Text);
		}
	}
}

其输出示例如下所示:

example-text-extraction

图像显示了三个单词“Write something in”,分为 2 个部分,顶部是正常的 PDF 输出,底部是相同的文本,带有 3 个粉红色的单词边界框和蓝绿色的字母边界框

对于顶部显示的 PDF 文本(“Write something in”),将检测到 3 个单词(粉红色),每个单词都包含带有字形边界框的单个字母。

创建 PDF 文档

要创建文档,请使用 PdfDocumentBuilder 类。标准 14 种字体提供了一种快速入门的方法。

在 PDF 之中有 14 种字体可以不嵌入到PDF中而直接支持,也就是说 PDF 阅读器必须支持这十四种字体,如果不支持的话,那么 PDF 中可能显示不出来正确的文字。这十四种字体为:

  1. Times-Roman
  2. Times-Bold
  3. Time-Italic
  4. Time-BoldItalic
  5. Courier
  6. Courier
  7. Courier-Bold
  8. Courier-Oblique
  9. Helvetica®
  10. Helvetica®-Bold
  11. Helvetica®-Oblique
  12. Helvetica®-BoldOblique
  13. Symbol
  14. ZapfDingbats

见:https://kbpdfstudio.qoppa.com/standard-14-pdf-fonts/

该示例使用的是 Helvetica 字体。

PdfDocumentBuilder builder = new PdfDocumentBuilder();

PdfPageBuilder page = builder.AddPage(PageSize.A4);

// Fonts must be registered with the document builder prior to use to prevent duplication.
// 字体必须在使用前先在文档构建器中注册,以防止重复。
PdfDocumentBuilder.AddedFont font = builder.AddStandard14Font(Standard14Font.Helvetica);

page.AddText("Hello World!", 12, new PdfPoint(25, 700), font);

byte[] documentBytes = builder.Build();

File.WriteAllBytes(@"C:\git\newPdf.pdf", documentBytes);

输出是一个 1 页的PDF文档,文档顶部附近用 Helvetica 字体显示文本“Hello World!”

builder-output

图像显示了在 Google Chrome 的PDF查看器中的 PDF 文档。可以看到文本 “Hello World!”,每种字体必须在使用之前向 PdfDocumentBuilder 注册,以便使页面能够共享字体资源。仅支持标准 14 种字体和TrueType字体(.ttf)。高级文档提取在这个例子中,执行了更高级的文档提取。使用PdfDocumentBuilder创建一个带有调试信息(边界框和阅读顺序)添加的 pdf 副本。

页面分割器 (Page Segmenter) 负责在页面中查找文本块。它们返回一个可视为段落的 TextBlock 列表。每个 TextBlock 包含属于它的行 (TextLine) 列表。反过来,每个 TextLine 包含属于它的 Words 列表。每个元素都有自己的边界框和文本。

高级示例

//using UglyToad.PdfPig;
//using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter;
//using UglyToad.PdfPig.DocumentLayoutAnalysis.ReadingOrderDetector;
//using UglyToad.PdfPig.DocumentLayoutAnalysis.WordExtractor;
//using UglyToad.PdfPig.Fonts.Standard14Fonts;
//using UglyToad.PdfPig.Writer;

var sourcePdfPath = "";
var outputPath = "";
var pageNumber = 1;
using (var document = PdfDocument.Open(sourcePdfPath))
{
    var builder = new PdfDocumentBuilder { };
    PdfDocumentBuilder.AddedFont font = builder.AddStandard14Font(Standard14Font.Helvetica);
    var pageBuilder = builder.AddPage(document, pageNumber);
    pageBuilder.SetStrokeColor(0, 255, 0);
    var page = document.GetPage(pageNumber);

    var letters = page.Letters; // no preprocessing

    // 1. Extract words 单词提取器
    var wordExtractor = NearestNeighbourWordExtractor.Instance;

    var words = wordExtractor.GetWords(letters);

    // 2. Segment page 页面分割器, 用来将页面分割为多个文本块
    var pageSegmenter = DocstrumBoundingBoxes.Instance;

    var textBlocks = pageSegmenter.GetBlocks(words);

    // 3. Postprocessing 后处理
    var readingOrder = UnsupervisedReadingOrderDetector.Instance;
    var orderedTextBlocks = readingOrder.Get(textBlocks);

    // 4. Add debug info - Bounding boxes and reading order
   //     添加调试信息 - 为文本块加上边框和顺序编号
    foreach (var block in orderedTextBlocks)
    {
        var bbox = block.BoundingBox;
        pageBuilder.DrawRectangle(bbox.BottomLeft, bbox.Width, bbox.Height);
        pageBuilder.AddText(block.ReadingOrder.ToString(), 8, bbox.TopLeft, font);
    }

    // 5. Write result to a file 将结果写回文件中
    byte[] fileBytes = builder.Build();
    File.WriteAllBytes(outputPath, fileBytes); // save to file 保存文件
}

boundingBoxes_ReadingOrder

图像显示由上述代码块创建的PDF文档,其中文字的边界框和阅读顺序被展示。

有关 高级文档分析 的更多信息,请参见文档布局分析。有关更高级工具以分析文档布局,请参见 导出

使用方式

PdfDocument

PdfDocument 类提供了访问从文件加载或作为字节传递的文档内容的方法。要从文件打开,请使用 PdfDocument.Open 静态方法:

using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;

using (PdfDocument document = PdfDocument.Open(@"C:\my-file.pdf"))
{
	int pageCount = document.NumberOfPages;

	// Page number starts from 1, not 0.
	Page page = document.GetPage(1);

	decimal widthInPoints = page.Width;
	decimal heightInPoints = page.Height;

	string text = page.Text;
}

PdfDocument 只能在 using 语句中使用,因为它实现了 IDisposable 接口(除非消费者在其他地方处置它)。加密文档可以通过 PdfPig 打开。要提供所有者或用户密码,请在调用 Open 方法时提供可选的 ParsingOptions,并定义 Password 属性。例如:

using (PdfDocument document 
        = PdfDocument.Open(
              @"C:\my-file.pdf",  
              new ParsingOptions { Password = "password here" }))

您还可以提供一个密码列表尝试:

using (PdfDocument document = PdfDocument.Open(@"C:\file.pdf", new ParsingOptions
{
	Passwords = new List<string> { "One", "Two" }
}))

该文档包含其遵循的PDF规范的版本,通过 document.Version 访问。

decimal version = document.Version;

创建 PDF 文档

PdfDocumentBuilder 创建一个没有页面或内容的新文档。对于文本内容,必须先在构建器中注册字体。此库默认支持 Adobe 提供的标准 14 种字体和 TrueType 格式字体。要添加标准 14 字体,请使用:

public AddedFont AddStandard14Font(Standard14Font type)

或者,对于 TrueType 字体这样使用:

AddedFont AddTrueTypeFont(IReadOnlyList<byte> fontFileBytes)

传递 TrueType 文件 (.ttf) 的字节。您可以使用以下方法检查 TrueType 文件嵌入到 PDF 文档中的适用性:

bool CanUseTrueTypeFont(IReadOnlyList<byte> fontFileBytes, out IReadOnlyList<string> reasons)

如果检查失败,则提供了不能使用该字体的原因列表。您应该在使用之前检查 TrueType 字体的许可证,因为压缩字体文件嵌入在结果文档中并与之一起分发。AddedFont 类表示存储在文档构建器上的字体的关键。在向页面添加文本内容时必须提供此键。要向文档添加页面,请使用:

PdfPageBuilder AddPage(PageSize size, bool isPortrait = true)

这创建了一个具有指定大小的新 PdfPageBuilder。第一个添加的页面是第 1 页,然后是第 2 页,接着是第 3 页,依此类推。页面构建器支持添加文本、绘制线条和矩形,并在绘制之前测量文本的大小。要绘制线条和矩形,请使用以下方法:

void DrawLine(PdfPoint from, PdfPoint to, decimal lineWidth = 1)
void DrawRectangle(PdfPoint position, decimal width, decimal height, decimal lineWidth = 1)

线宽可以变化,默认为 1。矩形是未填充的,目前填充颜色无法更改。要在页面上写入文本,您必须拥有来自 PdfDocumentBuilder 上上述方法的AddedFont 的引用。然后,您可以使用以下方法将文本绘制到页面上:

IReadOnlyList<Letter> AddText(string text, decimal fontSize, PdfPoint position, PdfDocumentBuilder.AddedFont font)

位置是绘制文本的基线。目前仅支持 ASCII 文本。您还可以使用以下方法在绘制之前测量文本的结果大小:

IReadOnlyList<Letter> MeasureText(string text, decimal fontSize, PdfPoint position, PdfDocumentBuilder.AddedFont font)

这不会改变页面的状态,不像 AddText。支持使用以下方式更改文本、线条和矩形的 RGB 颜色:

void SetStrokeColor(byte r, byte g, byte b)
void SetTextAndFillColor(byte r, byte g, byte b)

RGB 值在 0 到 255 之间。该颜色将在调用这些方法后,所有后续操作中保持有效,直到使用以下方式重置:

void ResetColor()

这将重置描边、填充和文本绘制的颜色为黑色。

文档信息

PdfDocument 提供访问 PDF 文件中定义的 DocumentInformation 文档元数据。这些信息通常不会提供,因此大多数条目将为空:

PdfDocument document = PdfDocument.Open(fileName);

// The name of the program used to convert this document to PDF.
string producer = document.Information.Producer;

// The title given to the document
string title = document.Information.Title;
// etc...

文档结构

该文档现在具有一个 Structure 成员:

UglyToad.PdfPig.Structure structure = document.Structure;

这提供了对标记化 PDF 文档内容的访问:

Catalog catalog = structure.Catalog;
DictionaryToken pagesDictionary = catalog.PagesDictionary;

页面字典 PageDictionary 是 PDF 文档内部页面树的根。该结构还暴露了一个 GetObject(IndirectReference reference) 方法,允许随机访问 PDF 中的任何对象,只要其标识符编号是已知的。这是一个形式为 69 0 R 的标识符,其中 69 是对象编号,0 是代数。

页面

页面包含以点为单位的页面宽度和高度,以及映射到 PageSize 枚举:

PageSize size = Page.Size;

bool isA4 = size == PageSize.A4;

Page 提供对其中文本的访问支持:

string text = page.Text;

有一种方法可以访问单词 Word。默认方法使用基本启发式算法。对于高级情况,您还可以实现自己的 IWordExtractor,或使用 NearestNeighbourWordExtractor

IEnumerable<Word> words = page.GetWords();

您还可以访问用于在页面的内容流中绘制图形和内容的原始操作:

IReadOnlyList<IGraphicsStateOperation> operations = page.Operations;

请参阅 PDF 规范以了解各个操作符的含义。

还有一个 API 可以检索每个页面的 PDF 图像对象:

IEnumerable<XObjectImage> images = page.GetImages();

请阅读关于图像的 wiki。

字符

由于 PDF 内部结构的方式,页面文本可能不是文档中文本的可读表示形式。由于 PDF 是一种展示格式,文本可以以任何顺序绘制,而不一定是阅读顺序。这意味着可能缺少空格或单词可能在文本中处于意想不到的位置。

为了帮助用户解决页面上实际文本的顺序,页面文件提供了对字母列表的访问:

IReadOnlyList<Letter> letters = page.Letters;

这些字母包含:

  • 字母的文本:letter.Value。
  • 字母的左下角位置:letter.Location。
  • 字母的宽度:letter.Width。
  • 以未缩放的相对文本单位表示的字体大小(这些大小是PDF内部的,并且不对应于像素、点或其他单位的大小):letter.FontSize。
  • 如果存在的话,渲染字母的字体名称:letter.FontName。
  • 一个矩形,这是完全包含字母/字形可见区域的最小矩形:letter.GlyphRectangle。
  • 基线开始和结束点 StartBaseLine 和 EndBaseLine 表示字母是否旋转。TextDirection 指示这是否是常用旋转或自定义旋转。

字母位置在 PDF 坐标中测量,原点是页面的左下角。因此,较高的Y值意味着更靠近页面的顶部。

注解

使用以下方法检索每一页上的注释:

page.GetAnnotations()

此调用未被缓存,并且在使用之前文档不得被丢弃。

书签

文档的书签(大纲)可以在文档级别检索:

bool hasBookmarks = document.TryGetBookmarks(out Bookmarks bookmarks);

如果文档未定义任何书签,则将返回 false。

表单

可以使用以下方法检索交互式表单(AcroForms)的表单字段:

bool hasForm = document.TryGetForm(out AcroForm form);

如果文档不包含表单,则返回 false。可以使用 AcroForm 的 Fields 属性访问字段。由于表单是在文档级别定义的,因此这将返回文档中所有页面的字段。字段的类型由枚举 AcroFieldType 定义,例如 PushButton、Checkbox、Text 等。

请注意,这些表单是只读的,无法使用 PdfPig 更改或添加值。

超级链接

页面有一个方法来提取超链接(链接类型的注释):

IReadOnlyList<UglyToad.PdfPig.Content.Hyperlink> hyperlinks = page.GetHyperlinks();

TrueType 字体

用于处理 PDF 文件中 TrueType 字体的类可以供公开使用。给定一个输入文件:

using UglyToad.PdfPig.Fonts.TrueType;
using UglyToad.PdfPig.Fonts.TrueType.Parser;

byte[] fontBytes = System.IO.File.ReadAllBytes(@"C:\font.ttf");
TrueTypeDataBytes input = new TrueTypeDataBytes(fontBytes);
TrueTypeFont font = TrueTypeFontParser.Parse(input);

解析后的字体可以进行检查。

嵌入文件

PDF 文件可能包含完全嵌入在其中的其他文件用于文档注释。可以访问嵌入文件及其字节内容的列表:

if (document.Advanced.TryGetEmbeddedFiles(out IReadOnlyList<EmbeddedFile> files)
    && files.Count > 0)
{
    var firstFile = files[0];
    string name = firstFile.Name;
    IReadOnlyList<byte> bytes = firstFile.Bytes;
}

合并 PDF 文件

您可以使用 PdfMerger 类合并两个或更多现有的 PDF 文件:

var resultFileBytes = PdfMerger.Merge(filePath1, filePath2);
File.WriteAllBytes(@"C:\pdfs\outputfilename.pdf", resultFileBytes);

创建中文 Pdf

尽管官网中提到仅仅支持 ASCII 字符, 但这里有一个创建中文 pdf 的示例. 示例中使用了 Windows 中提供的字体文件.

using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.Core;
using UglyToad.PdfPig.Writer;

PdfDocumentBuilder builder = new PdfDocumentBuilder();
PdfPageBuilder page = builder.AddPage(PageSize.A4);


// 读取宋体字体文件到字节数组  
byte[] simSunFontBytes;
using (FileStream fontFileStream = File.OpenRead("C:\\Windows\\Fonts\\STSONG.TTF"))
{
    simSunFontBytes = new byte[fontFileStream.Length];
    fontFileStream.Read(simSunFontBytes, 0, simSunFontBytes.Length);
}
// 添加支持中文的字体  
PdfDocumentBuilder.AddedFont font = builder.AddTrueTypeFont(simSunFontBytes);

//写入
page.AddText("你好,这是一个PDF文档。", 12, new PdfPoint(25, 520), font);
byte[] b = builder.Build();

// 将PDF数据写入到文件中  
File.WriteAllBytes("output.pdf", b);

API 参考资料

如果您希望生成 doxygen 文档,请运行 doxygen doxygen-docs,然后打开 docs/doxygen/html/index.html。

有关API部分的详细文档,请参见 Wiki

问题

如果您遇到 bug,请 务必 提交问题。但是为了让我们能够协助您,您必须提供导致该问题的文件。请将其托管在一个公开可用的地方。感谢

参考资料

posted on 2025-06-28 16:09  冠军  阅读(182)  评论(0)    收藏  举报