From d7946b18bc61bdbf8197aece32a43f441b1b70d9 Mon Sep 17 00:00:00 2001 From: Joakim Taule Kartveit Date: Wed, 15 Nov 2023 10:59:44 +0100 Subject: [PATCH 1/2] Use pdfgen-core --- build.gradle.kts | 10 +- src/main/kotlin/no/nav/pdfgen/Bootstrap.kt | 15 +- src/main/kotlin/no/nav/pdfgen/Environment.kt | 73 ---- .../kotlin/no/nav/pdfgen/MetricRegistry.kt | 20 -- .../no/nav/pdfgen/api/GeneratePdfApi.kt | 95 ++---- .../nav/pdfgen/domain/syfosoknader/Periode.kt | 8 - .../domain/syfosoknader/PeriodeMapper.kt | 27 -- src/main/kotlin/no/nav/pdfgen/pdf/Create.kt | 145 -------- .../kotlin/no/nav/pdfgen/template/Helpers.kt | 319 ------------------ .../no/nav/pdfgen/template/Templates.kt | 37 -- .../kotlin/no/nav/pdfgen/util/FontMetadata.kt | 15 - src/main/kotlin/no/nav/pdfgen/util/Image.kt | 42 --- .../kotlin/no/nav/pdfgen/DockerImageTest.kt | 8 +- src/test/kotlin/no/nav/pdfgen/HelperTest.kt | 5 +- src/test/kotlin/no/nav/pdfgen/PdfGenITest.kt | 5 +- .../kotlin/no/nav/pdfgen/RenderingTest.kt | 44 ++- 16 files changed, 85 insertions(+), 783 deletions(-) delete mode 100644 src/main/kotlin/no/nav/pdfgen/Environment.kt delete mode 100644 src/main/kotlin/no/nav/pdfgen/MetricRegistry.kt delete mode 100644 src/main/kotlin/no/nav/pdfgen/domain/syfosoknader/Periode.kt delete mode 100644 src/main/kotlin/no/nav/pdfgen/domain/syfosoknader/PeriodeMapper.kt delete mode 100644 src/main/kotlin/no/nav/pdfgen/pdf/Create.kt delete mode 100644 src/main/kotlin/no/nav/pdfgen/template/Helpers.kt delete mode 100644 src/main/kotlin/no/nav/pdfgen/template/Templates.kt delete mode 100644 src/main/kotlin/no/nav/pdfgen/util/FontMetadata.kt delete mode 100644 src/main/kotlin/no/nav/pdfgen/util/Image.kt diff --git a/build.gradle.kts b/build.gradle.kts index 7c40a3e..73424f3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,7 @@ val junitJupiterVersion = "5.10.1" val verapdfVersion = "1.24.1" val ktfmtVersion = "0.44" val testcontainersVersion= "1.19.1" +val pdfgencoreVersion = "1.0.2" plugins { @@ -67,10 +68,14 @@ tasks { repositories { mavenCentral() + maven { + url = uri("https://github-package-registry-mirror.gc.nav.no/cached/maven-release") + } } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib") + implementation("no.nav.pdfgen:pdfgen-core:$pdfgencoreVersion") implementation("com.github.jknack:handlebars:$handlebarsVersion") implementation("com.github.jknack:handlebars-jackson2:$handlebarsVersion") @@ -78,12 +83,7 @@ dependencies { implementation("com.openhtmltopdf:openhtmltopdf-slf4j:$openHtmlToPdfVersion") implementation("com.openhtmltopdf:openhtmltopdf-svg-support:$openHtmlToPdfVersion") - implementation("org.jsoup:jsoup:$jsoupVersion") - implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") - implementation("javax.xml.bind:jaxb-api:$jaxbApiVersion") - implementation("org.glassfish.jaxb:jaxb-runtime:$jaxbVersion") implementation("io.ktor:ktor-server-netty:$ktorVersion") implementation("io.ktor:ktor-server-core:$ktorVersion") diff --git a/src/main/kotlin/no/nav/pdfgen/Bootstrap.kt b/src/main/kotlin/no/nav/pdfgen/Bootstrap.kt index 3b004eb..9240687 100644 --- a/src/main/kotlin/no/nav/pdfgen/Bootstrap.kt +++ b/src/main/kotlin/no/nav/pdfgen/Bootstrap.kt @@ -3,8 +3,6 @@ package no.nav.pdfgen // Uncommemt to enable debug to file // import java.io.File -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.registerKotlinModule import com.github.jknack.handlebars.Template import com.openhtmltopdf.slf4j.Slf4jLogger import com.openhtmltopdf.util.XRLog @@ -32,13 +30,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import no.nav.pdfgen.api.setupGeneratePdfApi -import no.nav.pdfgen.template.loadTemplates +import no.nav.pdfgen.core.PDFgen +import no.nav.pdfgen.core.template.loadTemplates import org.slf4j.Logger import org.slf4j.LoggerFactory import org.verapdf.gf.foundry.VeraGreenfieldFoundryProvider -val objectMapper: ObjectMapper = ObjectMapper().registerKotlinModule() - val log: Logger = LoggerFactory.getLogger("pdfgen") fun main() { @@ -49,8 +46,10 @@ fun initializeApplication(port: Int): ApplicationEngine { System.setProperty("sun.java2d.cmm", "sun.java2d.cmm.kcms.KcmsServiceProvider") VeraGreenfieldFoundryProvider.initialise() - val env = Environment() - val templates = loadTemplates(env) + PDFgen.init(no.nav.pdfgen.core.Environment()) + + val env = no.nav.pdfgen.core.Environment() + val templates = loadTemplates() val collectorRegistry: CollectorRegistry = CollectorRegistry.defaultRegistry XRLog.setLoggerImpl(Slf4jLogger()) @@ -92,7 +91,7 @@ fun initializeApplication(port: Int): ApplicationEngine { } } } - setupGeneratePdfApi(env, templates) + setupGeneratePdfApi(env) } } } diff --git a/src/main/kotlin/no/nav/pdfgen/Environment.kt b/src/main/kotlin/no/nav/pdfgen/Environment.kt deleted file mode 100644 index 1db1394..0000000 --- a/src/main/kotlin/no/nav/pdfgen/Environment.kt +++ /dev/null @@ -1,73 +0,0 @@ -package no.nav.pdfgen - -import com.fasterxml.jackson.module.kotlin.readValue -import io.ktor.util.* -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.util.* -import kotlin.streams.toList -import no.nav.pdfgen.util.FontMetadata -import org.apache.pdfbox.io.IOUtils - -val templateRoot: Path = Paths.get("templates/") -val imagesRoot: Path = Paths.get("resources/") -val fontsRoot: Path = Paths.get("fonts/") - -data class Environment( - val images: Map = loadImages(), - val resources: Map = loadResources(), - val colorProfile: ByteArray = - IOUtils.toByteArray(Environment::class.java.getResourceAsStream("/sRGB2014.icc")), - val fonts: List = - objectMapper.readValue(Files.newInputStream(fontsRoot.resolve("config.json"))), - val disablePdfGet: Boolean = System.getenv("DISABLE_PDF_GET")?.let { it == "true" } ?: false, - val enableHtmlEndpoint: Boolean = - System.getenv("ENABLE_HTML_ENDPOINT")?.let { it == "true" } ?: false, -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Environment - - if (!colorProfile.contentEquals(other.colorProfile)) return false - - return true - } - - override fun hashCode(): Int { - return colorProfile.contentHashCode() - } -} - -fun loadImages() = - Files.list(imagesRoot) - .filter { - val validExtensions = setOf("jpg", "jpeg", "png", "bmp", "svg") - !Files.isHidden(it) && it.fileName.extension in validExtensions - } - .map { - val fileName = it.fileName.toString() - val extension = - when (it.fileName.extension) { - "jpg" -> "jpeg" // jpg is not a valid mime-type - "svg" -> "svg+xml" - else -> it.fileName.extension - } - val base64string = Base64.getEncoder().encodeToString(Files.readAllBytes(it)) - val base64 = "data:image/$extension;base64,$base64string" - fileName to base64 - } - .toList() - .toMap() - -fun loadResources() = - Files.list(imagesRoot) - .filter { - val validExtensions = setOf("svg") - !Files.isHidden(it) && it.fileName.extension in validExtensions - } - .map { it.fileName.toString() to Files.readAllBytes(it) } - .toList() - .toMap() diff --git a/src/main/kotlin/no/nav/pdfgen/MetricRegistry.kt b/src/main/kotlin/no/nav/pdfgen/MetricRegistry.kt deleted file mode 100644 index 822d0de..0000000 --- a/src/main/kotlin/no/nav/pdfgen/MetricRegistry.kt +++ /dev/null @@ -1,20 +0,0 @@ -package no.nav.pdfgen - -import io.prometheus.client.Summary - -val HANDLEBARS_RENDERING_SUMMARY: Summary = - Summary.Builder() - .name("handlebars_rendering") - .help("Time it takes for handlebars to render the template") - .register() -val OPENHTMLTOPDF_RENDERING_SUMMARY: Summary = - Summary.Builder() - .name("openhtmltopdf_rendering_summary") - .help("Time it takes to render a PDF") - .labelNames("application_name", "template_type") - .register() -val JSOUP_PARSE_SUMMARY: Summary = - Summary.Builder() - .name("jsoup_parse") - .help("Time it takes jsoup to parse the template") - .register() diff --git a/src/main/kotlin/no/nav/pdfgen/api/GeneratePdfApi.kt b/src/main/kotlin/no/nav/pdfgen/api/GeneratePdfApi.kt index 73f5439..d54b38e 100644 --- a/src/main/kotlin/no/nav/pdfgen/api/GeneratePdfApi.kt +++ b/src/main/kotlin/no/nav/pdfgen/api/GeneratePdfApi.kt @@ -1,10 +1,6 @@ package no.nav.pdfgen.api import com.fasterxml.jackson.databind.JsonNode -import com.github.jknack.handlebars.Context -import com.github.jknack.handlebars.JsonNodeValueResolver -import com.github.jknack.handlebars.Template -import com.github.jknack.handlebars.context.MapValueResolver import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.request.contentType @@ -23,20 +19,24 @@ import java.nio.file.Files import java.nio.file.Paths import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import net.logstash.logback.argument.StructuredArguments -import no.nav.pdfgen.* -import no.nav.pdfgen.pdf.PdfContent -import no.nav.pdfgen.pdf.createPDFA -import no.nav.pdfgen.template.TemplateMap -import no.nav.pdfgen.template.loadTemplates +import no.nav.pdfgen.core.Environment +import no.nav.pdfgen.core.OPENHTMLTOPDF_RENDERING_SUMMARY +import no.nav.pdfgen.core.objectMapper +import no.nav.pdfgen.core.pdf.createHtml +import no.nav.pdfgen.core.pdf.createHtmlFromTemplateData +import no.nav.pdfgen.core.pdf.createPDFA +import no.nav.pdfgen.log -fun Routing.setupGeneratePdfApi(env: Environment, templates: TemplateMap) { +fun Routing.setupGeneratePdfApi(env: Environment) { route("/api/v1/genpdf") { if (!env.disablePdfGet) { get("/{applicationName}/{template}") { - val hotTemplates = loadTemplates(env) - createHtml(call, hotTemplates, true)?.let { document -> - call.respond(PdfContent(document, env)) + val template = call.parameters["template"]!! + val applicationName = call.parameters["applicationName"]!! + val jsonNode = hotTemplateData(applicationName, template) + + createHtml(template, applicationName, jsonNode)?.let { document -> + call.respond(createPDFA(document)) } ?: call.respondText( "Template or application not found", @@ -46,8 +46,11 @@ fun Routing.setupGeneratePdfApi(env: Environment, templates: TemplateMap) { } post("/{applicationName}/{template}") { val startTime = System.currentTimeMillis() - createHtml(call, templates)?.let { document -> - call.respond(PdfContent(document, env)) + val template = call.parameters["template"]!! + val applicationName = call.parameters["applicationName"]!! + + createHtmlFromTemplateData(template, applicationName)?.let { document -> + call.respond(createPDFA(document)) log.info("Done generating PDF in ${System.currentTimeMillis() - startTime}ms") } ?: call.respondText( @@ -55,6 +58,7 @@ fun Routing.setupGeneratePdfApi(env: Environment, templates: TemplateMap) { status = HttpStatusCode.NotFound ) } + post("/html/{applicationName}") { val applicationName = call.parameters["applicationName"]!! val timer = @@ -62,7 +66,7 @@ fun Routing.setupGeneratePdfApi(env: Environment, templates: TemplateMap) { val html = call.receiveText() - call.respond(PdfContent(html, env)) + call.respond(createPDFA(html)) log.info( "Generated PDF using HTML template for $applicationName om ${timer.observeDuration()}ms" ) @@ -78,7 +82,7 @@ fun Routing.setupGeneratePdfApi(env: Environment, templates: TemplateMap) { withContext(Dispatchers.IO) { call.receive().use { inputStream -> ByteArrayOutputStream().use { outputStream -> - createPDFA(inputStream, outputStream, env) + createPDFA(inputStream, outputStream) call.respondBytes( outputStream.toByteArray(), contentType = ContentType.Application.Pdf @@ -98,8 +102,11 @@ fun Routing.setupGeneratePdfApi(env: Environment, templates: TemplateMap) { route("/api/v1/genhtml") { if (!env.disablePdfGet) { get("/{applicationName}/{template}") { - val hotTemplates = loadTemplates(env) - createHtml(call, hotTemplates, true)?.let { call.respond(it) } + val template = call.parameters["template"]!! + val applicationName = call.parameters["applicationName"]!! + val jsonNode = hotTemplateData(applicationName, template) + + createHtml(template, applicationName, jsonNode)?.let { call.respond(it) } ?: call.respondText( "Template or application not found", status = HttpStatusCode.NotFound @@ -109,7 +116,11 @@ fun Routing.setupGeneratePdfApi(env: Environment, templates: TemplateMap) { post("/{applicationName}/{template}") { val startTime = System.currentTimeMillis() - createHtml(call, templates)?.let { + val template = call.parameters["template"]!! + val applicationName = call.parameters["applicationName"]!! + val jsonNode: JsonNode = call.receive() + + createHtml(template, applicationName, jsonNode)?.let { call.respond(it) log.info("Done generating HTML in ${System.currentTimeMillis() - startTime}ms") } @@ -135,45 +146,3 @@ private fun hotTemplateData(applicationName: String, template: String): JsonNode ) return data } - -private suspend fun createHtml( - call: ApplicationCall, - templates: TemplateMap, - useHottemplate: Boolean = false, -): String? { - val template = call.parameters["template"]!! - val applicationName = call.parameters["applicationName"]!! - val jsonNode = - if (useHottemplate) hotTemplateData(applicationName, template) else call.receive() - log.debug("JSON: {}", objectMapper.writeValueAsString(jsonNode)) - return render(applicationName, template, templates, jsonNode) -} - -fun render( - applicationName: String, - template: String, - templates: Map, Template>, - jsonNode: JsonNode -): String? { - return HANDLEBARS_RENDERING_SUMMARY.startTimer() - .use { - templates[applicationName to template]?.apply( - Context.newBuilder(jsonNode) - .resolver( - JsonNodeValueResolver.INSTANCE, - MapValueResolver.INSTANCE, - ) - .build(), - ) - } - ?.let { html -> - log.debug("Generated HTML {}", StructuredArguments.keyValue("html", html)) - - /* Uncomment to output html to file for easier debug - * File("pdf.html").bufferedWriter().use { out -> - * out.write(html) - * } - */ - html - } -} diff --git a/src/main/kotlin/no/nav/pdfgen/domain/syfosoknader/Periode.kt b/src/main/kotlin/no/nav/pdfgen/domain/syfosoknader/Periode.kt deleted file mode 100644 index f89b323..0000000 --- a/src/main/kotlin/no/nav/pdfgen/domain/syfosoknader/Periode.kt +++ /dev/null @@ -1,8 +0,0 @@ -package no.nav.pdfgen.domain.syfosoknader - -import java.time.LocalDate - -class Periode { - val fom: LocalDate? = null - val tom: LocalDate? = null -} diff --git a/src/main/kotlin/no/nav/pdfgen/domain/syfosoknader/PeriodeMapper.kt b/src/main/kotlin/no/nav/pdfgen/domain/syfosoknader/PeriodeMapper.kt deleted file mode 100644 index 5733967..0000000 --- a/src/main/kotlin/no/nav/pdfgen/domain/syfosoknader/PeriodeMapper.kt +++ /dev/null @@ -1,27 +0,0 @@ -package no.nav.pdfgen.domain.syfosoknader - -import com.fasterxml.jackson.core.JsonParseException -import com.fasterxml.jackson.databind.JsonMappingException -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import java.io.IOException - -object PeriodeMapper { - private val objectMapper = ObjectMapper().registerModule(JavaTimeModule()) - - internal fun jsonTilPeriode(json: String): Periode { - try { - val periode = objectMapper.readValue(json, Periode::class.java) - if (periode.tom == null || periode.fom == null || periode.fom.isAfter(periode.tom)) { - throw IllegalArgumentException() - } - return periode - } catch (exception: JsonParseException) { - throw IllegalArgumentException(exception) - } catch (exception: JsonMappingException) { - throw IllegalArgumentException(exception) - } catch (iOException: IOException) { - throw RuntimeException(iOException) - } - } -} diff --git a/src/main/kotlin/no/nav/pdfgen/pdf/Create.kt b/src/main/kotlin/no/nav/pdfgen/pdf/Create.kt deleted file mode 100644 index 29941ce..0000000 --- a/src/main/kotlin/no/nav/pdfgen/pdf/Create.kt +++ /dev/null @@ -1,145 +0,0 @@ -package no.nav.pdfgen.pdf - -import com.openhtmltopdf.pdfboxout.PdfRendererBuilder -import com.openhtmltopdf.svgsupport.BatikSVGDrawer -import io.ktor.http.* -import io.ktor.http.content.* -import java.io.* -import java.lang.IllegalArgumentException -import java.util.* -import javax.imageio.ImageIO -import no.nav.pdfgen.Environment -import no.nav.pdfgen.log -import no.nav.pdfgen.util.scale -import no.nav.pdfgen.util.toPortait -import org.apache.pdfbox.pdmodel.PDDocument -import org.apache.pdfbox.pdmodel.PDPage -import org.apache.pdfbox.pdmodel.PDPageContentStream -import org.apache.pdfbox.pdmodel.common.PDMetadata -import org.apache.pdfbox.pdmodel.common.PDRectangle -import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkInfo -import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot -import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent -import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory -import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences -import org.apache.pdfbox.util.Matrix -import org.apache.xmpbox.XMPMetadata -import org.apache.xmpbox.type.BadFieldValueException -import org.apache.xmpbox.xml.XmpSerializer -import org.verapdf.pdfa.Foundries -import org.verapdf.pdfa.flavours.PDFAFlavour -import org.verapdf.pdfa.results.TestAssertion - -fun createPDFA(html: String, env: Environment): ByteArray { - val pdf = - ByteArrayOutputStream() - .apply { - PdfRendererBuilder() - .apply { - for (font in env.fonts) { - useFont( - { ByteArrayInputStream(font.bytes) }, - font.family, - font.weight, - font.style, - font.subset - ) - } - } - .usePdfAConformance(PdfRendererBuilder.PdfAConformance.PDFA_2_A) - .usePdfUaAccessbility(true) - .useColorProfile(env.colorProfile) - .useSVGDrawer(BatikSVGDrawer()) - .withHtmlContent(html, null) - .toStream(this) - .run() - } - .toByteArray() - require(verifyCompliance(pdf)) { "Non-compliant PDF/A :(" } - return pdf -} - -fun createPDFA(imageStream: InputStream, outputStream: OutputStream, env: Environment) { - PDDocument().use { document -> - val page = PDPage(PDRectangle.A4) - document.addPage(page) - val image = toPortait(ImageIO.read(imageStream)) - - val quality = 1.0f - - val pdImage = JPEGFactory.createFromImage(document, image, quality) - val imageSize = scale(pdImage, page) - - PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, false).use { - it.drawImage(pdImage, Matrix(imageSize.width, 0f, 0f, imageSize.height, 0f, 0f)) - } - - val xmp = XMPMetadata.createXMPMetadata() - val catalog = document.documentCatalog - val cal = Calendar.getInstance() - - try { - val dc = xmp.createAndAddDublinCoreSchema() - dc.addCreator("pdfgen") - dc.addDate(cal) - - val id = xmp.createAndAddPFAIdentificationSchema() - id.part = 2 - id.conformance = "U" - - val serializer = XmpSerializer() - val baos = ByteArrayOutputStream() - serializer.serialize(xmp, baos, true) - - val metadata = PDMetadata(document) - metadata.importXMPMetadata(baos.toByteArray()) - catalog.metadata = metadata - } catch (e: BadFieldValueException) { - throw IllegalArgumentException(e) - } - - val intent = PDOutputIntent(document, env.colorProfile.inputStream()) - intent.info = "sRGB IEC61966-2.1" - intent.outputCondition = "sRGB IEC61966-2.1" - intent.outputConditionIdentifier = "sRGB IEC61966-2.1" - intent.registryName = "http://www.color.org" - catalog.addOutputIntent(intent) - catalog.language = "nb-NO" - - val pdViewer = PDViewerPreferences(page.cosObject) - pdViewer.setDisplayDocTitle(true) - catalog.viewerPreferences = pdViewer - - catalog.markInfo = PDMarkInfo(page.cosObject) - catalog.structureTreeRoot = PDStructureTreeRoot() - catalog.markInfo.isMarked = true - - document.save(outputStream) - document.close() - } -} - -private fun verifyCompliance( - input: ByteArray, - flavour: PDFAFlavour = PDFAFlavour.PDFA_2_A -): Boolean { - val pdf = ByteArrayInputStream(input) - val validator = Foundries.defaultInstance().createValidator(flavour, false) - val result = Foundries.defaultInstance().createParser(pdf).use { validator.validate(it) } - val failures = result.testAssertions.filter { it.status != TestAssertion.Status.PASSED } - failures.forEach { test -> - log.warn(test.message) - log.warn("Location ${test.location.context} ${test.location.level}") - log.warn("Status ${test.status}") - log.warn("Test number ${test.ruleId.testNumber}") - } - return failures.isEmpty() -} - -class PdfContent( - private val html: String, - private val env: Environment, - override val contentType: ContentType = ContentType.Application.Pdf, -) : OutgoingContent.ByteArrayContent() { - override fun bytes(): ByteArray = createPDFA(html, env) -} diff --git a/src/main/kotlin/no/nav/pdfgen/template/Helpers.kt b/src/main/kotlin/no/nav/pdfgen/template/Helpers.kt deleted file mode 100644 index 01e5065..0000000 --- a/src/main/kotlin/no/nav/pdfgen/template/Helpers.kt +++ /dev/null @@ -1,319 +0,0 @@ -package no.nav.pdfgen.template - -import com.fasterxml.jackson.databind.node.ArrayNode -import com.github.jknack.handlebars.Context -import com.github.jknack.handlebars.Handlebars -import com.github.jknack.handlebars.Helper -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import java.time.temporal.ChronoUnit -import java.util.* -import no.nav.pdfgen.Environment -import no.nav.pdfgen.domain.syfosoknader.Periode -import no.nav.pdfgen.domain.syfosoknader.PeriodeMapper - -val dateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") -val dateFormatLong: DateTimeFormatter = - DateTimeFormatter.ofPattern("d. MMMM yyyy").withLocale(Locale.of("no", "NO")) -val datetimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm") - -fun formatDate(formatter: DateTimeFormatter, context: CharSequence): String = - try { - formatter.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parseBest(context)) - } catch (e: Exception) { - formatter.format(DateTimeFormatter.ISO_DATE_TIME.parse(context)) - } - -fun registerNavHelpers(handlebars: Handlebars, env: Environment) { - handlebars.apply { - registerHelper( - "iso_to_nor_date", - Helper { context, _ -> - if (context == null) return@Helper "" - formatDate(dateFormat, context) - }, - ) - - registerHelper( - "iso_to_nor_datetime", - Helper { context, _ -> - if (context == null) return@Helper "" - formatDate(datetimeFormat, context) - }, - ) - - registerHelper( - "iso_to_date", - Helper { context, _ -> - if (context == null) return@Helper "" - dateFormat.format(DateTimeFormatter.ISO_DATE.parse(context)) - }, - ) - - registerHelper( - "iso_to_long_date", - Helper { context, _ -> - if (context == null) return@Helper "" - try { - dateFormatLong.format(DateTimeFormatter.ISO_DATE_TIME.parse(context)) - } catch (e: Exception) { - dateFormatLong.format(DateTimeFormatter.ISO_DATE.parse(context)) - } - }, - ) - - registerHelper( - "duration", - Helper { context, options -> - ChronoUnit.DAYS.between( - LocalDate.from(DateTimeFormatter.ISO_DATE.parse(context)), - LocalDate.from(DateTimeFormatter.ISO_DATE.parse(options.param(0))), - ) - }, - ) - - // Expects json-objects of the form { "fom": "2018-05-20", "tom": "2018-05-29" } - registerHelper( - "json_to_period", - Helper { context, _ -> - if (context == null) { - return@Helper "" - } else { - val periode: Periode = PeriodeMapper.jsonTilPeriode(context) - return@Helper periode.fom!!.format(dateFormat) + - " - " + - periode.tom!!.format(dateFormat) - } - }, - ) - registerHelper( - "insert_at", - Helper { context, options -> - if (context == null) return@Helper "" - val divider = options.hash("divider", " ") - options.params - .map { it as Int } - .fold(context.toString()) { v, idx -> - v.substring(0, idx) + divider + v.substring(idx, v.length) - } - }, - ) - - registerHelper( - "eq", - Helper { context, options -> - if (context?.toString() == options.param(0)?.toString()) options.fn() - else options.inverse() - }, - ) - - registerHelper( - "not_eq", - Helper { context, options -> - if (context?.toString() != options.param(0)?.toString()) options.fn() - else options.inverse() - }, - ) - - registerHelper( - "gt", - Helper> { context, options -> - val param = options.param(0) as Comparable - if (context > param) options.fn() else options.inverse() - }, - ) - - registerHelper( - "lt", - Helper> { context, options -> - val param = options.param(0) as Comparable - if (context < param) options.fn() else options.inverse() - }, - ) - - registerHelper( - "safe", - Helper { context, _ -> - if (context == null) "" else Handlebars.SafeString(context) - }, - ) - - registerHelper( - "image", - Helper { context, _ -> if (context == null) "" else env.images[context] }, - ) - - registerHelper( - "resource", - Helper { context, _ -> env.resources[context]?.toString(Charsets.UTF_8) ?: "" }, - ) - - registerHelper( - "capitalize", - Helper { context, _ -> - context?.lowercase()?.replaceFirstChar { it.uppercase() } ?: "" - }, - ) - - registerHelper( - "capitalize_names", - Helper { context, _ -> - if (context == null) { - "" - } else - Handlebars.SafeString( - context - .trim() - .replace("\\s+".toRegex(), " ") - .lowercase() - .capitalizeWords(" ") - .capitalizeWords("-") - .capitalizeWords("'"), - ) - }, - ) - - registerHelper( - "uppercase", - Helper { context, _ -> context?.uppercase() ?: "" }, - ) - - registerHelper( - "inc", - Helper { context, _ -> context + 1 }, - ) - - registerHelper( - "formatComma", - Helper { context, _ -> context?.toString()?.replace(".", ",") ?: "" }, - ) - - registerHelper( - "any", - Helper { first, options -> - if ((listOf(first) + options.params).all { options.isFalsy(it) }) { - options.inverse() - } else { - options.fn() - } - }, - ) - - registerHelper( - "contains_field", - Helper?> { list, options -> - val checkfor = options.param(0, null as String?) - - val contains = - list - ?.map { Context.newContext(options.context, it) } - ?.any { ctx -> !options.isFalsy(ctx.get(checkfor)) } - ?: false - - if (contains) { - options.fn() - } else { - options.inverse() - } - }, - ) - - registerHelper( - "contains_all", - Helper { list, options -> - val textValues = list.map { it.textValue() } - - val params = options.params.toList() - val contains = if (params.isEmpty()) false else textValues.containsAll(params) - - if (contains) { - options.fn() - } else { - options.inverse() - } - }, - ) - - registerHelper( - "currency_no", - Helper { context, options -> - if (context == null) return@Helper "" - val withoutDecimals = options.param(0, false) - - val splitNumber = context.toString().split(".") - - // we're joining with a non-breaking space since currency values should not be split - // across several lines - val formattedNumber = - splitNumber.first().reversed().chunked(3).joinToString("\u00A0").reversed() - if (withoutDecimals) { - formattedNumber - } else { - val decimals = - splitNumber.drop(1).firstOrNull()?.let { (it + "0").substring(0, 2) } - ?: "00" - "$formattedNumber,$decimals" - } - }, - ) - - registerHelper( - "int_as_currency_no", - Helper { context, _ -> - val kr = context / 100 - val øre = context % 100 - - // using .format(locale = Locale("nb")) should also do the trick, but it appears - // this no longer works - // so we just reuse the string-based code from above to get the format we want :) - val formattedKr = - kr.toString().reversed().chunked(3).joinToString("\u00A0").reversed() - "$formattedKr,%02d".format(øre) - }, - ) - - registerHelper( - "string_as_currency_no", - Helper { context, _ -> - val value = context.filter { c -> c.isDigit() }.toInt() - val kr = value / 100 - val øre = value % 100 - - val formattedKr = - kr.toString().reversed().chunked(3).joinToString("\u00A0").reversed() - "$formattedKr,%02d".format(øre) - }, - ) - - registerHelper( - "is_defined", - Helper { context, options -> - if (context != null) options.fn() else options.inverse() - }, - ) - - registerHelper( - "breaklines", - Helper { context, _ -> - if (context == null) { - "" - } else { - val santizedText = Handlebars.Utils.escapeExpression(context) - val withLineBreak = - santizedText - .toString() - .replace("\\r\\n", "
") - .replace("\\n", "
") - .replace("\r\n", "
") - .replace("\n", "
") - Handlebars.SafeString(withLineBreak) - } - }, - ) - } -} - -private fun String.capitalizeWords(wordSplitter: String) = - this.split(wordSplitter).joinToString(wordSplitter) { - it.trim().replaceFirstChar { it.uppercase() } - } diff --git a/src/main/kotlin/no/nav/pdfgen/template/Templates.kt b/src/main/kotlin/no/nav/pdfgen/template/Templates.kt deleted file mode 100644 index 42cb6f9..0000000 --- a/src/main/kotlin/no/nav/pdfgen/template/Templates.kt +++ /dev/null @@ -1,37 +0,0 @@ -package no.nav.pdfgen.template - -import com.github.jknack.handlebars.Handlebars -import com.github.jknack.handlebars.Template -import com.github.jknack.handlebars.io.FileTemplateLoader -import com.github.jknack.handlebars.io.StringTemplateSource -import io.ktor.util.* -import java.nio.file.Files -import no.nav.pdfgen.Environment -import no.nav.pdfgen.templateRoot - -typealias TemplateMap = Map, Template> - -fun setupHandlebars(env: Environment) = - Handlebars(FileTemplateLoader(templateRoot.toFile())).apply { - registerNavHelpers(this, env) - infiniteLoops(true) - } - -fun loadTemplates(env: Environment): TemplateMap = - Files.list(templateRoot) - .filter { !Files.isHidden(it) && Files.isDirectory(it) } - .map { - it.fileName.toString() to Files.list(it).filter { b -> b.fileName.extension == "hbs" } - } - .flatMap { (applicationName, templateFiles) -> - templateFiles.map { - val fileName = it.fileName.toString() - val templateName = fileName.substring(0..fileName.length - 5) - val templateBytes = Files.readAllBytes(it).toString(Charsets.UTF_8) - val xhtml = - setupHandlebars(env).compile(StringTemplateSource(fileName, templateBytes)) - (applicationName to templateName) to xhtml - } - } - .toList() - .toMap() diff --git a/src/main/kotlin/no/nav/pdfgen/util/FontMetadata.kt b/src/main/kotlin/no/nav/pdfgen/util/FontMetadata.kt deleted file mode 100644 index 252f3fb..0000000 --- a/src/main/kotlin/no/nav/pdfgen/util/FontMetadata.kt +++ /dev/null @@ -1,15 +0,0 @@ -package no.nav.pdfgen.util - -import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder -import java.nio.file.Files -import no.nav.pdfgen.fontsRoot - -data class FontMetadata( - val family: String, - val path: String, - val weight: Int, - val style: BaseRendererBuilder.FontStyle, - val subset: Boolean, -) { - val bytes: ByteArray = Files.readAllBytes(fontsRoot.resolve(path)) -} diff --git a/src/main/kotlin/no/nav/pdfgen/util/Image.kt b/src/main/kotlin/no/nav/pdfgen/util/Image.kt deleted file mode 100644 index 198ee85..0000000 --- a/src/main/kotlin/no/nav/pdfgen/util/Image.kt +++ /dev/null @@ -1,42 +0,0 @@ -package no.nav.pdfgen.util - -import java.awt.geom.AffineTransform -import java.awt.image.AffineTransformOp -import java.awt.image.BufferedImage -import org.apache.pdfbox.pdmodel.PDPage -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject - -data class ImageSize(val width: Float, val height: Float) - -fun toPortait(image: BufferedImage): BufferedImage { - if (image.height >= image.width) { - return image - } - - val rotateTransform = - AffineTransform.getRotateInstance( - Math.toRadians(90.0), - (image.height / 2f).toDouble(), - (image.height / 2f).toDouble() - ) - - return AffineTransformOp(rotateTransform, AffineTransformOp.TYPE_BILINEAR) - .filter(image, BufferedImage(image.height, image.width, image.type)) -} - -fun scale(image: PDImageXObject, page: PDPage): ImageSize { - var width = image.width.toFloat() - var height = image.height.toFloat() - - if (width > page.cropBox.width) { - width = page.cropBox.width - height = width * image.height.toFloat() / image.width.toFloat() - } - - if (height > page.cropBox.height) { - height = page.cropBox.height - width = height * image.width.toFloat() / image.height.toFloat() - } - - return ImageSize(width, height) -} diff --git a/src/test/kotlin/no/nav/pdfgen/DockerImageTest.kt b/src/test/kotlin/no/nav/pdfgen/DockerImageTest.kt index 0dcd1d2..1b907d4 100644 --- a/src/test/kotlin/no/nav/pdfgen/DockerImageTest.kt +++ b/src/test/kotlin/no/nav/pdfgen/DockerImageTest.kt @@ -10,6 +10,7 @@ import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.serialization.jackson.* +import kotlin.io.path.Path import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -18,21 +19,16 @@ import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.Network import org.testcontainers.containers.wait.strategy.Wait import org.testcontainers.images.builder.ImageFromDockerfile -import kotlin.io.path.Path internal class DockerImageTest { - @Test @DisabledIfEnvironmentVariable(named = "CI", matches = "true") internal fun `Test Dockerfile`() { val network = Network.newNetwork() val pdfgenContainer = - GenericContainer( - ImageFromDockerfile() - .withDockerfile(Path("./Dockerfile")) - ) + GenericContainer(ImageFromDockerfile().withDockerfile(Path("./Dockerfile"))) .withNetwork(network) .withExposedPorts(8080) .waitingFor(Wait.forHttp("/internal/is_ready")) diff --git a/src/test/kotlin/no/nav/pdfgen/HelperTest.kt b/src/test/kotlin/no/nav/pdfgen/HelperTest.kt index 8f2ad66..94eefa9 100644 --- a/src/test/kotlin/no/nav/pdfgen/HelperTest.kt +++ b/src/test/kotlin/no/nav/pdfgen/HelperTest.kt @@ -8,16 +8,15 @@ import com.github.jknack.handlebars.Handlebars import com.github.jknack.handlebars.JsonNodeValueResolver import com.github.jknack.handlebars.context.MapValueResolver import com.github.jknack.handlebars.io.ClassPathTemplateLoader -import no.nav.pdfgen.template.registerNavHelpers +import no.nav.pdfgen.core.template.registerNavHelpers import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows internal class HelperTest { val jsonNodeFactory = JsonNodeFactory.instance - private val env = Environment() private val handlebars = - Handlebars(ClassPathTemplateLoader()).apply { registerNavHelpers(this, env) } + Handlebars(ClassPathTemplateLoader()).apply { registerNavHelpers(this) } private fun jsonContext(jsonNode: JsonNode): Context { println(ObjectMapper().writeValueAsString(jsonNode)) diff --git a/src/test/kotlin/no/nav/pdfgen/PdfGenITest.kt b/src/test/kotlin/no/nav/pdfgen/PdfGenITest.kt index 4657381..c048671 100644 --- a/src/test/kotlin/no/nav/pdfgen/PdfGenITest.kt +++ b/src/test/kotlin/no/nav/pdfgen/PdfGenITest.kt @@ -16,7 +16,7 @@ import java.nio.file.Files import java.nio.file.Paths import java.util.concurrent.Executors import kotlinx.coroutines.* -import no.nav.pdfgen.template.loadTemplates +import no.nav.pdfgen.core.template.loadTemplates import org.apache.pdfbox.io.IOUtils import org.apache.pdfbox.pdmodel.PDDocument import org.junit.jupiter.api.AfterEach @@ -28,8 +28,7 @@ internal class PdfGenITest { private val applicationPort = getRandomPort() private val application = initializeApplication(applicationPort) private val client = HttpClient(CIO) { expectSuccess = false } - private val env = Environment() - private val templates = loadTemplates(env) + private val templates = loadTemplates() private val timeoutSeconds: Long = 10 @AfterEach diff --git a/src/test/kotlin/no/nav/pdfgen/RenderingTest.kt b/src/test/kotlin/no/nav/pdfgen/RenderingTest.kt index a6eb130..7ed2645 100644 --- a/src/test/kotlin/no/nav/pdfgen/RenderingTest.kt +++ b/src/test/kotlin/no/nav/pdfgen/RenderingTest.kt @@ -1,11 +1,15 @@ package no.nav.pdfgen +// import no.nav.pdfgen.core.api.render import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper +import com.github.jknack.handlebars.Context +import com.github.jknack.handlebars.JsonNodeValueResolver +import com.github.jknack.handlebars.context.MapValueResolver import java.io.ByteArrayInputStream -import no.nav.pdfgen.api.render -import no.nav.pdfgen.pdf.createPDFA -import no.nav.pdfgen.template.loadTemplates +import no.nav.pdfgen.core.HANDLEBARS_RENDERING_SUMMARY +import no.nav.pdfgen.core.pdf.createPDFA +import no.nav.pdfgen.core.template.loadTemplates import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -15,8 +19,7 @@ import org.verapdf.pdfa.flavours.PDFAFlavour import org.verapdf.pdfa.results.TestAssertion internal class RenderingTest { - private val env = Environment() - private val templates = loadTemplates(env) + private val templates = loadTemplates() private val objectMapper = ObjectMapper() @BeforeEach @@ -38,7 +41,7 @@ internal class RenderingTest { println( "Renders the template $templateName for application $applicationName without exceptions" ) - render(applicationName, templateName, templates, node) + render(applicationName, templateName, node) } } @@ -61,8 +64,8 @@ internal class RenderingTest { println( "Renders the template $templateName for application $applicationName to a PDF/A compliant document" ) - val doc = render(applicationName, templateName, templates, node) - val pdf = createPDFA(doc!!, env) + val doc = render(applicationName, templateName, node) + val pdf = createPDFA(doc!!) Foundries.defaultInstance().createParser(ByteArrayInputStream(pdf)).use { that -> val validationResult = validator.validate(that) validationResult.testAssertions @@ -83,7 +86,7 @@ internal class RenderingTest { val pdfaFlavour = PDFAFlavour.PDFA_2_U val validator = Foundries.defaultInstance().createValidator(pdfaFlavour, false) val doc = testTemplateIncludedFonts - val pdf = createPDFA(doc, env) + val pdf = createPDFA(doc) Foundries.defaultInstance().createParser(ByteArrayInputStream(pdf)).use { val validationResult = validator.validate(it) validationResult.testAssertions @@ -98,3 +101,26 @@ internal class RenderingTest { } } } + +// TODO remove this and use pdfgen-core method +fun render(directoryName: String, template: String, jsonNode: JsonNode): String? { + return HANDLEBARS_RENDERING_SUMMARY.startTimer() + .use { + loadTemplates()[directoryName to template]?.apply( + Context.newBuilder(jsonNode) + .resolver( + JsonNodeValueResolver.INSTANCE, + MapValueResolver.INSTANCE, + ) + .build(), + ) + } + ?.let { html -> + /* Uncomment to output html to file for easier debug + * File("pdf.html").bufferedWriter().use { out -> + * out.write(html) + * } + */ + html + } +} From c50508a3498d860b764e7780162ded2b85a875eb Mon Sep 17 00:00:00 2001 From: Joakim Taule Kartveit Date: Wed, 15 Nov 2023 11:38:14 +0100 Subject: [PATCH 2/2] Bumped pdfgencore to 1.0.3 --- build.gradle.kts | 2 +- .../kotlin/no/nav/pdfgen/RenderingTest.kt | 29 +------------------ 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 73424f3..83777ab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,7 +16,7 @@ val junitJupiterVersion = "5.10.1" val verapdfVersion = "1.24.1" val ktfmtVersion = "0.44" val testcontainersVersion= "1.19.1" -val pdfgencoreVersion = "1.0.2" +val pdfgencoreVersion = "1.0.3" plugins { diff --git a/src/test/kotlin/no/nav/pdfgen/RenderingTest.kt b/src/test/kotlin/no/nav/pdfgen/RenderingTest.kt index 7ed2645..1e24617 100644 --- a/src/test/kotlin/no/nav/pdfgen/RenderingTest.kt +++ b/src/test/kotlin/no/nav/pdfgen/RenderingTest.kt @@ -1,14 +1,10 @@ package no.nav.pdfgen -// import no.nav.pdfgen.core.api.render import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper -import com.github.jknack.handlebars.Context -import com.github.jknack.handlebars.JsonNodeValueResolver -import com.github.jknack.handlebars.context.MapValueResolver import java.io.ByteArrayInputStream -import no.nav.pdfgen.core.HANDLEBARS_RENDERING_SUMMARY import no.nav.pdfgen.core.pdf.createPDFA +import no.nav.pdfgen.core.pdf.render import no.nav.pdfgen.core.template.loadTemplates import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach @@ -101,26 +97,3 @@ internal class RenderingTest { } } } - -// TODO remove this and use pdfgen-core method -fun render(directoryName: String, template: String, jsonNode: JsonNode): String? { - return HANDLEBARS_RENDERING_SUMMARY.startTimer() - .use { - loadTemplates()[directoryName to template]?.apply( - Context.newBuilder(jsonNode) - .resolver( - JsonNodeValueResolver.INSTANCE, - MapValueResolver.INSTANCE, - ) - .build(), - ) - } - ?.let { html -> - /* Uncomment to output html to file for easier debug - * File("pdf.html").bufferedWriter().use { out -> - * out.write(html) - * } - */ - html - } -}