pdfbox 嵌入用于注释的子集字体

pdfbox embedding subset font for annotations

我正在尝试使用 Apache PDFBOX v2.0.21 修改现有的 PDF 文档,添加签名和注释。这意味着我正在积极使用增量保存模式。我还嵌入了 LiberationSans 字体以容纳一些 Unicode 字符。对我来说,使用 PDF 嵌入字体的子集化功能是有意义的,因为完全嵌入 LiberationSans 会使 PDF 文件的大小增加 200+ KB。

经过多次试验和错误后,我终于设法让一些东西工作了——除了字体子集。我这样做的方法是使用

初始化 PDFont 对象一次
  try (InputStream fs = PDFService.class.getResourceAsStream("/static/fonts/LiberationSans-Regular.ttf")) {
     _font = PDType0Font.load(pddoc, fs, true);
  }

然后使用自定义外观流来显示文本。

   private void addAnnotation(String name, PDDocument doc, PDPage page, float x, float y, String text) throws IOException {
      
      List<PDAnnotation> annotations = page.getAnnotations();

      PDAnnotationRubberStamp t = new PDAnnotationRubberStamp();

      t.setAnnotationName(name); // might play important role
      t.setPrinted(true); // always visible
      t.setReadOnly(true); // does not interact with user
      t.setContents(text); 
      
      PDRectangle rect = ....;
      t.setRectangle(rect);

      PDAppearanceDictionary ap = new PDAppearanceDictionary();
      ap.setNormalAppearance(createAppearanceStream(doc, t));
      ap.getCOSObject().setNeedToBeUpdated(true);
      t.setAppearance(ap);
      
      annotations.add(t);
      page.setAnnotations(annotations);
      
      t.getCOSObject().setNeedToBeUpdated(true);
      page.getResources().getCOSObject().setNeedToBeUpdated(true);
      page.getCOSObject().setNeedToBeUpdated(true);
      doc.getDocumentCatalog().getPages().getCOSObject().setNeedToBeUpdated(true);
      doc.getDocumentCatalog().getCOSObject().setNeedToBeUpdated(true);      
   }
   
   private PDAppearanceStream createAppearanceStream(final PDDocument document, PDAnnotation ann) throws IOException
   {
      PDAppearanceStream aps = new PDAppearanceStream(document);
      PDRectangle rect = ann.getRectangle();
      rect = new PDRectangle(0, 0, rect.getWidth(), rect.getHeight());
      aps.setBBox(rect); // set bounding box to the dimensions of the annotation itself
     
      // embed our unicode font (NB: yes, this needs to be done otherwise aps.getResources() == null which will cause NPE later during setFont)
      PDResources res = new PDResources();
      _fontName = res.add(_font).getName();
      aps.setResources(res);

      PDAppearanceContentStream apsContent = null;
      
      try {
         // draw directly on the XObject's content stream
         apsContent = new PDAppearanceContentStream(aps);

         apsContent.beginText();
         apsContent.setFont(_font, _fontSize);         
         apsContent.showText(ann.getContents());
         apsContent.endText();
      }
      finally {
         if (apsContent != null) {
            try { apsContent.close(); } catch (Exception ex) { log.error(ex.getMessage(), ex); }
         }
      }      

      aps.getResources().getCOSObject().setNeedToBeUpdated(true);
      aps.getCOSObject().setNeedToBeUpdated(true);
      return aps;
   }     

此代码运行,但创建了一个带有点而不是实际字符的 PDF,我猜这意味着字体子集尚未嵌入。此外,我收到以下警告:

2021-04-17 12:33:31.326 WARN 20820 --- [ main] o.a.p.pdmodel.PDAbstractContentStream : attempting to use subset font LiberationSans without proper context

查看源代码后,我明白了,我想我在创建外观流时搞砸了一些东西——不知何故它没有与 PDDocument 连接,子集化也没有正常继续。请注意,当字体完全嵌入时,上面的代码运行良好(即如果我调用 PDType0Font.load 并将最后一个参数设置为 false)

有谁能给我一些提示吗?谢谢!

我不知道 - 我幸运吗?编程中的运气常常指向完全错误或误导的东西。无论如何,如果有人还能给点提示,我的耳朵可不止是开着...

