65.9K
CodeProject 正在变化。 阅读更多。
Home

从 PDF 文件读取 Acrofields

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2015年9月29日

GPL3

6分钟阅读

viewsIcon

42488

downloadIcon

970

生活的多样性 - 有益的 PDF AcroForm 阅读器。

引言

我真正喜欢咨询工作的一点是,我能接触到各种各样的项目。最近,有位客户问我们是否能为他们开发一个应用程序,让他们可以填写 PDF 并将数据存储在数据库中。

背景

我四处寻找,发现了几种不同的处理 PDF 文件的库,但我并不是说我做了详尽的搜索或试用。几乎所有这些库都宣称它们在代码中创建 PDF 文档非常容易,许多库提供了读取文档的功能,但几乎所有库都无法加载用 Adobe Acrobat 创建的文档,只能打开较旧的未加密 PDF 文件。

我发现的一个例外是 iTextSharp,这是一个来自 http://itextpdf.com/ 的库,它既提供有 iText 支持的商业版本,也提供一个遵循 copyleft AGPL 许可证的开源版本。

Using the Code

下载代码后,您需要使用 Nuget 下载 iTextSharp 和 SQLite 包。请注意每个包的许可要求。

该项目是在 Visual Studio 2015 上开发的,目标是 .NET 4.5 版本,但也已在 Visual Studio 2013 上测试过。

附带的软件是为我们客户解决问题的方案。它是一个 Windows 服务,可以监视(使用 FileSystemWatcher)PDF 文件被拖放到特定位置或被修改,并读取表单字段集合和页面内容标记。

表单字段集合以键值对的形式保存在 SQLite 数据库表中,字段名称为键,其数据为值。

该项目包含四个项目和一个用于帮助安装的部署项目。

  1. SaveToDB – 包含程序运行器,用于查找被拖放并保存到数据库的文件。
  2. DataClass – 提供读写数据库的方法,它同时支持 SQLite 和 Microsoft SQL Server 调用。在连接字符串中使用 providerName="SQLite" 将数据保存到 SQLite 数据库,否则将默认使用 SQL Server。
  3. LoggerClass – 简化的日志记录例程。
  4. PDFScanner – 一个 Windows 服务项目,它引导一个程序运行器并包含用于 installutil.exe 的安装程序。

(日志记录器和配置管理器在这里被实例化,并注入到 ProgramRunner 类的 runProgram 函数中。)

NameValueCollection cfg = ConfigurationManager.AppSettings;

ProgramRunner pr = new ProgramRunner();
ILogger lg = new Logger((Logger.LogLevel)Enum.Parse
             (typeof(Logger.LogLevel), cfg["LogLevel"]), cfg["LogLocation"], "logDB.txt");

pr.runProgram(lg, cfg);

主要处理过程被封装在一个 using 语句中,并为文件夹中每个被拖放或更改的文件进行处理。

PdfReader 有十二种不同的重载方法,我选择的是直接从磁盘打开文件。using 语句确保与 PdfReader 相关的所有资源都能被正确关闭和释放。

foreach (string item in GetFilesToProcess())
{
     string newFile = RenameFile(item);

     //We determine new name to copy to, but read from the original file (item).
     using (PdfReader reader = new PdfReader(item))
     {

     }
}

在读取 PDF 文件时,会扫描文本和表单字段。这部分代码读取第一页,返回一个 StringBuilder 对象,该对象可以与保存在数据库中的表单类型的正则表达式进行匹配。

//Recognition goes to ITEXTPdf.com - http://itextpdf.com/examples/iia.php?id=275
StringBuilder sb = new StringBuilder();
byte[] streamBytes = reader.GetPageContent(1);
PRTokeniser tokenizer = new PRTokeniser(new RandomAccessFileOrArray
                        (new RandomAccessSourceFactory().CreateSource(streamBytes)));
        while (tokenizer.NextToken())
        {
            if (tokenizer.TokenType == PRTokeniser.TokType.STRING)
            {
                 sb.Append(tokenizer.StringValue);

            }
        }

有一个选项是使用正则表达式扫描文本,查找匹配项以确定表单类型。如果没有匹配项,它将查看文件名来确定类型,并使用文件名作为类型。

//Specify regex to search in first page
//Regex rx = new Regex("Number:(?<one>.+)Rev:");
   Regex rx = new Regex(_cfg["FileTypeRegEx"]);
   var group = rx.Match(sb.ToString()).Groups["one"];
   string ftype = group.Value.ToString().Trim();

 //If there is no match, pick up the filename without extension and use as file type
    if (string.IsNullOrEmpty(ftype))
              ftype = System.IO.Path.GetFileNameWithoutExtension(item);

我在应用程序配置文件的 FileTypeRegEx 键下指定搜索字符串

由于配置文件是 XML 文档,可能被解释为标签的字符必须进行转义。因此

