Apache FOP - is there a way to embed font programmatically?
Asked Answered
T

5

5

When creating a PDF using Apache FOP it is possible to embed a font with configuration file. The problem emerges when the app is a web application and it is necessary to embed a font that is inside WAR file (so treated as resource).

It is not acceptable to use particular container's folder structure to determine where exactly the war is located (when in configuration xml file we set tag to ./, it is set to the base folder of running container like C:\Tomcat\bin).

So the question is: Do anyone know the way to embed a font programatically?

Thrum answered 25/2, 2019 at 8:0 Comment(0)
T
9

After going through lots of FOP java code I managed to get it to work.

Descriptive version

Main idea is to force FOP to use custom PDFRendererConfigurator that will return desired font list when getCustomFontCollection() is executed.

In order to do it we need to create custom PDFDocumentHandlerMaker that will return custom PDFDocumentHandler (form method makeIFDocumentHandler()) which will in turn return our custom PDFRendererConfigurator (from getConfigurator() method) that, as above, will set out custom font list.

Then just add custom PDFDocumentHandlerMaker to RendererFactory and it will work.

FopFactory > RendererFactory > PDFDocumentHandlerMaker > PDFDocumentHandler > PDFRendererConfigurator

Full code

FopTest.java

public class FopTest {

    public static void main(String[] args) throws Exception {

        // the XSL FO file
        StreamSource xsltFile = new StreamSource(
                Thread.currentThread().getContextClassLoader().getResourceAsStream("template.xsl"));
        // the XML file which provides the input
        StreamSource xmlSource = new StreamSource(
                Thread.currentThread().getContextClassLoader().getResourceAsStream("employees.xml"));
        // create an instance of fop factory
        FopFactory fopFactory = new FopFactoryBuilder(new File(".").toURI()).build();

        RendererFactory rendererFactory = fopFactory.getRendererFactory();
        rendererFactory.addDocumentHandlerMaker(new CustomPDFDocumentHandlerMaker());

        // a user agent is needed for transformation
        FOUserAgent foUserAgent = fopFactory.newFOUserAgent();

        // Setup output
        OutputStream out;
        out = new java.io.FileOutputStream("employee.pdf");

        try {
            // Construct fop with desired output format
            Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out);

            // Setup XSLT
            TransformerFactory factory = TransformerFactory.newInstance();
            Transformer transformer = factory.newTransformer(xsltFile);

            // Resulting SAX events (the generated FO) must be piped through to
            // FOP
            Result res = new SAXResult(fop.getDefaultHandler());

            // Start XSLT transformation and FOP processing
            // That's where the XML is first transformed to XSL-FO and then
            // PDF is created
            transformer.transform(xmlSource, res);
        } finally {
            out.close();
        }

    }

}

CustomPDFDocumentHandlerMaker.java

public class CustomPDFDocumentHandlerMaker extends PDFDocumentHandlerMaker {

    @Override
    public IFDocumentHandler makeIFDocumentHandler(IFContext ifContext) {
        CustomPDFDocumentHandler handler = new CustomPDFDocumentHandler(ifContext);
        FOUserAgent ua = ifContext.getUserAgent();
        if (ua.isAccessibilityEnabled()) {
            ua.setStructureTreeEventHandler(handler.getStructureTreeEventHandler());
        }
        return handler;
    }

}

CustomPDFDocumentHandler.java

public class CustomPDFDocumentHandler extends PDFDocumentHandler {

    public CustomPDFDocumentHandler(IFContext context) {
        super(context);
    }

    @Override
    public IFDocumentHandlerConfigurator getConfigurator() {
        return new CustomPDFRendererConfigurator(getUserAgent(), new PDFRendererConfigParser());
    }

}

CustomPDFRendererConfigurator.java

public class CustomPDFRendererConfigurator extends PDFRendererConfigurator {