再次查看代码后,我在PDDocument.save()中看到了以下内容:

// subset designated fonts
for (PDFont font : fontsToSubset)
{
    font.subset();
}

这在我使用的 PDDocument.saveIncremental() 中没有发生。只是为了弄乱代码,在我的文档上调用 saveIncremental() 之前,我做了以下操作:

 _font.subset(); // you can see in the beginning of the question how _font is created
 _font.getCOSObject().setNeedToBeUpdated(true);
 pddoc.saveIncremental(baos);

信不信由你,但文档已正确保存 - 至少它在 Acrobat Reader DC 和 Chrome & Firefox PDF 查看器中显示正确。请注意,在外观内容流的 showText() 期间,Unicode 代码点被添加到字体的子集中。

2021 年 4 月 18 日更新:正如我在评论中提到的,我收到用户的报告,他们开始看到诸如“无法提取嵌入式字体 XXXXXX+LiberationSans-Regular”之类的消息来自...”,当他们打开修改后的 PDF 文件时。奇怪的是,我在测试期间没有看到这些消息。事实证明,我的 Acrobat Reader DC 副本比他们的更新,特别是连续发布版本 2021.001.20149 没有显示错误,而连续发布版本 2020.012.20043 显示了上述消息。

经过调查,问题出在我嵌入字体的方式上。我不知道是否存在任何其他方式,而且我对 PDF 规范也不那么熟悉,无法知道其他方式。从上面的代码可以看出,我所做的是为文档加载一次字体,然后在 EVERY 注释的外观流的资源字典中自由使用它。结果,注释内容流的所有资源字典都引用了使用相同 /BaseFont 名称定义的 F1 字体。 PDF 参考,第 3 版。在 p.323 上特别指出:

"... the PostScript name of the font - ... - begins with a tag followed by a plus sign (+). The tag consists of exactly six uppercase letters; the choice of letters is arbitrary, but different subsets in the same PDF file must have different tags..."

一旦我开始为每个注释调用PDType0Font.load并在为每个注释创建外观流后调用 subset()(当然还有 setNeedToBeUpdated),我看到 BaseName 属性开始看起来确实不同了 - 事实上,较早的 2020 版 Acrobat Reader DC 停止了抱怨。

[编辑 07/10/2021:即使尝试每页使用一个 PDFont 对象(使用此字体有多个注释),并在对所有注释的外观调用 showText 后对其进行子集化一次,似乎也没有工作 - 子集似乎使用了我传递给第一个 showText 的字母,而不是其他字母,导致第二个、第三个等注释的错误呈现,这些注释可能包含第一个注释中不存在的字符 - 所以我重申有效的方法是对 每个单独的注释 使用 loadFont,然后(在使用 showText 修改外观后,这将标记子集化期间要使用的字母)在每个注释上调用 subset()这些字体(会导致字体名称的改变)]

请注意,除了使用 iText RUPS 检查 PDF 内容外,还可以使用 Foxit PDF 查看器至少确保子集字体名称不同。 Acrobat Reader DC 和 PDF-xChange 在属性 -> 字体中只显示初始字体名称,如 LiberationSans,不显示 6 个字母的唯一前缀。

更新 19/04/2021 我仍在处理这个问题 - 因为我仍然收到关于臭名昭著的“无法提取嵌入字体”消息的报告。很可能该消息的原始原因不是(或不仅仅是)不同的子集具有相同的 BaseFont 名称这一事实。我观察到的一件事是,在某些计算机上,我正在使用的图章注释会导致 Acrobat Reader DC 自动打开所谓的“注释窗格” - 有一些选项可以关闭这个自动功能(首选项 - > 评论 -> 打开带有评论的 PDF 时显示评论窗格)。当此窗格手动或自动打开时,会出现错误消息(我想知道为什么相同版本的 Acrobat Reader DC 在不同机器上表现不同)。我认为 Acrobat Reader 试图提取字体的完整版本但失败了,因为它只是一个子集。但是,我想,这与文档的语义内容无关——文档仍然通过“qpdf --check”。我目前正在尝试寻找是否可以限制邮票不允许评论 - 即某种方法可以禁用 Acrobat Reader DC 中的评论窗格,尽管我希望不大。

更新 20/04/2021 打开了一个新问题 here