compose-pdf
A pure-Kotlin Kotlin Multiplatform library that generates vector PDFs whose output is
identical across devices, with selectable/searchable text, small files, automatic
pagination, authored with a Compose-style DSL. Output does not depend on the device
font-scale or the host UI lifecycle.
Coordinates: io.github.rikoappdev:compose-pdf · Targets: Android + iOS + JVM · License: Apache-2.0.
The published artifact bundles no font — you pass your own (see Fonts).
Why a custom engine
To be identical to the dot and vector/searchable at once, no per-platform text engine may
touch layout, and the PDF must contain real text operators with an embedded font (so it can't be
rasterized — and no public Kotlin Multiplatform PDF backend exists for vector output on iOS). So
all layout, text shaping, glyph positioning, TrueType subsetting and PDF serialization run in
shared commonMain integer math.
"Identical" = every glyph's (x,y) origin and the extracted Unicode match across platforms
(engineered to exact integer equality). Raw file bytes may differ (compression/float) — invisible
to users.
Features
val pdf: ByteArray = pdfDocument(PageConfig(margin = 36.dp)) {
header { row { cell(1f) { text("ACME Inc.", TextStyle(fontSize = 10.sp, fontWeight = FontWeight.Bold)) } }; divider() }
text("Report", TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold))
table(columns = listOf(PdfColumn(, ), PdfColumn(, , TextAlign.End))) {
row(, ); row(, ); totalRow(, )
}
photoGrid(jpegBytesList, perRow = , cellHeight = dp)
}.render(regularFontBytes, boldFontBytes)
Gallery
A set of ready-made example documents ships in commonMain under
examples/ExampleDocuments.kt;
renders each one. Regenerate with .
(the preview image opens the PDF too).
Two of them embed a logo via — the field-service report uses an Android ,
the catalogue an .
Every value in these documents is invented, generic placeholder data — the fictitious
Contoso / Northwind / Fabrikam companies and reserved .example addresses. They exist only to
demonstrate the engine (text flow, weighted rows, nested rounded boxes, tables with zebra
striping / totals / repeating headers, automatic pagination, page numbers, vector + image layout).
Live preview (design-time @Preview)
The optional compose-pdf-preview artifact renders a pdfDocument { … } spec onto a Compose
Canvas, reusing the engine's computed layout — so you see your document in the IDE preview
pane as you edit the builder, with no app run and no export. It's a design-time tool (like a
@Preview of a screen), not an in-app "view instead of download" button.
@PdfDocumentPreview
@Composable
fun MyReportPreview() = PdfPreview(
myReport(data),
previewFontRegular(), previewFontBold(),
pageWidth = PreviewPageWidthDp.dp,
)
Fonts
Fonts are supplied by your application, not bundled in the library. render takes the Regular
and Bold face bytes; the engine subsets and embeds only the glyphs a document uses. This keeps the
library font-agnostic and dependency-free, and gives identical output on every platform — the app
reads its own .ttf (via Compose Resources, Android assets, a file, the network, …) and passes the
bytes in.
A typical app keeps one default face and optionally lets the user pick another for export:
val regular: ByteArray = loadFont(selectedFont ?: defaultFont)
val bold: ByteArray = loadFont((selectedFont ?: defaultFont).bold)
val pdf = document.render(regular, bold)
Any TrueType font works. For Latin diacritics (e.g. Czech/Slovak/Polish) pick a face that covers
Latin Extended-A/B, such as Noto Sans or DejaVu Sans.
Color emoji
Pass an optional color-emoji font as a third argument and code points your text faces can't
render are drawn inline as real color bitmaps (the "phone" emoji look) instead of .notdef boxes:
val emoji: ByteArray = loadFont("NotoColorEmoji.ttf")
val pdf = document.render(regular, bold, emoji)
Building & testing
./gradlew :composepdf:jvmTest
./gradlew :composepdf:compileCommonMainKotlinMetadata
./gradlew :composepdf:compileAndroidMain
./gradlew :composepdf:iosSimulatorArm64Test
Requires JDK 17+ (CI uses 21). The cross-platform golden test runs the layout engine over a fixed
document with deterministic metrics and asserts identical integer glyph origins on every platform.
Generated test PDFs/PNGs are written under composepdf/build/.
Roadmap
- GPOS kerning / ligatures.
- More image formats (WebP, …).
- Complex scripts / RTL / bidi.
- Color vector emoji (
COLR/CPAL) and ZWJ / skin-tone sequences.
- Long-word breaking inside narrow columns.