    public CustomPDFRendererConfigurator(FOUserAgent userAgent, RendererConfigParser rendererConfigParser) {
        super(userAgent, rendererConfigParser);
    }

    @Override
    protected FontCollection getCustomFontCollection(InternalResourceResolver resolver, String mimeType)
            throws FOPException {

        List<EmbedFontInfo> fontList = new ArrayList<EmbedFontInfo>();
        try {
            FontUris fontUris = new FontUris(Thread.currentThread().getContextClassLoader().getResource("UbuntuMono-Bold.ttf").toURI(), null);
            List<FontTriplet> triplets = new ArrayList<FontTriplet>();
            triplets.add(new FontTriplet("UbuntuMono", Font.STYLE_NORMAL, Font.WEIGHT_NORMAL));
            EmbedFontInfo fontInfo = new EmbedFontInfo(fontUris, false, false, triplets, null, EncodingMode.AUTO, EmbeddingMode.AUTO);
            fontList.add(fontInfo);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return createCollectionFromFontList(resolver, fontList);
    }

}
Thrum answered 26/2, 2019 at 15:23 Comment(4)
The most inportant is not to forget to add a new font familty in <fo:root tag. font-family="UbuntuMono" in the given case. And it will work perfectly.Vaccination
Hi! Thanks for the solution. What is the content of "template.xsl" ?Emboly
Is there any way to use this with PDFTranscoder?Emboly
Finally a working solution. Maybe a bit complicated, but it works. Thank you !Holbert
T
0

Yes you can do this. You need to set FOP's first base directory programmatically.

    fopFactory = FopFactory.newInstance();
    // for image base URL : images from Resource path of project
    String serverPath = request.getSession().getServletContext().getRealPath("/");
    fopFactory.setBaseURL(serverPath);
    // for fonts base URL :  .ttf from Resource path of project
    fopFactory.getFontManager().setFontBaseURL(serverPath);

Then use FOB font config file.It will use above base path.

Just put your font files in web applications resource folder and refer that path in FOP's font config file.

After Comment : Reading font config programmatically (not preferred & clean way still as requested)

    //This is NON tested and PSEUDO code to get understanding of logic
    FontUris fontUris = new FontUris(new URI("<font.ttf relative path>"), null);
    EmbedFontInfo fontInfo = new EmbedFontInfo(fontUris, "is kerning enabled boolean", "is aldvaned enabled boolean", null, "subFontName");
    List<EmbedFontInfo> fontInfoList = new ArrayList<>();
    fontInfoList.add(fontInfo);
    //set base URL for Font Manager to use relative path of ttf file.
    fopFactory.getFontManager().updateReferencedFonts(fontInfoList);

You can get more info for FOP's relative path https://xmlgraphics.apache.org/fop/2.2/configuration.html

Tumefaction answered 25/2, 2019 at 8:47 Comment(4)
Well, is there any way NOT to use config file and do it fully programmatically?Thrum
You need to add font.ttf file in resource. To skip <font> xml , use need to overide fop's FontManager factoryTumefaction
Could you elaborate on that?Thrum
Pseudo logic added above to get understanding of what to doTumefaction
E
0

The following approach may be useful for those who use PDFTranscoder.

Put the following xml template in the resources:

<?xml version="1.0" encoding="UTF-8"?>
<fop version="1.0">
   <fonts>
       <font kerning="no" embed-url="IBM_PLEX_MONO_PATH" embedding-mode="subset">
               <font-triplet name="IBM Plex Mono" style="normal" weight="normal"/>
      </font>
   </fonts>
</fop>

Then one can load this xml and replace the line with font (IBM_PLEX_MONO_PATH) with the actual URI of the font from the resource bundle at runtime:

private val fopConfig = DefaultConfigurationBuilder()
    .buildFromFile(javaClass.getResourceAsStream("/fonts/fopconf.xml")?.use {
        val xml = BufferedReader(InputStreamReader(it)).use { bf ->
            bf.readLines()
                .joinToString("")
                .replace(
                    "IBM_PLEX_MONO_PATH",
                    javaClass.getResource("/fonts/IBM_Plex_Mono/IBMPlexMono-Text.ttf")!!.toURI().toString()
                )
        }
        val file = Files.createTempFile("fopconf", "xml")
        file.writeText(xml)
        file.toFile()
    })

Now one can use this config with PDFTranscoder and your custom fonts will be probably rendered and embedded in PDF:

