Skip to content

Instantly share code, notes, and snippets.

@IRus
Created August 8, 2025 09:32
Show Gist options
  • Save IRus/4d4299e1b401086847b75440ab4d5638 to your computer and use it in GitHub Desktop.
Save IRus/4d4299e1b401086847b75440ab4d5638 to your computer and use it in GitHub Desktop.
Using pandoc from Kotlin
class PandocConverter {
/**
* Convert HTML to PDF using Pandoc
* @param inputHtmlPath Path to the input HTML file
* @param outputPdfPath Path to the output PDF file
* @param options Additional Pandoc options
* @return ConversionResult containing success status and any output/error messages
*/
fun convertHtmlToPdf(
inputHtmlPath: String,
outputPdfPath: String,
options: PandocOptions = PandocOptions(),
): ConversionResult {
// Build the command
val command = buildPandocCommand(inputHtmlPath, outputPdfPath, options)
println(command.joinToString(" "))
return try {
// Create ProcessBuilder
val processBuilder = ProcessBuilder(command)
// Set working directory if specified
options.workingDirectory?.let {
processBuilder.directory(File(it))
}
// Redirect error stream to output stream for easier handling
processBuilder.redirectErrorStream(true)
// Start the process
val process = processBuilder.start()
// Read the output
val output = process.inputStream.bufferedReader().use { it.readText() }
// Wait for the process to complete with timeout
val completed = process.waitFor(options.timeoutSeconds, TimeUnit.SECONDS)
if (!completed) {
process.destroyForcibly()
return ConversionResult(
success = false,
errorMessage = "Process timed out after ${options.timeoutSeconds} seconds",
)
}
val exitCode = process.exitValue()
ConversionResult(
success = exitCode == 0,
output = output,
errorMessage = if (exitCode != 0) "Pandoc exited with code $exitCode" else null,
exitCode = exitCode,
)
} catch (e: IOException) {
ConversionResult(
success = false,
errorMessage = "Failed to execute Pandoc: ${e.message}",
)
} catch (e: InterruptedException) {
ConversionResult(
success = false,
errorMessage = "Process was interrupted: ${e.message}",
)
}
}
/**
* Build the Pandoc command with all options
*/
private fun buildPandocCommand(
inputPath: String,
outputPath: String,
options: PandocOptions,
): List<String> {
val command = mutableListOf<String>()
// Base command
command.add(options.pandocBinary)
// Input file
command.add(inputPath)
// Output file
command.add("-o")
command.add(outputPath)
// PDF engine (wkhtmltopdf, weasyprint, prince, etc.)
options.pdfEngine?.let {
command.add("--pdf-engine=$it")
}
// CSS file for styling
options.cssFile?.let {
command.add("--css=$it")
}
// Page size
options.pageSize?.let {
command.add("-V")
command.add("papersize=$it")
}
// Margins
options.margins?.let { margins ->
margins.top?.let {
command.add("-V")
command.add("margin-top=$it")
}
margins.bottom?.let {
command.add("-V")
command.add("margin-bottom=$it")
}
margins.left?.let {
command.add("-V")
command.add("margin-left=$it")
}
margins.right?.let {
command.add("-V")
command.add("margin-right=$it")
}
}
// Table of contents
if (options.tableOfContents) {
command.add("--toc")
}
// Standalone document
if (options.standalone) {
command.add("--standalone")
}
// Additional custom options
options.customOptions.forEach { command.add(it) }
return command
}
}
/**
* Data class for Pandoc options
*/
data class PandocOptions(
val pandocBinary: String = "/opt/homebrew/bin/pandoc",
val pdfEngine: String? = "/Library/TeX/texbin/pdflatex",
val cssFile: String? = null,
val pageSize: String? = "a4",
val margins: Margins? = Margins(),
val tableOfContents: Boolean = false,
val standalone: Boolean = true,
val workingDirectory: String? = null,
val timeoutSeconds: Long = 60,
val customOptions: List<String> = emptyList(),
)
/**
* Data class for page margins
*/
data class Margins(
val top: String? = "2cm",
val bottom: String? = "2cm",
val left: String? = "2cm",
val right: String? = "2cm",
)
/**
* Data class for conversion result
*/
data class ConversionResult(
val success: Boolean,
val output: String = "",
val errorMessage: String? = null,
val exitCode: Int? = null,
)
/**
* Data class for batch conversion task
*/
data class ConversionTask(
val inputPath: String,
val outputPath: String,
val options: PandocOptions = PandocOptions(),
)
/**
* Example usage and utility functions
*/
fun main() {
val converter = PandocConverter()
// Example 1: Simple conversion with default options
val result1 = converter.convertHtmlToPdf(
inputHtmlPath = "/Users/yoda/Downloads/index.html",
outputPdfPath = "/Users/yoda/Downloads/index-output-1.pdf",
)
if (result1.success) {
println("Conversion successful!")
} else {
println("Conversion failed: ${result1.output}")
}
// Example 2: Conversion with custom options
val customOptions = PandocOptions(
cssFile = "/app/styles/custom.css",
pageSize = "letter",
margins = Margins(
top = "1in",
bottom = "1in",
left = "1.25in",
right = "1.25in",
),
tableOfContents = true,
timeoutSeconds = 120,
)
val result2 = converter.convertHtmlToPdf(
inputHtmlPath = "/Users/yoda/Downloads/index.html",
outputPdfPath = "/Users/yoda/Downloads/index-output-2.pdf",
options = customOptions,
)
if (result2.success) {
println("Conversion successful!")
} else {
println("Conversion failed: ${result2.output}")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment