Skip to content

RikoAppDev/compose-pdf

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

  • Embedded subset Type0/CIDFontType2 (Identity-H) + ToUnicode → selectable & searchable text, including Latin diacritics (composite glyphs subset correctly).
  • Compose-style DSL: text, spacer, divider, row { cell(weight) { } }, column, box(padding, border, background), keyValue(label, value), image / photoGrid (JPEG /DCTDecode pass-through and PNG — decoded in pure Kotlin to a /FlateDecode image with an /SMask for transparency; PhotoFit.Cover / Contain / Smart — smart preserves aspect but crops extreme strips), table (weighted columns, repeating header, total rows, optional zebra striping).
  • Vector images (SVG + Android VectorDrawable): vector(bytes, …) imports both formats (auto-detected) into native, resolution-independent PDF vector paths — the full SVG/VectorDrawable path grammar (incl. elliptical arcs), basic shapes (rect/circle/ellipse/line/poly…), <group>/transform, nonzero & even-odd fill, stroke, per-element opacity, currentColor and the full CSS named-color set. Embedded as a reusable Form XObject, so a logo repeated in a header costs a single object. Pure-Kotlin, dependency-free (no XML library).
  • Header / footer / page numbers: repeating header/footer bands and an auto page-number line whose space is reserved (content never overlaps it). PageConfig controls it all — repeatHeader (every page vs. first page only, like a title block), pageNumberFormat, pageNumberStyle, pageNumbers.
  • Familiar value types: TextStyle (with copy), PdfColor/Color(0xFF…), Dp/.dp, Sp/.sp, FontWeight, TextAlign.
  • Automatic pagination: paragraphs split by line; tables split by row (repeating the header); bordered boxes and columns split across pages with the border/background redrawn per fragment; rows and images stay atomic (never cut). Optional keep-together moves a block whole instead of leaving a sliver.
  • Progress reporting: render(regular, bold, onProgress) calls the optional onProgress: (Float) -> Unit with 0f1f as pages are laid out and serialized — drive a real determinate progress bar. Omit it and output is byte-for-byte unchanged.
  • FlateDecode compression: content streams, the subset font program and the ToUnicode CMap are deflated by a pure-Kotlin encoder (deterministic on every platform).
  • Regular + Bold faces (bundled).
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(3f, "Item"), PdfColumn(1f, "Qty", TextAlign.End))) {
        row("Item A", "3"); row("Item B", "7"); totalRow("Total", "10")
    }
    photoGrid(jpegBytesList, perRow = 3, cellHeight = 80.dp)
}.render(regularFontBytes, boldFontBytes)

Gallery

A set of ready-made example documents ships in commonMain under examples/ExampleDocuments.kt; GalleryExportTest renders each one. Regenerate with ./gradlew :composepdf:jvmTest --tests "*GalleryExportTest". Every entry below links its source code → the exported PDF (the preview image opens the PDF too). Two of them embed a logo via vector() — the field-service report uses an Android VectorDrawable, the catalogue an SVG.

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).

Field service report — repeating header (VectorDrawable badge) + footer + page numbers, bordered record cards with 3-column summaries, totals, photo grid & signatures; 5 pages.
code · PDF
Field service report example
Annual report — title + metric cards, 120-row ledger with a repeating header, zebra striping, periodic subtotals and a grand total; 5 pages.
code · PDF
Annual report example
Product catalogue — SVG brand mark in the header, categorized tables interleaved with photo grids; 3 pages.
code · PDF
Product catalogue example
Service agreement — 16 numbered sections of wrapped paragraphs (keep-together) + a signatures block; 6 pages.
code · PDF
Service agreement example
Invoice — weighted header columns, line-item table, stacked totals.
code · PDF
Invoice example
Business letter — letterhead and automatically wrapped body paragraphs.
code · PDF
Business letter example
Price list — repeating header band, multiple categorized tables.
code · PDF
Price list example
Status report — summary box, metric cards, milestones table.
code · PDF
Status report example
Transaction ledger — 90 rows over 3 pages, repeating table header + page numbers.
code · PDF
Transaction ledger example
Photo gallery — mixed aspect ratios laid out with PhotoFit.Smart / Contain.
code · PDF
Photo gallery example
Résumé — weighted two-column CV, section headings, a skills table; 1 page.
code · PDF
Résumé example
Event program — header band + agenda schedule tables; 1 page.
code · PDF
Event program example

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.

// in androidMain — Android Studio renders androidMain @Preview live
@Preview
@Composable
fun MyReportPreview() = PdfPreview(
    myReport(data),                            // your pdfDocument { … } spec
    previewFontRegular(), previewFontBold(),   // a bundled font, loaded SYNCHRONOUSLY for @Preview
)
  • previewFontRegular() / previewFontBold() load a bundled Noto Sans synchronously — the async Compose-resources API does not work in the IDE preview runtime. (For the real export, pass your own .ttf to render(); the core stays font-agnostic and bundles no font.)
  • Ready-to-open examples ship in the artifact (ExamplePreviews.kt): open it in the IDE and the pane renders the sample documents immediately.
  • The preview runs the same layout pass as render(), so page count, line breaks, tables, boxes, images and vectors land where the PDF puts them; only intra-line glyph advances use the platform font (a faithful approximation — the PDF stays the source of truth).
  • Targets Android + JVM (the platforms with an IDE preview runtime); the core engine remains Android + iOS + JVM. The UI-free PdfDocumentSpec.previewPages(regular, bold) in the core returns the resolved per-page draw model if you want to paint it on a different surface.

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)        // your resource mechanism
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.

Building & testing

./gradlew :composepdf:jvmTest                          # identity + feature gates (incl. cross-platform golden)
./gradlew :composepdf:compileCommonMainKotlinMetadata  # shared-code purity check
./gradlew :composepdf:compileAndroidMain               # Android target
./gradlew :composepdf:iosSimulatorArm64Test            # runs the golden on iOS (macOS only)

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

  • Live @Composable preview bridge — draw the engine's computed glyph/box positions onto a Compose Canvas for an on-screen preview (the PDF stays the source of truth).
  • GPOS kerning / ligatures (v1 uses advance-width shaping).
  • More image formats (JPEG + PNG today; WebP/others later).
  • Complex scripts / RTL / bidi.
  • Emoji & color fonts — the engine subsets a single monochrome outline (glyf) TrueType face, so emoji and color-glyph fonts (COLR/CPAL, CBDT, sbix) are not rendered yet (codepoints with no outline in the supplied face fall back to a missing glyph); needs color-glyph support or an emoji fallback face.
  • Long-word breaking inside narrow columns.

About

Kotlin Multiplatform vector PDF library: identical cross-platform, selectable text, small files, Compose-style DSL, automatic pagination.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Contributors

Languages