 val pdfTranscoder = if (type == PDF) PDFTranscoder() else EPSTranscoder()
    ContainerUtil.configure(pdfTranscoder, fopConfig)
    val input = TranscoderInput(ByteArrayInputStream(svg.toByteArray()))
    ByteArrayOutputStream().use { byteArrayOutputStream ->
        val output = TranscoderOutput(byteArrayOutputStream)
        pdfTranscoder.transcode(input, output)
        byteArrayOutputStream.toByteArray()
    }
Emboly answered 29/6, 2022 at 20:45 Comment(0)
M
0

通过查看源代码,可以试用如下方法。

InternalResourceResolver resourceResolver = fopFactory.getFontManager().getResourceResolver();
fopFactory.getFontManager().setFontSubstitutions(new FontSubstitutions() {
    private static final long serialVersionUID = 1L;

    @Override
    public void adjustFontInfo(FontInfo fontInfo) {
        super.adjustFontInfo(fontInfo);

        int num = fontInfo.getFonts().size() + 1;
        String internalName = null;
        for (EmbedFontInfo embedFontInfo : customFont()) {
            internalName = "F" + num;
            num++;

            LazyFont font = new LazyFont(embedFontInfo, resourceResolver, false);
            fontInfo.addMetrics(internalName, font);

            List<FontTriplet> triplets = embedFontInfo.getFontTriplets();
            for (FontTriplet triplet : triplets) {
                fontInfo.addFontProperties(internalName, triplet);
            }
        }
    }
});

private List<EmbedFontInfo> customFont() {
    List<EmbedFontInfo> fontList = new ArrayList<EmbedFontInfo>();
    
    URI fontUri1 = Paths.get("fonts/msyh.ttc").toUri();
    FontUris fontUris1 = new FontUris(fontUri1, null);
    List<FontTriplet> triplets1 = new ArrayList<FontTriplet>();
    triplets1.add(new FontTriplet("Microsoft YaHei UI", Font.STYLE_NORMAL, Font.WEIGHT_NORMAL));
    triplets1.add(new FontTriplet("Microsoft YaHei UI", Font.STYLE_NORMAL, Font.WEIGHT_BOLD));
    EmbedFontInfo fontInfo1 = new EmbedFontInfo(fontUris1, false, false, triplets1, "Microsoft YaHei UI");
    fontList.add(fontInfo1);

    URI fontUri2 = Paths.get("fonts/msyhbd.ttc").toUri();
    FontUris fontUris2 = new FontUris(fontUri2, null);
    List<FontTriplet> triplets2 = new ArrayList<FontTriplet>();
    triplets2.add(new FontTriplet("Microsoft YaHei UI Bold", Font.STYLE_NORMAL, Font.WEIGHT_NORMAL));
    triplets2.add(new FontTriplet("Microsoft YaHei UI Bold", Font.STYLE_NORMAL, Font.WEIGHT_BOLD));
    EmbedFontInfo fontInfo2 = new EmbedFontInfo(fontUris2, false, false, triplets2, "Microsoft YaHei UI Bold");
    fontList.add(fontInfo2);
    
    return fontList;
}
Montherlant answered 28/5, 2023 at 16:20 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Oys
I
0

Custom fonts may added using JAR file entries in the META-INF/MANIFEST.MF file, see https://xmlgraphics.apache.org/fop/2.9/fonts#autodetect.

That requires font auto-detection to be turned on. However, auto-detect will detect system fonts too. That has a performance hit, but depending on your usage that might not be an issue. Given that your app is running in a Tomcat container you should be fine if you reuse your FopFactory. I haven't looked at the difference in memory usage, though.

If you want to optimise performance or memory, it looks possible to tweak environment variables so that FontFileFinder would quickly fail to find any system fonts. A better solution would be to create a custom FontDetector that only loads fonts defined in manifest files (based on FontDetectorFactory$DefaultFontDetector), or fetches fonts from other resources.

To use a custom FontDetector, create a FontManager with it, create an EnvironmentProfile with that and use that with your FopFactoryBuilder.

Manifest-Version: 1.0

Name: fonts/OpenSans-Regular.ttf
Content-Type: application/x-font-truetype
<?xml version="1.0"?>
<fop version="1.0">
  <renderers>
    <renderer mime="application/pdf">
      <fonts>
        <auto-detect/>
      </fonts>
    </renderer>
  </renderers>
</fop>

The following excerpts are optional to improve performance and (maybe) memory consumption by not looking for system fonts.

/**
 * <p>
 * A stripped down version of {@code FontDetectorFactory$DefaultFontDetector}
 * that detects only fonts defined in manifest files.
 * </p>
 * <p>
 * To use this, {@code auto-detect} must be turned on in the configuration, and
 * an {@code EnviromentProfile} containing this detector must be passed to the
 * {@code FopFactoryBuilder}.
 * </p>
 */
public class ManifestFontDetector implements FontDetector {

