技术文章

在 Delphi 中将 PDF 注释导入和导出为 XFDF

HotPDF 通过作用于当前加载文档的两个函数 ExportLoadedAnnotationsToXFDFImportLoadedAnnotationsFromXFDF,将 PDF 注释导出和导入为 XFDF。XFDF 是标准化为 ISO 19444-1 的 XML 注释交换格式,这一对函数允许 Delphi 或 C++Builder 程序将其批注传递给 Acrobat 或第三方评审工具,并将标记好的结果拿回来,所有这些都不需要重写注释所在的页面内容

想象一下它解决的两个方向。评审人员在 Acrobat 中打开您生成的报告,在对齐有误的图表上画一个红色箭头,圈出一个错误的总计,并在页边空白处键入一条说明,然后将批注导出到一个小型的 XFDF 文件中。或者是相反方向:您的程序自行生成标记,并且您需要将它们发送给使用非 HotPDF 工具的人。无论哪种方式,注释都作为双方都理解的 XML 进行传输,且 PDF 页面仍然与原来按字节保持一致

FDF 和 XFDF 有什么区别?

FDF 和 XFDF 在两种不同的语法中承载相同的有效载荷,当您决定将哪个文件移交给另一个工具时,这一区别就很重要。FDF 是 PDF 规范本身内部定义的较旧的表单数据格式:它使用 PDF 对象语法,so an FDF 文件看起来像是一个精简的 PDF,需要一个支持 PDF 的解析器才能读取。XFDF 是相同数据的 XML 表达形式,独立标准化为 ISO 19444-1,这意味着任何平台上的任何 XML 库都可以打开、diff 或生成它。两种格式都可以在 <fields> 树(受 ISO 19444-1 第 6.3 条管辖)中携带表单字段值,并在 <annots> 树中携带注释;HotPDF 拆分了这些职责,通过 ExportLoadedFormToXFDF 路由表单数据,并为 <annots> 端保留 ExportLoadedAnnotationsToXFDF。当您与 Web 服务、Java 评审服务器或脚本交换批注时,XFDF 是不强制另一端嵌入 PDF 解析器的格式

如何在 Delphi 中将 PDF 注释导出为 XFDF?

HotPDF 通过遍历已加载文档的每一页,为每个支持的注释发射一个 XFDF 元素,并返回写入的注释数量来导出注释。先加载 PDF,然后使用目标路径调用 ExportLoadedAnnotationsToXFDF。整数结果是序列化的注释计数;零或以下的结果意味着没有导出任何内容,也没有写入任何文件,这是您的信号,表明该文档没有携带支持的子类型的注释

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report-reviewed.pdf', '') > 0 then
    begin
      // 每一页上每个支持的注释都会写入一个 XFDF 元素
      Written := Pdf.ExportLoadedAnnotationsToXFDF('comments.xfdf');
      if Written <= 0 then
        ShowMessage('No supported annotations were found');
    end;
  finally
    Pdf.Free;
  end;
end;

输出的 XFDF 是简单的、可读的 XML。HotPDF 在 ISO 19444-1 命名空间中写入一个 <xfdf> 根,一个 <annots> 容器,并且每个注释都有一个子节点,其基于 0 的页面索引、颜色和几何形状作为属性或子元素。带黄色填充和开叉箭头的线条,坐落在填充多边形旁,序列化后如下所示

<?xml version="1.0" encoding="UTF-8"?>
<xfdf xmlns="http://ns.adobe.com/xfdf/">
  <annots>
    <line page="0" start="72,700" end="220,700"
          color="#FF0000" interior-color="#FFFF00"
          head="OpenArrow" tail="None">
      <contents-richtext>Baseline looks off</contents-richtext>
    </line>
    <polygon page="0" color="#0000FF" interior-color="#CCE5FF">
      <vertices>72,120;180,120;180,200;72,200</vertices>
    </polygon>
  </annots>
</xfdf>

注释子类型如何映射到 XFDF 元素

每个注释子类型映射到具有其自己的几何约定的特定 ISO 19444-1 元素,且 HotPDF 遵循这些结构而不是发明自己的。线条注释携带保存两个端点坐标对的 startend 属性(直接取自注释的 L 数组),而 LE 线端样式则成为 headtail 属性。多边形和折线注释将它们顶点的列表移入 <vertices> 子元素中,作为分号分隔的 x,y 对,而不是属性,因为期望子元素的阅读器会默默丢弃隐藏在其它地方的顶点。可以保存多个单独笔划的墨水(Ink)注释,嵌套了一个 <inklist> 元素,其中每个笔划有一个 <gesture> 子元素,因此多笔划签名可以作为不同的手势在传输中幸存,而不是作为一个合并的墨块

