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:
- Make sure your font binary is in the classpath
- Based on my code example above, also make sure the font binary is copied to a temp folder on start-up of the application
- 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.
- 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")
}
}