From 4dc99b52049d95ec69335d58d33830d3c529cd22 Mon Sep 17 00:00:00 2001 From: Tamara Cook <10754072+tamaracha@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:31:29 +0200 Subject: [PATCH] Support generating PNG and SVG files --- CHANGELOG.md | 5 ++ README.md | 36 ++++++++++- examples/build.gradle.kts | 8 +++ examples/src/main/typst/document.typ | 4 +- .../gradle/typst/GradleTypstPlugin.kt | 61 +++++++++++++++---- .../extensions/TypstOutputFormatExtension.kt | 37 +++++++++++ .../gradle/typst/extensions/TypstSourceSet.kt | 14 ++++- .../gradle/typst/tasks/TypstCompileTask.kt | 36 ++++++----- 8 files changed, 167 insertions(+), 34 deletions(-) create mode 100644 plugin/src/main/kotlin/de/infolektuell/gradle/typst/extensions/TypstOutputFormatExtension.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index afa9f8d..62bd9e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `localPackages` property in `TypstExtension` which is configured with a platform-dependent convention where local Typst packages are installed. - `packagePath` can be set for `TypstCompileTask` which lets Gradle track changes in local package files and Typst to look for packages in the given directory. This is configured with `localPackages` from the Typst extension by default. +- Typst source set got a format section where the output formats supported by Typst can be enabled and configured. So the documents of a source set can be output in multiple formats at once. + +### Changed + +- `TypstSourceSet.merged` was moved to `TypstSourceSet.format.pdf.merged`. ### Removed diff --git a/README.md b/README.md index a3b06c7..9f4603e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This Gradle plugin offers a way to maintain such projects: ## Features - [x] Compile multiple documents in parallel for faster builds +- [x] Generate all output formats supported by Typst (PDF, PNG, and SVG) - [x] Incremental build: Edit files and rebuild only affected documents - [x] Typst can either be automatically downloaded from GitHub releases, or use a local installation - [x] Define multiple source sets in one project to produce variants of your content, e.g., versions for printing and web publishing @@ -58,8 +59,6 @@ typst.sourceSets { val web by registering { // The files to compile (without .typ extension) documents = listOf("frontmatter", "main", "appendix", "backmatter") - // Setting this creates a merged PDF file from the documents list - merged = "thesis-web-$version" // Values set in this map are passed to Typst as --input options inputs.put("version", version.toString()) } @@ -78,7 +77,7 @@ In a source set folder, these subfolders are watched for changes: - _images_: Image files included in your documents - _typst_: Typst files, can be documents or contain declarations for importing -Running `gradlew build` now will compile all documents. +Running `gradlew build` now will compile all documents into _build/typst//_. ### Shared sources @@ -97,6 +96,37 @@ typst.sourceSets { } ``` +### Output formats + +Currently, Typst can output a document as PDF or as a series of images in PNG or SVG format. +The desired output options can be configured per source set, e.g., PDF for printing and PNG for web publishing. + +```gradle kotlin dsl +typst.sourceSets { + val web by registering { + documents = listOf("frontmatter", "main", "appendix", "backmatter") + format { + // The PNG format is right + png { + enabled = true + // Customized resolution (144 by default) + ppi = 72 + } + // Disable the PDF format which is active by default + pdf.enabled = false + } + } + + val printing by registering { + documents = listOf("frontmatter", "main", "poster", "appendix", "backmatter") + format { + // Setting this creates a merged PDF file from the documents list + pdf.merged = "thesis-$version" + } + } +} +``` + ### Images Image files in _src//images_ are copied to _build/generated/typst/images_. diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 771b7cf..792f67f 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -26,6 +26,14 @@ typst { create("main") { documents.add("document") inputs.put("gitHash", project.version.toString()) + format { + pdf.enabled = true + svg.enabled = true + png { + enabled = true + ppi = 72 + } + } } } creationTimestamp = timestamp.get() diff --git a/examples/src/main/typst/document.typ b/examples/src/main/typst/document.typ index 6f1f5b1..9e78f6a 100644 --- a/examples/src/main/typst/document.typ +++ b/examples/src/main/typst/document.typ @@ -1,5 +1,5 @@ -#let gitHash = sys.inputs.at("gitHash") +#let gitHash = sys.inputs.at("gitHash", default: "") = Test document #gitHash -#lorem(50) +#lorem(5000) diff --git a/plugin/src/main/kotlin/de/infolektuell/gradle/typst/GradleTypstPlugin.kt b/plugin/src/main/kotlin/de/infolektuell/gradle/typst/GradleTypstPlugin.kt index 3db7960..83d2575 100644 --- a/plugin/src/main/kotlin/de/infolektuell/gradle/typst/GradleTypstPlugin.kt +++ b/plugin/src/main/kotlin/de/infolektuell/gradle/typst/GradleTypstPlugin.kt @@ -45,6 +45,12 @@ class GradleTypstPlugin : Plugin { project.layout.projectDirectory.dir(project.providers.environmentVariable("APPDATA")) } extension.localPackages.convention(appDataDir.map { it.dir("typst/packages") }) + extension.sourceSets.configureEach { s -> + s.format.pdf.enabled.convention(true) + s.format.png.enabled.convention(false) + s.format.png.ppi.convention(144) + s.format.svg.enabled.convention(false) + } project.tasks.withType(TypstCompileTask::class.java).configureEach { task -> task.compiler.convention(extension.compiler) task.packagePath.set(extension.localPackages) @@ -70,20 +76,51 @@ class GradleTypstPlugin : Plugin { task.quality.convention(100) } s.images.add(convertImagesTask.flatMap { it.target }) - val typstTask = project.tasks.register("compile${title}Typst", TypstCompileTask::class.java) { task -> - task.onlyIf { s.documents.get().isNotEmpty() } - task.documents.convention(s.documents.map { docs -> docs.map { typstRoot.file("$it.typ") } }) - task.variables.convention(s.inputs) - task.sources.data.convention(s.data) - task.sources.fonts.convention(s.fonts) - task.sources.images.convention(s.images) - task.sources.typst.convention(s.typst) - task.destinationDir.convention(s.destinationDir) - } + val documentFilesProvider = s.documents.map { docs -> docs.map { typstRoot.file("$it.typ") } } + val typstTask = project.tasks.register("compile${title}TypstPdf", TypstCompileTask::class.java) { task -> + val format = s.format.pdf + task.onlyIf { format.enabled.get() } + task.onlyIf { s.documents.get().isNotEmpty() } + task.documents.convention(documentFilesProvider) + task.targetFilenames.convention(s.documents.map { docs -> docs.map { "$it.${format.extension}" } }) + task.variables.convention(s.inputs) + task.sources.data.convention(s.data) + task.sources.fonts.convention(s.fonts) + task.sources.images.convention(s.images) + task.sources.typst.convention(s.typst) + task.destinationDir.convention(s.destinationDir.dir("pdf")) + } project.tasks.register("merge${title}Typst", MergePDFTask::class.java) { task -> - task.onlyIf { s.merged.isPresent } + task.onlyIf { s.format.pdf.merged.isPresent } task.documents.convention(typstTask.flatMap { it.compiled }) - task.merged.convention(s.merged.zip(s.destinationDir) { name, dir -> dir.file("$name.pdf") }) + task.merged.convention(s.format.pdf.merged.zip(s.destinationDir) { name, dir -> dir.file("$name.pdf") }) + } + project.tasks.register("compile${title}TypstPng", TypstCompileTask::class.java) { task -> + val format = s.format.png + task.onlyIf { format.enabled.get() } + task.onlyIf { s.documents.get().isNotEmpty() } + task.documents.convention(documentFilesProvider) + task.targetFilenames.convention(s.documents.map { docs -> docs.map { "$it-{p}-of-{t}.${format.extension}" } }) + task.ppi.convention(format.ppi) + task.variables.convention(s.inputs) + task.sources.data.convention(s.data) + task.sources.fonts.convention(s.fonts) + task.sources.images.convention(s.images) + task.sources.typst.convention(s.typst) + task.destinationDir.convention(s.destinationDir.dir("png")) + } + project.tasks.register("compile${title}TypstSvg", TypstCompileTask::class.java) { task -> + val format = s.format.svg + task.onlyIf { format.enabled.get() } + task.onlyIf { s.documents.get().isNotEmpty() } + task.documents.convention(documentFilesProvider) + task.targetFilenames.convention(s.documents.map { docs -> docs.map { "$it-{p}-of-{t}.${format.extension}" } }) + task.variables.convention(s.inputs) + task.sources.data.convention(s.data) + task.sources.fonts.convention(s.fonts) + task.sources.images.convention(s.images) + task.sources.typst.convention(s.typst) + task.destinationDir.convention(s.destinationDir.dir("svg")) } } val typstCompileTask = project.tasks.register("compileTypst") { task -> diff --git a/plugin/src/main/kotlin/de/infolektuell/gradle/typst/extensions/TypstOutputFormatExtension.kt b/plugin/src/main/kotlin/de/infolektuell/gradle/typst/extensions/TypstOutputFormatExtension.kt new file mode 100644 index 0000000..589b945 --- /dev/null +++ b/plugin/src/main/kotlin/de/infolektuell/gradle/typst/extensions/TypstOutputFormatExtension.kt @@ -0,0 +1,37 @@ +package de.infolektuell.gradle.typst.extensions + +import org.gradle.api.Action +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Nested + +abstract class TypstOutputFormatExtension { + abstract class OutputFormat { + abstract val enabled: Property + } + abstract class PDF : OutputFormat() { + val extension: String get() = "pdf" + abstract val merged: Property + } + abstract class PNG : OutputFormat() { + val extension: String get() = "png" + abstract val ppi: Property + } + abstract class SVG : OutputFormat() { + val extension: String get() = "svg" + } + @get:Nested + abstract val pdf: PDF + fun pdf(action: Action) { + action.execute(pdf) + } + @get:Nested + abstract val png: PNG + fun png(action: Action) { + action.execute(png) + } + @get:Nested + abstract val svg: SVG + fun svg(action: Action) { + action.execute(svg) + } +} diff --git a/plugin/src/main/kotlin/de/infolektuell/gradle/typst/extensions/TypstSourceSet.kt b/plugin/src/main/kotlin/de/infolektuell/gradle/typst/extensions/TypstSourceSet.kt index cb3fee0..cb754cc 100644 --- a/plugin/src/main/kotlin/de/infolektuell/gradle/typst/extensions/TypstSourceSet.kt +++ b/plugin/src/main/kotlin/de/infolektuell/gradle/typst/extensions/TypstSourceSet.kt @@ -1,20 +1,30 @@ package de.infolektuell.gradle.typst.extensions +import org.gradle.api.Action import org.gradle.api.Named import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty -import org.gradle.api.provider.* +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Provider +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Nested abstract class TypstSourceSet : Named { abstract val destinationDir: DirectoryProperty abstract val documents: ListProperty abstract val inputs: MapProperty - abstract val merged: Property abstract val typst: SetProperty abstract val data: SetProperty abstract val images: SetProperty abstract val fonts: SetProperty + @get:Nested + abstract val format: TypstOutputFormatExtension + fun format(action: Action) { + action.execute(format) + } + fun addSourceSet(sourceSet: TypstSourceSet) { typst.addAll(sourceSet.typst) data.addAll(sourceSet.data) diff --git a/plugin/src/main/kotlin/de/infolektuell/gradle/typst/tasks/TypstCompileTask.kt b/plugin/src/main/kotlin/de/infolektuell/gradle/typst/tasks/TypstCompileTask.kt index d0698b8..a9b07c7 100644 --- a/plugin/src/main/kotlin/de/infolektuell/gradle/typst/tasks/TypstCompileTask.kt +++ b/plugin/src/main/kotlin/de/infolektuell/gradle/typst/tasks/TypstCompileTask.kt @@ -33,6 +33,7 @@ abstract class TypstCompileTask @Inject constructor(private val executor: Worker val fontDirectories: ListProperty val creationTimestamp: Property val useSystemFonts: Property + val ppi: Property val target: RegularFileProperty } @@ -46,6 +47,7 @@ abstract class TypstCompileTask @Inject constructor(private val executor: Worker parameters.variables.get().forEach { (k, v) -> action.args("--input", "$k=$v") } if (parameters.creationTimestamp.isPresent) action.args("--creation-timestamp", parameters.creationTimestamp.get()) if (parameters.packagePath.isPresent) action.args("--package-path", parameters.packagePath.asFile.get().absolutePath) + if (parameters.ppi.isPresent) action.args("--ppi", parameters.ppi.get().toString()) action.args(parameters.document.get().asFile.absolutePath) .args(parameters.target.asFile.get().absolutePath) } @@ -59,6 +61,8 @@ abstract class TypstCompileTask @Inject constructor(private val executor: Worker abstract val packagePath: DirectoryProperty @get:InputFiles abstract val documents: ListProperty + @get:Input + abstract val targetFilenames: ListProperty @get:Input abstract val root: Property @get:Input @@ -66,6 +70,9 @@ abstract class TypstCompileTask @Inject constructor(private val executor: Worker @get:Optional @get:Input abstract val creationTimestamp: Property + @get:Optional + @get:Input + abstract val ppi: Property @get:Input abstract val useSystemFonts: Property @get:Nested @@ -73,26 +80,25 @@ abstract class TypstCompileTask @Inject constructor(private val executor: Worker @get:OutputDirectory abstract val destinationDir: DirectoryProperty @get:OutputFiles - val compiled: Provider> = documents.zip(destinationDir) { docs, dest -> - docs.map { dest.file(it.asFile.nameWithoutExtension + ".pdf") } - } + val compiled: Provider> = targetFilenames.zip(destinationDir) { docs, dir -> docs.map { dir.file(it) } } @TaskAction protected fun compile () { val executable = compiler.asFileTree.matching { spec -> spec.include("**/typst", "**/typst.exe") }.singleFile.absolutePath val queue = executor.noIsolation() - documents.get().forEach { document -> - queue.submit(TypstAction::class.java) { params -> - params.executable.set(executable) - params.packagePath.set(packagePath) - params.document.set(document) - params.root.set(root) - params.variables.set(variables) - params.fontDirectories.set(sources.fonts) - params.useSystemFonts.set(useSystemFonts) - params.creationTimestamp.set(creationTimestamp) - params.target.set(destinationDir.file(document.asFile.nameWithoutExtension + ".pdf")) + documents.get().zip(compiled.get()) { document, targetFile -> + queue.submit(TypstAction::class.java) { params -> + params.executable.set(executable) + params.packagePath.set(packagePath) + params.document.set(document) + params.root.set(root) + params.variables.set(variables) + params.fontDirectories.set(sources.fonts) + params.useSystemFonts.set(useSystemFonts) + params.creationTimestamp.set(creationTimestamp) + params.ppi.set(ppi) + params.target.set(targetFile) + } } - } } }