富文本、颜色和边框样式与几何形状一起保留。说明的富文本正文被写入为 <contents-richtext> 子节点;PDF 在 IC 数组中存储的内部填充(圆形、矩形、多边形或线条箭头的内部绘制以及擦除框的填充)在传输时表现为 #RRGGBB 形式的 interior-color 属性;并且边框宽度、虚线模式和云状边框效果映射到 widthdashesstyleintensity 属性,因此云形轮廓的标注在远端读取时仍为云状。HotPDF 还保留了附加在标记注释上的弹出窗口,将弹出子几何形状及其打开或关闭状态导入到注释的 Popup 字典中,并携带文本注释的打开和评审状态,因此评审过的文档不仅保留其形状,还保留评审人员所依赖的工作流元数据

将 XFDF 导入回加载的文档

HotPDF 通过解析 XML,使用 NewLoadedAnnotation 为每个元素创建全新的注释,将其附加到该元素命名的页面上,并返回它添加了多少个注释来导入 XFDF。该工作流程与导出是对称的:加载基底 PDF,使用评审人员的文件调用 ImportLoadedAnnotationsFromXFDF,然后保存加载的文档以持久化新的标记。如果文件缺失或 XML 无法解析,该函数返回零,加载的文档将保持原样

var
  Pdf: THotPDF;
  Added: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report.pdf', '') > 0 then
    begin
      Added := Pdf.ImportLoadedAnnotationsFromXFDF('comments.xfdf');
      if Added > 0 then
        Pdf.SaveLoadedDocument('report-annotated.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

因为每个 XFDF 元素都命名了自己的页面索引,所以即使您依次导入几个文件,注释也会降落在作者所针对的页面上,这使得在单次保存前,安全地将来自多个评审人员的批注收集到同一个加载的文档上。下面的示例将两位评审人员的意见合并到一个合并副本中。要在代码中构建和编辑注释对象本身而不是将它们作为文件进行交换,请参见 HotPDF 如何直接从 Delphi 中创建和编辑 PDF 注释对象

var
  Pdf: THotPDF;
  Total, I: Integer;
  Files: array[0..1] of string;
begin
  Files[0] := 'alice-comments.xfdf';
  Files[1] := 'bob-comments.xfdf';
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('master.pdf', '') > 0 then
    begin
      Total := 0;
      for I := Low(Files) to High(Files) do
        Inc(Total, Pdf.ImportLoadedAnnotationsFromXFDF(Files[I]));
      if Total > 0 then
        Pdf.SaveLoadedDocument('master-merged.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

什么可以干净地往返,而什么不能

HotPDF 对 ISO 19444-1 所支持的注释子类型进行往返传输,并特意跳过其余部分,而不是发出一些阅读器可能会误解的内容。支持的集合涵盖了主导实际评审工作的标记类型:文本注释、自由文本、线条、矩形、圆形、多边形、折线、四种文本标记类型(高亮、下划线、删除线和波浪线)、图章、墨水和插入符号,加上文件附件、声音、擦除和链接,总共 18 种子类型。其子类型落在该列表之外的注释在导出时会被跳过,且由于它是被跳过而不是写入空内容,因此它不会膨胀该函数返回的计数

富文本是客观存在的局限。HotPDF 保留了 <contents-richtext> 正文,以便格式化文本和纯内容都能在传输中幸存,但 XFDF 携带的是评论的文本和样式标记,而不是渲染的外观流,因此接收端应用程序会使用其自己的字体和布局重新绘制弹出窗口,而不是重现 HotPDF 的精确像素。请将往返视为对内容和意图的忠实,而不是对屏幕上精确到像素的渲染的忠实。如果您的样式化内容存在于 XFA 表单数据中而不是注释流中,规则会有所不同,且 HotPDF 如何处理 XFA exData、富文本和超链接 涵盖了该独立路径

字符级别的处理比看起来更严格,这正是您所希望的。HotPDF 在写入文本时应用 ISO 19444-1 第 5.8.2 条的转义规则,对具有 XML 意义的字符和控制字节进行编码,以便包含 &、尖括号或换行符的批注能够产生任何合规解析器都接受的格式良好的 XML,并在导入时反转相同的规则。这就是为什么从电子表格中粘贴带有标点符号的说明能够原封不动地返回,而不会损坏文件的原因

注释交换是已加载文档 API 所做工作的一个切片,并且它可以与其余部分组合:导入评审人员的 XFDF、调整页面或 编辑文档元数据、扁平化或重新授权文件,然后为下一轮导出全新的 XFDF。所有这些都包含在适用于 Delphi 和 C++Builder 的标准 HotPDF Component 中,其参考文档中记录了完整的注释子类型覆盖范围和伴随的表单数据 XFDF 函数