使用 PDFBox 为扁平化 PDF 表单嵌入字体

Posted

技术标签:

【中文标题】使用 PDFBox 为扁平化 PDF 表单嵌入字体【英文标题】:Embed fonts for flattend PDF form with PDFBox 【发布时间】:2018-12-29 09:29:53 【问题描述】:

我用 PDFBox 填写了一个 PDF 表单,我在保存之前将其展平。该表单具有用于文本和表单域的自定义字体。当我在未安装此自定义字体的设备上打开输出文档(带有展平字段)时,普通文本的字体仍然正确,但展平字段的字体显示为后备 (?) 字体。在安装了这种自定义字体的设备上,一切看起来都符合预期。

有没有办法在展平表单后强制对所有文本使用相同的自定义字体?

用PDFBox填写PDF表单的代码(简体):

public class App

    public static void main(String[] args) throws IOException 
        String formTemplate = "src/main/resources/fonts.pdf";
        String filledForm = "src/main/resources/fonts_out.pdf";
        PDDocument pdfDocument = PDDocument.load(new File(formTemplate));
        PDAcroForm acroForm = pdfDocument.getDocumentCatalog().getAcroForm();
        acroForm.getField("text").setValue("Same font in form text field (updated with PDFBox)");
        acroForm.setNeedAppearances(true);
        acroForm.refreshAppearances();
        acroForm.flatten();
        pdfDocument.save(filledForm);
        pdfDocument.close();
    

PDF: Input Output

预期:

系统未安装字体时的结果:

【问题讨论】:

您可能想展示一些关键代码,并且可能还分享 pdf 以允许重现问题。 @mkl:我在问题中添加了代码、PDF 和输出图像。 是什么字体? TrueType (TTF)? 1 型(原子力显微镜)? @Lonzak:这是一个 TTF。 你是如何添加表单域的?这也是 libreOffice 的一个功能吗? 【参考方案1】:

对您的 PDF 的一些观察(上述编码问题不存在 - 只是代表我的无知):

    SansDroid 字体未嵌入到 PDF 中。通过用新嵌入的F5 字体替换F2 字体来解决此问题。

    NeedAppearances 标志已设置,这意味着表单字段没有出现。任何读者都必须(重新)创建这些。 PDFBox 在展平之前不会自动完成此操作,因此我添加了这部分

    为了不再引起任何关于缺少字体的警告,我完全删除了 F2 字体。

    我通过预检运行原始 PDF,它给了我以下警告:“缺少所需的密钥 /Subtype。路径:->Pages->Kids->[0]->Annots- >[0]->AP->N " 键确实存在,但它似乎表明表单字段的外观存在错误。如果我删除 /N dict,错误就消失了。流是“/Tx BMC EMC”-也许缺少一些 EOL?但是由于无论如何都会重新生成外观,因此之后错误就消失了。

使用以下代码将 DroidSans 字体嵌入到 PDF 中:

File pdf = new File("Fonts.pdf");
final PDDocument document = PDDocument.load(pdf);

FileInputStream fontFile = new FileInputStream(new File("DroidSans.ttf"));
PDFont font = PDType0Font.load(document, fontFile, false);

//1. embedd and register the font (Catalog dict)
PDAcroForm pDAcroForm = document.getDocumentCatalog().getAcroForm();
//create a new font resource
PDResources res = pDAcroForm.getDefaultResources();
if (res == null) res = new PDResources();
COSName fontName = res.add(font);
pDAcroForm.setDefaultResources(res);

//2. Now change the font of form field to the newly added font
PDField field = pDAcroForm.getField("text");
//field.setValue("Same font in form text field (updated with PDFBox)");

COSDictionary dict = field.getCOSObject();
COSString defaultAppearance = (COSString) dict.getDictionaryObject(COSName.DA);

if (defaultAppearance != null)
    String currentValue = dict.getString(COSName.DA);
    //replace the font - this should be improved with a more general version
    dict.setString(COSName.DA,currentValue.replace("F2", fontName.getName()));

    //remove F2 completely
    COSDictionary resources = res.getCOSObject();
    for(Entry<COSName, COSBase> resource : resources.entrySet()) 
        if(resource.getKey().equals(COSName.FONT)) 
            COSObject fonts = (COSObject)resource.getValue();
            COSDictionary fontDict = (COSDictionary)fonts.getObject();

            COSName toBeRemoved=null;
            for(Entry<COSName, COSBase> item : fontDict.entrySet()) 
                if(item.getKey().getName().equals("F2")) 
                    toBeRemoved = item.getKey();
                
            
            if(toBeRemoved!=null) 
                fontDict.removeItem(toBeRemoved);
            
        
    

