Skip to content

Getting started

Repository setup

PdfKmp is published to Maven Central. Make sure it's in your repository list:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}

Artifact coordinates

All publishable modules share the same version and are released in lock-step.

Artifact Coordinate What it is
Core (KMP) io.github.conamobiledev:pdfkmp The DSL + layout + renderer. Compose-free.
Compose Resources io.github.conamobiledev:pdfkmp-compose-resources Bridges Res.drawable.* into the DSL (opt-in).
Markdown io.github.conamobiledev:pdfkmp-markdown Renders CommonMark-lite through the DSL (opt-in).
Viewer io.github.conamobiledev:pdfkmp-viewer Compose Multiplatform PDF viewer screen (opt-in, not on web).

Platform-specific variants (-android, -jvm, -wasm-js) exist for plain non-KMP projects. KMP projects depend on the base coordinate only — Gradle resolves the right variant automatically for every target.

Paths are mutually exclusive

KMP projects depend on pdfkmp only (including for Android and Desktop targets). Plain Android projects use pdfkmp-android; plain JVM/Desktop projects use pdfkmp-jvm. Do not add both the base and the platform artifact.

Kotlin Multiplatform

# gradle/libs.versions.toml
[versions]
pdfkmp = "1.2.0"

[libraries]
pdfkmp = { module = "io.github.conamobiledev:pdfkmp", version.ref = "pdfkmp" }
pdfkmp-compose-resources = { module = "io.github.conamobiledev:pdfkmp-compose-resources", version.ref = "pdfkmp" } # optional
pdfkmp-viewer = { module = "io.github.conamobiledev:pdfkmp-viewer", version.ref = "pdfkmp" } # optional
// build.gradle.kts
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(libs.pdfkmp)
            implementation(libs.pdfkmp.compose.resources) // optional
            implementation(libs.pdfkmp.viewer)            // optional
        }
    }
}

Android-only

// app/build.gradle.kts
dependencies {
    implementation("io.github.conamobiledev:pdfkmp-android:1.2.0")
    implementation("io.github.conamobiledev:pdfkmp-viewer:1.2.0") // optional
}

Desktop / JVM-only

// build.gradle.kts
dependencies {
    implementation("io.github.conamobiledev:pdfkmp-jvm:1.2.0")
    implementation("io.github.conamobiledev:pdfkmp-viewer-jvm:1.2.0") // optional
}

The Desktop backend is built on Apache PDFBox (pulled in transitively) and is pure-Java, so the same artifact runs on macOS, Windows, and Linux with no native libraries to bundle.

Requirements

  • JDK 17+
  • Android Gradle Plugin 8.x, compileSdk 34+ (Android targets)
  • Xcode 16+ when targeting iOS via Kotlin Multiplatform
  • A desktop JRE 17+ when targeting Desktop / JVM

R8 / ProGuard

Fully supported — no additional keep rules required.

Hello world

import com.conamobile.pdfkmp.pdf
import com.conamobile.pdfkmp.storage.StorageLocation
import com.conamobile.pdfkmp.storage.save
import com.conamobile.pdfkmp.style.PdfColor
import com.conamobile.pdfkmp.unit.sp

val document = pdf {
    metadata { title = "Hello, PdfKmp" }
    page {
        text("Hello, world!") {
            fontSize = 24.sp
            bold = true
            color = PdfColor.Blue
        }
    }
}

val bytes: ByteArray = document.toByteArray()              // raw bytes
val saved = document.save(StorageLocation.Cache, "hello.pdf")  // returns SavedPdf (synchronous)
println(saved.path)  // absolute filesystem path you can hand to a viewer / share intent

pdf {} vs pdfAsync {}

pdfAsync is pdf with a one-shot suspend preflight pass tacked onto the front. The DSL inside is identical — only the top-level entry differs.

pdf { } pdfAsync { }
Function shape non-suspend suspend
drawable / vector / image (Res.drawable.X) throws at render time works
All other DSL features identical identical
When to reach for it document is built from in-memory bytes or pre-parsed VectorImages the tree contains one or more Res.drawable.* references

Reach for pdfAsync only when you use the typed Res.drawable.* overloads from the Compose Resources companion. Existing pdf { } code keeps working with zero changes.

suspend fun buildReport(): PdfDocument = pdfAsync {
    page {
        drawable(Res.drawable.logo, width = 64.dp, tint = PdfColor.Blue)
    }
}

Save & share per platform

save(...) is a regular (non-suspend) function returning a SavedPdf with path (the absolute filesystem path) and uri (the Android MediaStore content URI on public writes, null elsewhere). Typed storage locations resolve inside the platform sandbox.

val saved = doc.save(StorageLocation.Downloads, filename = "report.pdf")
println(saved.path)     // absolute path

Display with the system PdfRenderer, or share via a FileProvider + Intent.ACTION_SEND. The simplest path is the viewer: KmpPdfLauncher.open(saved.path, title = "Invoice").

PDFKit.PDFView displays the document as vector. Share via UIActivityViewController, or hand bytes to UIKit with doc.toNSData().

Locations resolve against the user's home (~/Downloads, ~/Documents). Open the PDF in the OS default handler via java.awt.Desktop.open.

Browsers ship excellent PDF viewers — hand them the bytes:

doc.openInNewTab()                                  // browser's own viewer
doc.save(StorageLocation.Downloads, "hello.pdf")    // browser download

Available storage locations

Location Android iOS Desktop (JVM) Use case
Cache app cache Library/Caches/ java.io.tmpdir temporary preview
AppFiles app files Library/Application Support/ ~/.pdfkmp persistent, app-private
AppExternalFiles scoped external app Documents/ ~/.pdfkmp larger app-scoped files
Downloads shared via MediaStore app Documents/ (no public Downloads) ~/Downloads user-visible
Documents shared Documents/ app Documents/ ~/Documents user documents
Temp cacheDir/tmp/ NSTemporaryDirectory() java.io.tmpdir one-shot temp
Custom("/abs/path") any writable dir any writable dir any writable dir full control

Android permissions

Cache, AppFiles, AppExternalFiles, Temp, and the MediaStore-backed Downloads / Documents (Android 10+) need no permission. For Custom paths outside your sandbox you supply your own permission handling.

Troubleshooting

PdfKmp recovers gracefully (and silently) from a few conditions — an undecodable image, a glyph missing from the active font, an unsupported feature on a given backend. If a generated PDF doesn't look the way you expect, install a PdfLog logger to surface those swallowed warnings:

import com.conamobile.pdfkmp.PdfLog

PdfLog.logger = { message -> println("PdfKmp: $message") }

See Diagnostics — PdfLog for details.

Where next