Search code examples
javaspring-bootkotlinfreemarkeropenhtmltopdf

How to use custom font using openHtmlToPdf?


I am developing a microservice in springboot to generate pdf by using freeMarker and openHtmlToPdf libraries. I want to introduce custom font(tamil language). But in only getting #### as output. Not sure where I am going wrong.

Method converting html to pdf

private fun convertToPdf(htmlContent: String): ByteArrayResource {
 val jsoupDocument = Jsoup.parse(htmlContent)
 jsoupDocument.outputSettings().syntax(Document.OutputSettings.Syntax.html)
 val xmlDocument = W3CDom().fromJsoup(jsoupDocument)

 val FONT_FILE = File("resources/fonts/NotoSansTamil.ttf")

 val byteArrayOutputStream = ByteArrayOutputStream()
 val baseUrl = javaClass
 .protectionDomain
 .codeSource
 .location
 .toString()

 PdfRendererBuilder()
 .withW3cDocument(xmlDocument, baseUrl)
 .useFont(FONT_FILE, "Nota Sans")
 .toStream(byteArrayOutputStream)
 .run()
 return ByteArrayResource(byteArrayOutputStream.toByteArray())
} 

Freemarker template


<html>
<head>
<#--    <meta charset="UTF-16">-->
    <title>FreeMarker</title>
    <style>
        @font-face {
            font-family: 'Open Sans';
            font-style: normal;
            font-weight: 400;
            src: url(./fonts/NotoSansTamil.ttf);
        }
    </style>
</head>
<body>
    <h1> Welcome to FreeMarker ${name} </h1>
</body>
</html>

Solution

  • There are multiple different problems that code.

    font-family mismatch

    You defined font as

    .useFont(FONT_FILE, "Nota Sans") //font-family will be 'Nota Sans'
    

    but in the hmtl frament using

    <style>
        @font-face {
            font-family: 'Open Sans';
            font-style: normal;
            font-weight: 400;
            src: url(./fonts/NotoSansTamil.ttf);
        }
    </style>
    

    Just for the record this font called Noto instead of Nota.

    Solution: Always use the same font family name in html fragment as defined in Kotlin code.

    Wrong File path

    As you defined the font is totally wrong. I assume your project layout is like Maven / Gradle suggest. In this case the resources folder should be [project_root]/src/main/resources

    val FONT_FILE = File("resources/fonts/NotoSansTamil.ttf")
    

    If I'm right then the font should be at [project_root]/src/main/resources/fonts/NotoSansTamil.ttf but your FONT_FILE will point to [project_root]/resources/fonts/NotoSansTamil.ttf.

    Messy Base URL

    You build and pass baseUrl to PdfRenderedBuilder to resolve resources.

    val baseUrl = javaClass
        .protectionDomain
        .codeSource
        .location
        .toString()
    

    If your layout is Maven / Gradle like, then it points to [project_root]/target/classes/. It will never work on the other machine and it won't work when you pakcage your code into an artifact.

    Loading font as file not a resource

    val FONT_FILE = File("resources/fonts/NotoSansTamil.ttf")
    

    This snippet specify a file on the file system, but you need a resource which is usually shipped with that artifact.

    Solution

    val fontUrl = object {}.javaClass
        .getResource("fonts/NotoSansTamil.ttf")?.toURI()!!
    val fontFile = File(fontUrl)
    

    Mixing font resolution concepts

    You added a font both renderer .useFont(...) and html / css side src: url(...); It's a bad idea and won't work properly.

    Solution: Choose only one way. Use either builder.useFont or a @font-face rule, not both. I suggest use builder.useFont. In this case @font-face definition and messy baseUrl are no longer required.

    Putting all together

    Fixing all previously mentioned problems the following code will work.

    NOTE: I have never written Kotlin code before, probably it could be made better, optimized, etc.

    <!DOCTYPE html PUBLIC "-//OPENHTMLTOPDF//DOC XHTML Character Entities Only 1.0//EN" "">
    
    <html lang="en">
    <head>
        <meta charset="UTF-16"/>
        <title>FreeMarker</title>
        <style>
            /*
                changed from @font-face
                no src: passed, it will be resolved at renderer side
             */
            body {
                font-family: noto-sans, sans-serif;
                font-style: normal;
                font-weight: 400;
            }
        </style>
    </head>
    <body>
        <h1> Welcome to FreeMarker!</h1>
    </body>
    </html>
    
    private const val html = """ ... """ // HTML content as Stirng
    
    fun main() {
        val outFile = createTempFile("sample_", ".pdf")
        // using font as resource
        val fontUrl = object {}.javaClass
            .getResource("fonts/NotoSansTamil.ttf")?.toURI()!!
    
        PdfRendererBuilder()
            .withHtmlContent(html, null) // baseUrl no longer passed
            .useFont(File(fontUrl), "noto-sans") //matching font-family
            .toStream(outFile.outputStream())
            .run()
        println("PDF file created at: $outFile")
        getDesktop().open(outFile.toFile())
    }