蓝娅萍 发表于 2025-6-28 14:10:07

使用 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 文本
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);
                }
        }
}其输出示例如下所示:

图像显示了三个单词“Write something in”,分为 2 个部分,顶部是正常的 PDF 输出,底部是相同的文本,带有 3 个粉红色的单词边界框和蓝绿色的字母边界框
对于顶部显示的 PDF 文本(“Write something in”),将检测到 3 个单词(粉红色),每个单词都包含带有字形边界框的单个字母。
创建 PDF 文档

要创建文档,请使用 PdfDocumentBuilder 类。标准 14 种字体提供了一种快速入门的方法。
在 PDF 之中有 14 种字体可以不嵌入到PDF中而直接支持,也就是说 PDF 阅读器必须支持这十四种字体,如果不支持的话,那么 PDF 中可能显示不出来正确的文字。这十四种字体为:

[*]Times-Roman
[*]Times-Bold
[*]Time-Italic
[*]Time-BoldItalic
[*]Courier
[*]Courier
[*]Courier-Bold
[*]Courier-Oblique
[*]Helvetica®
[*]Helvetica®-Bold
[*]Helvetica®-Oblique
[*]Helvetica®-BoldOblique
[*]Symbol
[*]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!”

图像显示了在 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 保存文件
}
图像显示由上述代码块创建的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;
    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);API 参考资料

如果您希望生成 doxygen 文档,请运行 doxygen doxygen-docs,然后打开 docs/doxygen/html/index.html。
有关API部分的详细文档,请参见 Wiki。
问题

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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 使用 PdfPig 处理 PDF 文档