Reputation: 13235
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?
Upvotes: 7
Views: 3240
Reputation: 1298
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:
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")
}
}
Upvotes: 2
Reputation: 19968
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
Upvotes: 3