Spring Boot in Docker build by buildpack cannot load Font
Asked Answered
C

2

7

My Spring Boot application runs in Docker and is build by gradlew bootBuildImage. When run in Docker container application cannot load fonts

Caused by: java.lang.NullPointerException
    at java.desktop/sun.awt.FontConfiguration.getVersion(Unknown Source)

Root cause seems to be missing fontconfig and ttf-dejavu packages. When using Dockerfile, one can easily install those packages using apk add, yum, apt-get, etc

But https://github.com/paketo-buildpacks/spring-boot and https://github.com/paketo-buildpacks/bellsoft-liberica do not have option to install additional packages.

Is there buildpack (or configuration option) that will build Docker images with font support?

Cary answered 2/11, 2021 at 16:46 Comment(1)
Check out this SO question. It's very similar and I think it'll answer your question. Is it possible to customize docker image generated with Spring Native (with buildpack)Hammock
P
3

You can manipulate the image after the fact. A sample Dockerfile would look like this:

FROM backend:latest

USER root # root for apt
RUN apt-get update && \
    apt-get install --assume-yes fontconfig && \
    rm -rf /var/lib/apt/lists/* /var/cache/debconf/*

USER 1000:1000 # back to cnb user
Pothead answered 7/4, 2022 at 13:39 Comment(0)
P
2

After debugging what exactly happens on JDK level, I've found a solution for this that doesn't require to manipulate the image.

It seems that the Sun developers back then were so kind to add a possibility to customize the whole loading of font configuration that is platform independent. It consists in creating a font configuration file that determines where the font binaries can be found for each font family and style (e.g. bold, italic, etc.). You can find more information about this here: Font Configuration Files. It's basically the same thing fontconfig does but without the need to have it installed on the OS. In order to make the JDK use the custom configuration, you need to set the system property sun.awt.fontconfig to the path of your configuration file.

I don't know what you're loading fonts for, but in my case I wanted the application to create reports with Jasper. I don't really need any fonts to be previously installed, as I'm already shipping the font binaries using Jasper's Font Extensions, but this fontconfig problem occurs whenever you want to load a font, even if you have shipped the font binaries.

My solution therefore, was to create an almost empty font configuration file like this:

version=1
sequence.allfonts=

Then you just have to make sure, that this file is copied to the container and the system property sun.awt.fontconfig is set pointing to it when the JVM is started.

I didn't want though that DevOps have to deal with this, so I ended up writing a little component in the application that automatically creates such an empty file in the temp folder if the property is not set.

Here is the code (in Kotlin, but should be straight forward to translate to Java):

@Component
@ConditionalOnProperty(Reporting.FONTCONFIG_WORKAROUND_ENABLED)
class EmptyFontconfigConfiguration {
    companion object {
        private const val FILE_NAME = "empty.fontconfig.properties.src"
        private const val FONTCONFIG_PROPERTY_NAME = "sun.awt.fontconfig"
    }

    @EventListener(ApplicationReadyEvent::class)
    fun applyWorkaround() {
        val logger: Logger = LoggerFactory.getLogger(EmptyFontconfigConfiguration::class.java)
        logger.atInfo().log { "fontconfig workaround is enabled, checking $FONTCONFIG_PROPERTY_NAME property" }
        val emptyConfigFile = File(System.getProperty("java.io.tmpdir"), FILE_NAME)
        val propertyValue = System.getProperty(FONTCONFIG_PROPERTY_NAME).emptyToNull()
        if (propertyValue == null || !File(propertyValue).exists()) {
            logger.atInfo().log { "Property $FONTCONFIG_PROPERTY_NAME is not set, setting to ${emptyConfigFile.path}" }
            System.setProperty(FONTCONFIG_PROPERTY_NAME, emptyConfigFile.path)
            if (!emptyConfigFile.exists()) {
                logger.atInfo().log { "Creating file ${emptyConfigFile.path}" }
                Files.write(
                    emptyConfigFile.toPath(), listOf(
                        "version=1",
                        "sequence.allfonts="
                    )
                )
            }
        }
    }
}

As you can see, I'm also using my own Spring property (@ConditionalOnProperty(Reporting.FONTCONFIG_WORKAROUND_ENABLED)) to disable this component completely (and with it the whole workaround) in case it causes issues.

UPDATE:

If you use Apache POI as well to generate Excel files, then using this almost empty font configuration is not enough, not even if you set the org.apache.poi.ss.ignoreMissingFontSystem system property (see this answer). This is because POI loads the font a bit differently and doesn't catch the exception that is thrown with this almost empty configuration.

So, the ultimate bullet proof solution is to create a proper valid font configuration and map all AWT logical fonts to a single font binary (or to multiple ones, if you prefer). I chose the open source font Open Sans and did the following:

  1. Make sure your font binary is in the classpath
  2. Based on my code example above, also make sure the font binary is copied to a temp folder on start-up of the application
  3. Generate a fontconfig properties file with all logical font names, styles and character subsets pointing to the one font binary that was copied to the temp folder (use absolute paths).The code below shows how I did it.
  4. Lean back and relax because you don't have to care about this issue anymore no matter in what container you're running your application!😊

Here is the code to generate the file:

    companion object {
        private const val FILE_NAME = "empty.fontconfig.properties.src"
        private const val FONTCONFIG_PROPERTY_NAME = "sun.awt.fontconfig"
        private const val FONT_NAME = "OpenSans"
    }
    ...
    private fun writeFontconfigFile(configFile: File, fontFile: File) {
        val logicalFontNames = listOf("dialog", "sansserif", "serif", "monospaced", "dialoginput")
        val styleNames = listOf("plain", "bold", "italic", "bolditalic")
        val characterSubsetNames = listOf("latin-1", "japanese-x0208", "korean", "chinese-big5", "chinese-gb18030")
        val fontFilePath = fontFile.canonicalPath
        configFile.printWriter().use { w ->
            w.println("# This file maps all fonts to $FONT_NAME using the file on $fontFilePath")
            w.println("# It's been generated based on the template from:")
            w.println("# https://github.com/srisatish/openjdk/blob/master/jdk/src/solaris/classes/sun/awt/fontconfigs/linux.fontconfig.properties")
            w.println()
            w.println()
            w.println("# Version")
            w.println("version=1")
            w.println()
            w.println("# Component Font Mappings")
            logicalFontNames.forEach { logicalFontName ->
                styleNames.forEach { styleName ->
                    characterSubsetNames.forEach { characterSubsetName ->
                        w.println("${logicalFontName}.${styleName}.${characterSubsetName}=$FONT_NAME")
                    }
                    w.println()
                }
            }
            w.println()
            w.println("# Search Sequences")
            w.println("sequence.allfonts=latin-1")
            w.println("sequence.allfonts.Big5=chinese-big5,latin-1")
            w.println("sequence.allfonts.x-euc-jp-linux=japanese-x0208,latin-1")
            w.println("sequence.allfonts.EUC-KR=korean,latin-1")
            w.println("sequence.allfonts.GB18030=chinese-gb18030,latin-1")
            w.println("sequence.fallback=chinese-big5,chinese-gb18030,japanese-x0208,korean")
            w.println()
            w.println("# Font File Names")
            w.println("filename.$FONT_NAME=$fontFilePath")
        }
    }
Proudfoot answered 5/4 at 9:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.