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
我正在尝试使用 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