Number: (?<one>.+)Rev:” – 查找 Number:Rev: 之间的字符并分配给“one”组,必须重写为 - “Number: (?&lt;one&gt;>.+)Rev:

<add key="FileTypeRegEx" value="Form Type: (?&lt;one&gt;\w+-\d+)" />

AcroFields 从文档中读取并使用参数化查询保存到数据库中,AcroFields 是整个文档范围的,而不是按页引用的。

如果字段有数据,以下字段数据将被保存在数据库中:

文件名、字段名、字段值、字段类型、文件类型

//Recognition goes to  https://simpledotnetsolutions.wordpress.com/2012/04/08/itextsharp-few-c-examples
//and http://itextpdf.com/examples/iia.php?id=121                                 

foreach (var field in fields.Fields)
{
      string fvalue = fields.GetField(field.Key.ToString()).ToString();

      if (!string.IsNullOrEmpty(fvalue.Trim()))
      {
              _locallog.Log("insert data", "storage", Logger.LogLevel.Info);

              while (!dbstuff.execCmdsNonQuery(sqldb, "insert into tstorage 
                    (FileName,FieldName,FieldValue,FieldType,FileType) values 
                    (@file,@field,@value,@type,@filetype)",
                    new SqlParameter[] { new SqlParameter("@file", newFile), 
                    new SqlParameter("@field", fields.GetTranslatedFieldName(field.Key)),
                    new SqlParameter("@value", fvalue), 
                    new SqlParameter("@type", fields.GetFieldType(field.Key).ToString()), 
                    new SqlParameter("@filetype", ftype) }))
                                            ;
       }
}

该表使用以下模式定义:

CREATE TABLE "TStorage" (
               `FileName`           TEXT NOT NULL,
               `FieldName`         TEXT NOT NULL,
               `FieldValue`          TEXT NOT NULL,
               `FieldType`           TEXT NOT NULL,
               `FieldProcessed`  INTEGER DEFAULT 0,
               `FileType`             TEXT,
               PRIMARY KEY(FileName,FieldName)
)

FileName 字段包含添加了时间戳newFile)以确保唯一性之后的文件名。

FieldName 使用 fields.GetTranslatedFieldName(field.Key) 派生,是创建文档时在 PDF 中分配的字段名。

FieldValue 是在 PDF 字段中填写的值,使用 fields.GetField(field.Key.ToString()).ToString() 方法读取。

我在表上有一个额外的字段,用于帮助与可能从数据库中读取的其他进程同步。FieldProcessed 在插入记录时默认为零,当文件的所有记录都写入后,会通过文件名进行基于集合的更新,将其更新为 1

这样,另一个进程就不会读取到部分加载的数据。此外,之后另一个进程可以将该字段标记为 2,表示该行不再需要,服务将使用基于集合的删除来删除它们。

FieldType 可以是八个值之一,定义了数据所代表的字段类型。

Pushbutton = 1(按钮),Checkbox = 2(复选框),Radiobutton = 3(单选按钮),Text = 4(文本),List = 5(列表),Combo = 6(组合框),Signature = 7(签名)和 None = 0(无)。

fileType 在文档本身中被定义为文本。我们所有的 PDF 都包含一个表单编号,我们用正则表达式来确定它。

一个 PDF 示例及其生成的内容

First test form

创建以下记录集:

Form Data

每一行指定表单中的一个字段。如果您想要一个列式表示,您需要对行进行透视转换。

Pivoting into columns

第二个相同类型的文件将生成如下所示的另一行。

Second saved PDF

two rows

请注意,由于我没有填写姓氏字段,因此没有写入相应的行,并且该列现在返回 NULL

关注点

使用 iTextSharp 非常简单,它有大量有用的文章和示例,并且第一次使用就如预期般工作。这次尝试非常成功,我希望这篇文章能为如何使用这个优秀软件包的有用示例库增添一笔,并可能对您的工作有所帮助。

本示例的软件在 AGPL 许可证下使用 iTextSharp,我已经包含了必要的通知、修改说明、AGPL 许可证文件以及获取源代码的位置。

您需要使用 NuGet 获取软件包。

商业许可证提供了开源 AGPL 许可证所不具备的优势。具体好处包括:

  • 在发生知识产权(IP)或专利侵权时提供赔偿。
  • 免除 copyleft AGPL 许可证的要求,这些要求包括:
    • 分发所有源代码,包括您自己的产品(即使是基于 Web 的应用程序)。
    • 您自己的产品必须在 AGPL 许可下授权。
    • 显著提及并包含 iText 版权和 AGPL 许可证。
    • 披露所做的修改。
  • 免除在生成的 PDF 属性中不得更改 PDF Producer 行的要求。
  • 只有商业许可证持有者才能获得商业 iText 支持。

历史

  • 2015年9月23日:提交
© . All rights reserved.