From 52b86e8f453d87a459929a401de642873f9510fa Mon Sep 17 00:00:00 2001 From: limebeck Date: Sun, 18 Feb 2024 15:33:11 +0300 Subject: [PATCH] V0.2.2: Add pdf rendering with playwright (alfa) --- README.MD | 24 ++++++- build.gradle.kts | 2 +- gradle.properties | 6 +- gradle/libs.versions.toml | 11 +-- gradle/wrapper/gradle-wrapper.properties | 2 +- reveal-kt/app/build.gradle.kts | 3 +- .../app/src/jvmMain/kotlin/Application.kt | 23 +++++- .../BundleToStatic.kt} | 64 ++--------------- .../jvmMain/kotlin/commands/InitTemplate.kt | 22 ++++++ .../src/jvmMain/kotlin/commands/RenderPdf.kt | 72 +++++++++++++++++++ .../src/jvmMain/kotlin/commands/RunServer.kt | 45 ++++++++++++ .../app/src/jvmMain/kotlin/pdf/PdfRenderer.kt | 30 ++++++++ .../app/src/jvmMain/kotlin/server/Server.kt | 6 +- 13 files changed, 234 insertions(+), 76 deletions(-) rename reveal-kt/app/src/jvmMain/kotlin/{Commands.kt => commands/BundleToStatic.kt} (55%) create mode 100644 reveal-kt/app/src/jvmMain/kotlin/commands/InitTemplate.kt create mode 100644 reveal-kt/app/src/jvmMain/kotlin/commands/RenderPdf.kt create mode 100644 reveal-kt/app/src/jvmMain/kotlin/commands/RunServer.kt create mode 100644 reveal-kt/app/src/jvmMain/kotlin/pdf/PdfRenderer.kt diff --git a/README.MD b/README.MD index 6313b6c..7fd160b 100644 --- a/README.MD +++ b/README.MD @@ -15,11 +15,31 @@ Run server with presentation: jbang revealkt@limebeck.dev run ./MyAwesomePresentation.reveal.kts ``` -Render presentation to static html site: +Bundle presentation to static html site: ```bash -jbang revealkt@limebeck.dev render ./MyAwesomePresentation.reveal.kts +jbang revealkt@limebeck.dev bundle ./MyAwesomePresentation.reveal.kts ``` +Render presentation to pdf via playwright: +```bash +jbang revealkt@limebeck.dev pdf ./MyAwesomePresentation.reveal.kts -o myPresentation.pdf +``` + +[!CAUTION] +For now playwright requires to download chromium at first run via npm + +Playwright documentation: https://playwright.dev/java/docs/browsers + +```bash +jbang revealkt@limebeck.dev pdf install-chrome +``` + +Uninstall chrome +```bash +jbang revealkt@limebeck.dev pdf uninstall-chrome +``` + + Create new presentation from template ```bash jbang revealkt@limebeck.dev init my-awesome-presentation diff --git a/build.gradle.kts b/build.gradle.kts index c571f37..c000940 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { kotlin("multiplatform") - id("com.github.ben-manes.versions") version "0.47.0" + alias(libs.plugins.versions) } repositories { diff --git a/gradle.properties b/gradle.properties index 83e07bb..75f9e10 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,11 +5,11 @@ yarn.ignoreScripts = false kotlinVersion=1.9.20 -revealKtVersion=0.2.1 +revealKtVersion=0.2.2 -kotlinCoroutinesVersion=1.7.3 +kotlinCoroutinesVersion=1.8.0 ktorVersion=2.1.3 -dokkaVersion=1.8.20 +dokkaVersion=1.9.10 repo.uri=https://maven.pkg.github.com/LimeBeck/reveal-kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc9dd4f..16f9944 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,21 +1,22 @@ [versions] kotlin = "1.9.0" -serialization = "1.4.1" +serialization = "1.6.3" ktor = "2.1.3" -kxhtml = "0.9.1" +kxhtml = "0.11.0" [libraries] slf4j = { module = "org.slf4j:slf4j-api", version = "2.0.7" } -logback = { module = "ch.qos.logback:logback-classic", version = "1.4.14"} -clikt = { module = "com.github.ajalt.clikt:clikt", version = "4.2.1" } +logback = { module = "ch.qos.logback:logback-classic", version = "1.5.0"} +clikt = { module = "com.github.ajalt.clikt:clikt", version = "4.2.2" } kxhtml-jvm = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kxhtml" } kxhtml = { module = "org.jetbrains.kotlinx:kotlinx-html", version.ref = "kxhtml" } uuid = { module = "com.benasher44:uuid", version = "0.8.2" } qrcode = { module = "io.github.g0dkar:qrcode-kotlin", version = "4.1.1" } +playwright = { module = "com.microsoft.playwright:playwright", version = "1.41.2" } [plugins] -versions = { id = "com.github.ben-manes.versions", version = "0.50.0" } +versions = { id = "com.github.ben-manes.versions", version = "0.51.0" } dokka = { id = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "kotlin" } build-config = { id = "dev.limebeck.build-time-config", version = "2.2.0"} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e1bef7e..17655d0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/reveal-kt/app/build.gradle.kts b/reveal-kt/app/build.gradle.kts index f40f6c9..1486724 100644 --- a/reveal-kt/app/build.gradle.kts +++ b/reveal-kt/app/build.gradle.kts @@ -34,7 +34,7 @@ buildTimeConfig { kotlin { jvm { java { - withSourcesJar() +// withSourcesJar() } withJava() testRuns["test"].executionTask.configure { @@ -76,6 +76,7 @@ kotlin { implementation(libs.logback) implementation(libs.slf4j) implementation(libs.clikt) + implementation(libs.playwright) } } val jvmTest by getting diff --git a/reveal-kt/app/src/jvmMain/kotlin/Application.kt b/reveal-kt/app/src/jvmMain/kotlin/Application.kt index 7664423..dbfa073 100644 --- a/reveal-kt/app/src/jvmMain/kotlin/Application.kt +++ b/reveal-kt/app/src/jvmMain/kotlin/Application.kt @@ -1,9 +1,28 @@ package dev.limebeck.application import com.github.ajalt.clikt.completion.completionOption +import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.subcommands +import dev.limebeck.application.commands.* +import dev.limebeck.revealkt.RevealkConfig -fun main(args: Array) = RevealKtApplication() - .subcommands(RunServer(), InitTemplate(), RenderToFile()) +fun main(args: Array) = RevealKtCliApplication() + .subcommands( + RunServer(), + InitTemplate(), + BundleToStatic(), + RenderPdf().subcommands(InstallChrome(), UninstallChrome()) + ) .completionOption() .main(args) + +class RevealKtCliApplication : CliktCommand( + printHelpOnEmptyArgs = true, + help = """ + RevealKt CLI + + Application version ${RevealkConfig.version} + """.trimIndent() +) { + override fun run() = Unit +} diff --git a/reveal-kt/app/src/jvmMain/kotlin/Commands.kt b/reveal-kt/app/src/jvmMain/kotlin/commands/BundleToStatic.kt similarity index 55% rename from reveal-kt/app/src/jvmMain/kotlin/Commands.kt rename to reveal-kt/app/src/jvmMain/kotlin/commands/BundleToStatic.kt index 6b7585b..602b00b 100644 --- a/reveal-kt/app/src/jvmMain/kotlin/Commands.kt +++ b/reveal-kt/app/src/jvmMain/kotlin/commands/BundleToStatic.kt @@ -1,62 +1,22 @@ -package dev.limebeck.application +package dev.limebeck.application.commands import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.output.MordantHelpFormatter import com.github.ajalt.clikt.parameters.arguments.argument -import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.file -import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.path -import dev.limebeck.application.server.* -import dev.limebeck.application.templates.generatePresentationTemplate -import dev.limebeck.revealkt.RevealkConfig +import dev.limebeck.application.getResourcesList +import dev.limebeck.application.isFont +import dev.limebeck.application.server.logger +import dev.limebeck.application.server.renderLoadResult import dev.limebeck.revealkt.scripts.RevealKtScriptLoader import java.io.File import java.nio.file.Path import kotlin.io.path.* -class RevealKtApplication : CliktCommand( - printHelpOnEmptyArgs = true, - help = """ - RevealKt CLI - - Application version ${RevealkConfig.version} - """.trimIndent() -) { - override fun run() = Unit -} - -class RunServer : CliktCommand(name = "run", help = "Serve presentation with live-reload") { - val port: Int by option(help = "Port").int().default(8080) - val host: String by option(help = "Host").default("0.0.0.0") - val basePath: Path? by option(help = "Script dir").path() - val script: File by argument(help = "Script file").file(canBeDir = false, mustBeReadable = true) - - init { - context { - helpFormatter = { - MordantHelpFormatter( - showDefaultValues = true, - context = it - ) - } - } - } - - override fun run() { - runServer( - Config( - server = ServerConfig(host, port), - basePath = basePath?.pathString ?: script.parent, - script = script - ) - ) - } -} - -class RenderToFile : CliktCommand(name = "render", help = "Render to file") { +class BundleToStatic : CliktCommand(name = "bundle", help = "Bundle to static html file") { val outputDir: Path? by option(help = "Output dir").path( mustBeWritable = true, canBeDir = true, @@ -112,16 +72,4 @@ class RenderToFile : CliktCommand(name = "render", help = "Render to file") { } } } -} - -class InitTemplate : CliktCommand(name = "init", help = "Create presentation template") { - val name: String by argument(help = "Presentation name") - val basePath: Path by option(help = "Template dir") - .path(canBeDir = true, canBeFile = false) - .default(Path.of(".")) - val dirname: String? by option() - - override fun run() { - generatePresentationTemplate(name, basePath / (dirname ?: name)) - } } \ No newline at end of file diff --git a/reveal-kt/app/src/jvmMain/kotlin/commands/InitTemplate.kt b/reveal-kt/app/src/jvmMain/kotlin/commands/InitTemplate.kt new file mode 100644 index 0000000..d07d50e --- /dev/null +++ b/reveal-kt/app/src/jvmMain/kotlin/commands/InitTemplate.kt @@ -0,0 +1,22 @@ +package dev.limebeck.application.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.path +import dev.limebeck.application.templates.generatePresentationTemplate +import java.nio.file.Path +import kotlin.io.path.div + +class InitTemplate : CliktCommand(name = "init", help = "Create new presentation from template") { + val name: String by argument(help = "Presentation name") + val basePath: Path by option(help = "Template dir") + .path(canBeDir = true, canBeFile = false) + .default(Path.of(".")) + val dirname: String? by option() + + override fun run() { + generatePresentationTemplate(name, basePath / (dirname ?: name)) + } +} \ No newline at end of file diff --git a/reveal-kt/app/src/jvmMain/kotlin/commands/RenderPdf.kt b/reveal-kt/app/src/jvmMain/kotlin/commands/RenderPdf.kt new file mode 100644 index 0000000..dc95155 --- /dev/null +++ b/reveal-kt/app/src/jvmMain/kotlin/commands/RenderPdf.kt @@ -0,0 +1,72 @@ +package dev.limebeck.application.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.context +import com.github.ajalt.clikt.output.MordantHelpFormatter +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.defaultLazy +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.file +import com.github.ajalt.clikt.parameters.types.int +import com.github.ajalt.clikt.parameters.types.path +import com.microsoft.playwright.CLI +import dev.limebeck.application.pdf.PdfRenderer +import dev.limebeck.application.server.Config +import dev.limebeck.application.server.ServerConfig +import dev.limebeck.application.server.runServer +import kotlinx.coroutines.runBlocking +import java.io.File +import java.nio.file.Path +import kotlin.concurrent.thread +import kotlin.io.path.pathString + +class RenderPdf : CliktCommand(name = "pdf", help = "Render pdf from presentation") { + val port: Int by option("-p", "--port", help = "Port").int().default(8080) + val host: String by option("-h", "--host", help = "Host").default("0.0.0.0") + val basePath: Path? by option("-b", help = "Script dir").path() + val script: File by argument(help = "Script file").file(canBeDir = false, mustBeReadable = true) + val output: File by option("-o", "--output", help = "Output file") + .file(canBeDir = false, mustBeWritable = true) + .defaultLazy { File("./output.pdf") } + + init { + context { + helpFormatter = { + MordantHelpFormatter( + showDefaultValues = true, + context = it + ) + } + } + } + + val renderer = PdfRenderer() + + override fun run() { + runServer( + Config( + server = ServerConfig(host, port), + basePath = basePath?.pathString ?: script.parent, + script = script + ), + background = false + ) + runBlocking { + val outputData = renderer.render("http://$host:$port/?print-pdf") + output.writeBytes(outputData) + } + } +} + +class InstallChrome : CliktCommand(name = "install-chrome", help = "Install chrome") { + override fun run() { + CLI.main(arrayOf("install", "chromium")) + } +} + +class UninstallChrome : CliktCommand(name = "uninstall-chrome", help = "Uninstall chrome") { + override fun run() { + CLI.main(arrayOf("uninstall")) + } +} diff --git a/reveal-kt/app/src/jvmMain/kotlin/commands/RunServer.kt b/reveal-kt/app/src/jvmMain/kotlin/commands/RunServer.kt new file mode 100644 index 0000000..0db9989 --- /dev/null +++ b/reveal-kt/app/src/jvmMain/kotlin/commands/RunServer.kt @@ -0,0 +1,45 @@ +package dev.limebeck.application.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.context +import com.github.ajalt.clikt.output.MordantHelpFormatter +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.file +import com.github.ajalt.clikt.parameters.types.int +import com.github.ajalt.clikt.parameters.types.path +import dev.limebeck.application.server.Config +import dev.limebeck.application.server.ServerConfig +import dev.limebeck.application.server.runServer +import java.io.File +import java.nio.file.Path +import kotlin.io.path.pathString + +class RunServer : CliktCommand(name = "run", help = "Serve presentation with live-reload") { + val port: Int by option(help = "Port").int().default(8080) + val host: String by option(help = "Host").default("0.0.0.0") + val basePath: Path? by option(help = "Script dir").path() + val script: File by argument(help = "Script file").file(canBeDir = false, mustBeReadable = true) + + init { + context { + helpFormatter = { + MordantHelpFormatter( + showDefaultValues = true, + context = it + ) + } + } + } + + override fun run() { + runServer( + Config( + server = ServerConfig(host, port), + basePath = basePath?.pathString ?: script.parent, + script = script + ) + ) + } +} \ No newline at end of file diff --git a/reveal-kt/app/src/jvmMain/kotlin/pdf/PdfRenderer.kt b/reveal-kt/app/src/jvmMain/kotlin/pdf/PdfRenderer.kt new file mode 100644 index 0000000..b19a652 --- /dev/null +++ b/reveal-kt/app/src/jvmMain/kotlin/pdf/PdfRenderer.kt @@ -0,0 +1,30 @@ +package dev.limebeck.application.pdf + +import com.microsoft.playwright.Page +import com.microsoft.playwright.Playwright + +class PdfRenderer { + suspend fun render(url: String): ByteArray = + Playwright.create( + Playwright + .CreateOptions() + .setEnv( + mapOf("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD" to "1") + ) + ).use { pw -> + val type = pw.chromium() + val browser = type.launch() + val ctx = browser.newContext() + return with(ctx.newPage()) { + navigate(url) + waitForTimeout(2000.0) + pdf( + Page.PdfOptions() + .setPrintBackground(true) + .setPreferCSSPageSize(true) + .setDisplayHeaderFooter(false) + .setLandscape(true) + ) + } + } +} diff --git a/reveal-kt/app/src/jvmMain/kotlin/server/Server.kt b/reveal-kt/app/src/jvmMain/kotlin/server/Server.kt index de6c02c..14dc19c 100644 --- a/reveal-kt/app/src/jvmMain/kotlin/server/Server.kt +++ b/reveal-kt/app/src/jvmMain/kotlin/server/Server.kt @@ -39,7 +39,7 @@ data class ServerConfig( val logger = LoggerFactory.getLogger("ServerLogger") @OptIn(FlowPreview::class) -fun runServer(config: Config) { +fun runServer(config: Config, background: Boolean = true) { println("Starting application...") val startTime = TimeSource.Monotonic.markNow() @@ -156,7 +156,7 @@ fun runServer(config: Config) { """.trimIndent().printToConsole(minRowLength = 60) try { - if (Desktop.isDesktopSupported()) { + if (Desktop.isDesktopSupported() && background) { val desktop = Desktop.getDesktop() desktop.browse(URI.create(url)) } @@ -165,5 +165,5 @@ fun runServer(config: Config) { } } } - }).start(wait = true) + }).start(wait = background) } \ No newline at end of file