Architecture¶
PdfKmp is built around a single closed (sealed) PdfNode tree authored via a
Compose-style DSL. Layout runs in common code; rendering is dispatched to a
platform-specific PdfCanvas.
The pipeline¶
pdf { … } authoring DSL (commonMain/dsl)
│
▼
PdfNode tree sealed node hierarchy (commonMain/node)
│
▼
measure(...) → MeasuredNode layout engine (commonMain/layout/LayoutEngine.kt)
│
▼
DocumentRenderer.place(...) page placement + pagination (commonMain/render)
│
▼
PdfCanvas draw calls one implementation per platform
Every DSL node maps 1:1 to a MeasuredNode produced by the layout engine, then
to draw calls on PdfCanvas. A new node type must update all three layers. The
renderer does a counting dry-run pass before the real pass so PageContext's
totalPages (and tableOfContents() page numbers) are exact.
pdf vs pdfAsync — the preflight pass¶
The synchronous pdf { } entry point walks the tree and renders straight away.
pdfAsync { } inserts one extra step in front of measure(...): a suspend
preflight that calls DocumentSpec.resolveDeferred(), replacing every
LazyNode (the deferred placeholders emitted by the typed Res.drawable.*
overloads) with a concrete node by running each one's suspend resource loader.
After resolving, pdfAsync re-collects custom fonts (a TextNode produced by a
resolver wasn't visible during the original DSL walk) before handing the finished
spec to the renderer. A LazyNode that reaches the renderer through synchronous
pdf { } throws — the error points the caller back at pdfAsync.
PdfDriverFactory — the expect/actual backend seam¶
Which backend encodes a document is chosen by defaultPdfDriverFactory(), an
expect fun with one actual per platform (Android / iOS / JVM / wasmJs). Both
pdf and pdfAsync take a factory: PdfDriverFactory = defaultPdfDriverFactory()
parameter, so tests (and consumers wanting a custom backend) can pass
FakePdfDriver or any other factory without touching the DSL. The factory's
create(metadata, customFonts) returns the platform PdfDriver that produces the
PdfCanvas the renderer draws onto.
Platform canvases¶
| Backend | Implementation | Notes |
|---|---|---|
| Android | AndroidPdfCanvas |
android.graphics.pdf.PdfDocument + Canvas. A pure-Kotlin post-processor adds the info dict, link/GoTo annotations, and outline in finish(). |
| iOS | IosPdfCanvas |
UIGraphicsBeginPDFContextToData + Core Graphics. |
| Desktop (JVM) | JvmPdfCanvas (JvmPdfDriver) |
Apache PDFBox; subset TrueType fonts, axial/radial shadings, real link annotations + info dict. PDFBox lives only in jvmMain. |
| Web (Wasm) | KmpPdfCanvas / KmpPdfDriver |
The kmpwriter pure-Kotlin PDF 1.7 writer (in commonMain), so the wasm target needs no platform PDF engine. |
| Test | FakePdfCanvas (FakePdfDriver) |
Records every draw call as a sealed DrawCall in commonTest so the whole pipeline runs without native APIs. |
Every text glyph and shape is emitted as a vector path — no rasterisation — across all backends.
The kmpwriter (web backend)¶
Because browsers expose no PDF engine to Wasm, PdfKmp ships its own pure-Kotlin
PDF writer in commonMain (com.conamobile.pdfkmp.kmpwriter): page/object
writers, WinAnsi text encoding with Standard-14 Helvetica metrics, an inflate/
deflate stream codec, axial/radial shading, JPEG + 8-bit PNG embedding, and a
navigation (links / destinations / outline) writer. The identical writer is
validated on the JVM by re-parsing and rasterising its output with PDFBox. See
Web (Kotlin/Wasm).
Recording driver¶
RecordingPdfDriver is a transparent decorator around any platform PdfDriver:
it snapshots every drawText and linkAnnotation (with page index) into
PdfDocument.textRuns / PdfDocument.hyperlinks so the viewer
can layer text selection / clickable overlays without re-parsing the bytes.
Output bytes are byte-for-byte identical to a non-recording render.
Module map¶
| Module | What it is |
|---|---|
:pdfkmp |
Core KMP library — Android aar + iOS framework PdfKmp + JVM jar + wasmJs klib. Compose-free. Publishable. |
:pdfkmp-compose-resources |
Opt-in bridge mapping Compose DrawableResource references onto the DSL (toVectorImage(), toBytes(), the pdfAsync overloads). Publishable. |
:pdfkmp-markdown |
Opt-in CommonMark-lite renderer (markdown(text, theme)). Publishable. |
:pdfkmp-viewer |
Opt-in Compose Multiplatform PdfViewer / KmpPdfLauncher. Android / iOS / Desktop (not web). Publishable. |
:sample-shared |
KMP library (Android target) holding the Compose-Resources slice of the Android sample. Not published. |
:sample |
Plain com.android.application Compose Android sample. |
iosApp/ |
SwiftUI / PDFKit iOS sample. |
:sample-desktop |
Compose for Desktop sample (master/detail + live playground). |
Where things live (library)¶
| What | Where |
|---|---|
DSL entry points (pdf { }, column, row, text, …) |
pdfkmp/src/commonMain/.../dsl/ |
Sealed node hierarchy (PdfNode, TextNode, …) |
pdfkmp/src/commonMain/.../node/ |
Layout engine (measure(...) → MeasuredNode) |
pdfkmp/src/commonMain/.../layout/LayoutEngine.kt |
| Renderer / page placement | pdfkmp/src/commonMain/.../render/DocumentRenderer.kt |
| Charts DSL | pdfkmp/src/commonMain/.../dsl/Charts.kt |
Document-level scope (encryption, attachment, pdfA, metadata) |
pdfkmp/src/commonMain/.../dsl/DocumentScope.kt |
| Android canvas | pdfkmp/src/androidMain/.../render/AndroidPdfCanvas.kt |
| iOS canvas | pdfkmp/src/iosMain/.../render/IosPdfCanvas.kt |
| JVM canvas (+ driver, font registry, shading) | pdfkmp/src/jvmMain/.../render/JvmPdfCanvas.kt |
Web writer (kmpwriter) |
pdfkmp/src/commonMain/.../kmpwriter/ |
| Test backend | pdfkmp/src/commonTest/.../test/FakePdfBackend.kt |
| Worked-example documents | pdfkmp/src/commonMain/.../samples/Samples.kt |
| Smoke tests for samples | pdfkmp/src/commonTest/.../samples/SamplesSmokeTest.kt |
Conventions¶
- Coordinates are in PDF points with a top-left origin (Y grows downward). Each backend translates to its native convention internally.
explicitApi()is on for:pdfkmp— every declaration ispublicorinternal.- Base package:
com.conamobile.pdfkmp.