Search code examples
javapdfpdfboxpdf-form

Embed fonts for flattend PDF form with PDFBox


I fill in a PDF form with PDFBox which I flatten before saving it. The form has a custom font for text and also form fields. When I open the output document (with flattened fields) on a device which does not have this custom font installed, the font of normal text is still correct, but the font of the flattened fields is displayed with a fallback (?) font. On a device which does have installed this custom font, everything looks as expected.

Is there a way to force using the same custom font for all text after flattening the form?

Code (simplified) used for filling in the PDF form with PDFBox:

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();
    }
}

PDFs: Input Output

Expected:

Expected

Result when font is not installed on system:

Result when font is not installed on system


Solution

  • Some observations for your PDF (the afore mentioned encoding problems are none - just plain ignorance on my behalf):

    1. The SansDroid font is not embedded into the PDF. This is fixed by replacing the F2 font with the newly embedded F5 font.

    2. The NeedAppearances flag is set meaning that there is no appearance for the form fields. Any reader must (re)create those. This is not done automatically by PDFBox before flattening so I added this part

    3. To not cause any more warnings about missing fonts I removed the F2 font completely.

    4. I run the original PDF through preflight and it gave me the following warning: "The required key /Subtype is missing. Path: ->Pages->Kids->[0]->Annots->[0]->AP->N " The key does exists however it seems to inicate that there is an error with the appearance of the form field. If I remove the /N dict the error is gone. The stream is "/Tx BMC EMC" - maybe there is some EOL missing? But since the appearance is regenerated anyway the error is gone afterwards.

    With the following code the DroidSans font is embedded into the 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();
    

    Please note that the above code is quite static - Searching and replacing a font called F2 only works for the supplied PDF in other cases it won't. You have to implement a more generic solution for that...