if(pDAcroForm.getNeedAppearances()) 
    pDAcroForm.refreshAppearances();
    pDAcroForm.setNeedAppearances(false);


//Flatten the document
pDAcroForm.flatten();

//Save the document
document.save("Form-Test-Result.pdf");
document.close();

请注意,上述代码是完全静态的 - 搜索和替换名为 F2 的字体仅适用于提供的 PDF,在其他情况下则无效。您必须为此实施更通用的解决方案...

【讨论】:

非常感谢您的调查!编码问题真的很奇怪——它是一个使用 LibreOffice PDF 导出创建的简单文档。无论如何,我已经尝试了您的代码,我认为它可以按预期工作。我用于测试pdfpro.co/pdf-viewer(仅在正确嵌入字体时才会产生预期结果)。 “F2”字体名称我很有趣——我只是想知道为什么它在安装了 Droid Sans 字体的系统上会这样工作...... “FEFF 应该是 UTF-16 BOM。在我看来,这样做很奇怪” - 请解释为什么它很奇怪。手头的内容可能没有必要,但本身并不错误。 因为在我的理解中,BOM 必须被引用:“以与文档的其余部分相同的方案编码”(或者在我们的例子中与字符串的其余部分一样)。 BOM 是一个 unicode 字符 U+FEFFBYTE 序列,仅允许用作 BOM。正如您在表格中看到的(cp. above wikilink),FEFF 是十六进制表示(254 255 十进制或 þÿ 在 1252 中)。字符串的内容是完全不同的字符。 HEX= 46 45 46 46(相同的十进制和 1252 中的 FEFF)。这是 ascii,因此不是有效的 BOM。 "请解释为什么它很奇怪。手头的内容可能没有必要,但本身并不错误。" 相反,它对解释很重要内容!但是应该使用有效的 BOM 而不是 ANSI 编码的字符。我的意思是如果我将"FEFFERNITZ" 写入文本字段,您如何区分 - 这是否意味着我要开始一个 UTF-16 字符串,其余的应该解释为 UTF-16?不... @uwolfer "我很感兴趣的“F2”字体名称 - 我只是想知道为什么它在安装了 Droid Sans 字体的系统上会这样工作......" F2 font 是一个有效的字体定义,但基本上只引用系统字体(cp。原始 PDF 中的对象 12 0 和 13 0)。如果字体存在,一切都很好,但如果不存在,则引用无效,例如*dobe Reader 在编辑字段时触发错误(Font DroidSans 缺失...)【参考方案2】:

PDFont font = PDType0Font.load(document, fontFile, false);

我认为最后一个参数('false')将所有字符嵌入字体中。 当使用像日文字体这样的大字体时,这会产生一个大尺寸的 pdf。 所以,我尝试了以下代码,它对我有用。

(* Scala, PDFBox 2.0.20)

// val font = PDType0Font.load(document, fontFile, true);
// form.flatten()

// hack for embed minimul font?
val page = new PDPage(PDRectangle.A6) // any page size.
val stream = new PDPageContentStream(document, page)
stream.beginText()
stream.setFont(font, 0)
stream.showText(allChars) // `allChars` are inputed all characters in forms in the creating pdf.
stream.endText()
stream.close()
// NOTE: I did NOT add the page to the PDF but worked.  

// Save the document ~

【讨论】:

最初的问题是关于 acroform,最好是子集。您的代码是用于普通文本的,可以,但仅限于此。

以上是关于使用 PDFBox 为扁平化 PDF 表单嵌入字体的主要内容,如果未能解决你的问题,请参考以下文章

使用 pdfBox 禁用 pdf 文本搜索

如果在 PDF 表单中多次出现,Java PDFBox 不会保持字段的字体外观

pdfbox或icepdf转换PDF为图片时,中文乱码处理

使用 PDFBOX 填写 PDF 表单中的多个字段并在填写后锁定编辑 pdf 文档

基于pdfbox实现的pdf添加文字水印工具

Itext 在 PDF 中嵌入字体