    private static Log log = LogFactory.getLog(ManifestFontDetector.class);

    /**
     * {@code application/x-font} and {@code application/x-font-truetype} are
     * copied from FontDetectorFactory$DefaultFontDetector, but are not standard
     * mime types. RFC 8081 added {@code font/*} media types.
     *
     * @see https://www.rfc-editor.org/rfc/rfc8081
     * @see https://www.iana.org/assignments/media-types/media-types.xhtml#font
     */
    private static final String[] FONT_MIMETYPES = {
            "application/x-font", "application/x-font-truetype"
    };

    @Override
    public void detect(FontManager fontManager, FontAdder fontAdder, boolean strict, FontEventListener eventListener, List<EmbedFontInfo> fontInfoList) throws FOPException {
        try {
            ClasspathResource resource = ClasspathResource.getInstance();
            for (String mimeTypes : FONT_MIMETYPES) {
                fontAdder.add(resource.listResourcesOfMimeType(mimeTypes), fontInfoList);
            }
        }
        catch (URISyntaxException use) {
            LogUtil.handleException(log, use, strict);
        }
    }

}
EnvironmentProfile environmentProfile = new EnvironmentProfile() {
    private final ResourceResolver resourceResolver = ResourceResolverFactory.createDefaultResourceResolver();
    private final FallbackResolver fallbackResolver = new UnrestrictedFallbackResolver();
    private final URI defaultBaseUri = URI.create("file:/temp");
    private final FontManager fontManager = new FontManager(
        ResourceResolverFactory.createInternalResourceResolver(defaultBaseUri, resourceResolver),
        new ManifestFontDetector(),
        FontCacheManagerFactory.createDefault());

    @Override
    public ResourceResolver getResourceResolver() {
        return resourceResolver;
    }

    @Override
    public FontManager getFontManager() {
        return fontManager;
    }

    @Override
    public FallbackResolver getFallbackResolver() {
        return fallbackResolver;
    }

    @Override
    public URI getDefaultBaseURI() {
        return defaultBaseUri;
    }
};

DefaultConfiguration configuration;
try (InputStream confStream = getClass().getResourceAsStream("/fop.xconf")) {
    configuration = new DefaultConfigurationBuilder().build(confStream);
}

FopFactory fopFactory = new FopFactoryBuilder(environmentProfile)
        .setConfiguration(configuration)
        .build();
Incudes answered 5/6 at 16:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.