
Single standalone PDF engine: parser, renderer, writer, encryption, fonts, text extraction and true redaction; build, edit, incremental saving, composable viewer and headless rasterizers for thumbnails and exports.
One pure-Kotlin document engine for Kotlin Multiplatform. Read, create, edit and render PDFs, and read reflowable EPUBs, from commonMain, with the exact same code on every target.
Getting started, guides, recipes, and the full API reference. If you read one thing, read this.
KitePDF is a complete PDF engine written from scratch in Kotlin: parser, renderer, writer, editor, encryption, fonts, the whole stack. The same core also drives a full reflowable EPUB 2/3 reader (kitepdf-epub): CSS cascade, embedded fonts including WOFF2, hyphenation, CJK typography and vertical writing, paginated to any page size. You call it all from common code and it runs unchanged on Android, iOS, desktop (JVM), the web (JS / Wasm) and Kotlin/Native. There is no platform PDF engine underneath, no expect/actual, no JNI, no native binary.
// commonMain. Nothing platform-specific. This runs everywhere Kotlin runs.
val doc = PdfDocument.open(bytes)
val pages = doc.pageCount // inspect
val text = doc.pages[0].extractText() // read
// edit in place, then save
val edited = doc.edit().apply {
redactRegion(doc.pages[0], Rectangle(72.0, 700.0, 320.0, 720.0))
}.saveRewritten()
// or build a brand-new document
val fresh = PdfBuilder()
.page { text(StandardFont.Helvetica, 24.0, x = 72.0, y = 720.0, "Hello, world!") }
.build()Almost every other Kotlin / KMP "PDF library" is not really a PDF library. It is a thin expect/actual (or JNI) wrapper around the platform's own engine: PdfRenderer on Android, PDFKit on iOS, PDF.js in the browser, PDFBox on the JVM. You inherit four different engines, four sets of bugs, and four feature sets that never quite line up.
KitePDF is the opposite. It is a single standalone engine, 100% common Kotlin, with zero expect/actual in the core. Write your PDF code once in commonMain and it behaves identically on every target, because it is literally the same code. When something renders wrong, it is one bug in one place, and it is ours to fix.
Rendering to a screen is the only thing that needs a platform. KitePDF keeps that cleanly separate (see Putting pixels on screen), so the engine stays pure and portable.
The engine is one dependency. Add it to commonMain and you have everything except drawing to a screen:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.yuroyami:kitepdf:0.2.0")
}
}
}That is it for reading, writing, editing, redacting, encryption, text extraction and building PDFs, on every Kotlin target. Its only dependency is kotlin-stdlib.
Not using Kotlin Multiplatform? The same artifact works in a plain Android or JVM project. Add io.github.yuroyami:kitepdf:0.2.0 to your normal dependencies { } block.
Drawing a page to the screen is the one job that needs a platform, so the rendering bindings are separate, opt-in artifacts. Add the one that matches how you draw:
| Artifact | Add it when you want |
|---|---|
io.github.yuroyami:kitepdf |
The engine: read / write / edit PDFs and read EPUBs. Pure kotlin-stdlib, every Kotlin target. |
io.github.yuroyami:kitepdf-compose-viewer |
A Compose PdfView / EpubView, drawn straight into a DrawScope. |
io.github.yuroyami:kitepdf-native-renderer |
Headless page → image through the platform canvas (AWT, CoreGraphics, android.graphics, Canvas2D). |
io.github.yuroyami:kitepdf-skia-renderer |
Headless page → image through Skia / Skiko: one API on JVM, Android, Apple, Linux and web. |
Every artifact is at 0.2.0. Want a single format instead of the umbrella? kitepdf-pdf is the PDF handler alone and kitepdf-epub the EPUB reader alone; kitepdf pulls in both. The pixels-on-screen section below shows each binding in use.
Everything below is pure common code from the kitepdf artifact. Each item links to its guide.
PdfView, drawn straight into a Compose DrawScope.A few quick tastes:
// Open a password-protected PDF
val doc = PdfDocument.open(bytes, password = "secret".encodeToByteArray())
require(doc.isAuthenticated)
// Read text
val text = doc.pages[0].extractText()
// Fill a form field and save (append-only)
val out = doc.edit()
.apply { setTextFieldValue(doc.formField("ApplicantName")!!, "Jane Doe") }
.saveIncremental()
// Watermark every page
val stamped = doc.edit().apply {
doc.pages.forEach { page ->
stampPage(page) {
setFillRgb(0.8, 0.1, 0.1)
text(StandardFont.HelveticaBold, 48.0, x = 120.0, y = 400.0, "DRAFT")
}
}
}.saveIncremental()See the full documentation for the complete API and worked examples.
The engine is headless. Showing a PDF is the one job that needs a platform, so it lives in separate, optional artifacts. Pick the one that matches how you draw.
A PDF page is just another composable. PdfView draws into a Compose DrawScope, so it scrolls, zooms and composes with your UI like anything else on a Canvas. No AndroidView, no UIKitView, no embedded web view.
implementation("io.github.yuroyami:kitepdf-compose-viewer:0.2.0")// commonMain of a Compose Multiplatform app
val state = rememberPdfViewState(doc)
PdfView(
state = state,
layout = PdfLayout.Paged(Orientation.Horizontal), // or Continuous / SinglePage
zoomSpec = PdfZoomSpec(maxZoom = 6f), // pinch, double-tap, pan
renderSpec = PdfRenderSpec.Rasterized(), // or Vectorized()
overlay = { PdfNavigationControls(it, Modifier.align(Alignment.BottomCenter)) },
)
// The same state drives widgets anywhere in your UI
PdfPageIndicator(state) // "3 / 12"
PdfThumbnailStrip(state) // tappable thumbnailsSee the viewer guide for layouts, zoom, render modes, navigation and export.
For servers, CI, thumbnails, or non-Compose UIs, render a page straight to image bytes.
implementation("io.github.yuroyami:kitepdf-native-renderer:0.2.0") // or kitepdf-skia-renderer// kitepdf-native-renderer, JVM (AWT). Also CoreGraphics (Apple), android.graphics, Canvas2D (JS).
val awtPng = AwtPdfRasterizer.encodeToPng(doc.pages[0], scale = 2.0)
// kitepdf-skia, one common API via Skiko (JVM, Android, Apple, Linux, web).
val skiaPng = PdfPageRasterizer.encodeToPng(doc.pages[0], scale = 2.0)See the rendering guide to choose between them.
The engine runs on every target Kotlin supports, with no per-platform code. Only the rendering bindings are limited by what their underlying toolkit ships.
| Target | Engine (kitepdf) |
Compose (-compose) |
Native renderer (-native-renderer) |
Skia (-skia) |
|---|---|---|---|---|
| Android | ✓ | ✓ | ✓ | ✓ |
| iOS (arm64 / simulator) | ✓ | ✓ | ✓ | ✓ |
| JVM (Desktop / Server) | ✓ | ✓ | ✓ | ✓ |
| macOS (Apple Silicon) | ✓ | ✓ | ✓ | ✓ |
| JS (Browser / Node) | ✓ | ✓ | ✓ | ✓ |
| wasmJs | ✓ | ✓ | – | ✓ |
| tvOS | ✓ | – | ✓ | ✓ |
| watchOS | ✓ | – | – | – |
| Linux (x64 / arm64) | ✓ | – | – | ✓ |
| Windows (mingwX64) | ✓ | – | – | – |
| Android Native, WASI | ✓ | – | – | – |
Full details and the reasons behind each gap are in the platform support guide.
KitePDF is pre-1.0 and actively developed.
Working today: reading and text extraction, metadata, outlines, annotations, form fields, encrypted documents, the Compose viewer (continuous / paged scroll, pinch zoom), headless rendering, editing and saving, true redaction, and building PDFs from scratch.
On the way: digital signatures, the JBIG2 and JPEG 2000 image codecs, less common form widgets, and advanced colour management.
If a PDF renders incorrectly, please open an issue with the file attached. The project ships a pixel-diff harness against MuPDF, and every fix lands as a regression test.
The sample/ module is a runnable Compose Multiplatform app that opens a PDF and exercises the API across Android, iOS and Desktop. Use it as a copy-paste starting point.
Apache License 2.0. A few encoding tables and font data are derived from MuPDF and keep their AGPL-3.0 headers in those specific source files; see the comments there.
Architectural reference: MuPDF by Artifex Software. Standard 14 font metrics derive from URW++ AFM files. Thanks to everyone who published the PDF specification and the tools that made this possible.
One pure-Kotlin document engine for Kotlin Multiplatform. Read, create, edit and render PDFs, and read reflowable EPUBs, from commonMain, with the exact same code on every target.
Getting started, guides, recipes, and the full API reference. If you read one thing, read this.
KitePDF is a complete PDF engine written from scratch in Kotlin: parser, renderer, writer, editor, encryption, fonts, the whole stack. The same core also drives a full reflowable EPUB 2/3 reader (kitepdf-epub): CSS cascade, embedded fonts including WOFF2, hyphenation, CJK typography and vertical writing, paginated to any page size. You call it all from common code and it runs unchanged on Android, iOS, desktop (JVM), the web (JS / Wasm) and Kotlin/Native. There is no platform PDF engine underneath, no expect/actual, no JNI, no native binary.
// commonMain. Nothing platform-specific. This runs everywhere Kotlin runs.
val doc = PdfDocument.open(bytes)
val pages = doc.pageCount // inspect
val text = doc.pages[0].extractText() // read
// edit in place, then save
val edited = doc.edit().apply {
redactRegion(doc.pages[0], Rectangle(72.0, 700.0, 320.0, 720.0))
}.saveRewritten()
// or build a brand-new document
val fresh = PdfBuilder()
.page { text(StandardFont.Helvetica, 24.0, x = 72.0, y = 720.0, "Hello, world!") }
.build()Almost every other Kotlin / KMP "PDF library" is not really a PDF library. It is a thin expect/actual (or JNI) wrapper around the platform's own engine: PdfRenderer on Android, PDFKit on iOS, PDF.js in the browser, PDFBox on the JVM. You inherit four different engines, four sets of bugs, and four feature sets that never quite line up.
KitePDF is the opposite. It is a single standalone engine, 100% common Kotlin, with zero expect/actual in the core. Write your PDF code once in commonMain and it behaves identically on every target, because it is literally the same code. When something renders wrong, it is one bug in one place, and it is ours to fix.
Rendering to a screen is the only thing that needs a platform. KitePDF keeps that cleanly separate (see Putting pixels on screen), so the engine stays pure and portable.
The engine is one dependency. Add it to commonMain and you have everything except drawing to a screen:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.yuroyami:kitepdf:0.2.0")
}
}
}That is it for reading, writing, editing, redacting, encryption, text extraction and building PDFs, on every Kotlin target. Its only dependency is kotlin-stdlib.
Not using Kotlin Multiplatform? The same artifact works in a plain Android or JVM project. Add io.github.yuroyami:kitepdf:0.2.0 to your normal dependencies { } block.
Drawing a page to the screen is the one job that needs a platform, so the rendering bindings are separate, opt-in artifacts. Add the one that matches how you draw:
| Artifact | Add it when you want |
|---|---|
io.github.yuroyami:kitepdf |
The engine: read / write / edit PDFs and read EPUBs. Pure kotlin-stdlib, every Kotlin target. |
io.github.yuroyami:kitepdf-compose-viewer |
A Compose PdfView / EpubView, drawn straight into a DrawScope. |
io.github.yuroyami:kitepdf-native-renderer |
Headless page → image through the platform canvas (AWT, CoreGraphics, android.graphics, Canvas2D). |
io.github.yuroyami:kitepdf-skia-renderer |
Headless page → image through Skia / Skiko: one API on JVM, Android, Apple, Linux and web. |
Every artifact is at 0.2.0. Want a single format instead of the umbrella? kitepdf-pdf is the PDF handler alone and kitepdf-epub the EPUB reader alone; kitepdf pulls in both. The pixels-on-screen section below shows each binding in use.
Everything below is pure common code from the kitepdf artifact. Each item links to its guide.
PdfView, drawn straight into a Compose DrawScope.A few quick tastes:
// Open a password-protected PDF
val doc = PdfDocument.open(bytes, password = "secret".encodeToByteArray())
require(doc.isAuthenticated)
// Read text
val text = doc.pages[0].extractText()
// Fill a form field and save (append-only)
val out = doc.edit()
.apply { setTextFieldValue(doc.formField("ApplicantName")!!, "Jane Doe") }
.saveIncremental()
// Watermark every page
val stamped = doc.edit().apply {
doc.pages.forEach { page ->
stampPage(page) {
setFillRgb(0.8, 0.1, 0.1)
text(StandardFont.HelveticaBold, 48.0, x = 120.0, y = 400.0, "DRAFT")
}
}
}.saveIncremental()See the full documentation for the complete API and worked examples.
The engine is headless. Showing a PDF is the one job that needs a platform, so it lives in separate, optional artifacts. Pick the one that matches how you draw.
A PDF page is just another composable. PdfView draws into a Compose DrawScope, so it scrolls, zooms and composes with your UI like anything else on a Canvas. No AndroidView, no UIKitView, no embedded web view.
implementation("io.github.yuroyami:kitepdf-compose-viewer:0.2.0")// commonMain of a Compose Multiplatform app
val state = rememberPdfViewState(doc)
PdfView(
state = state,
layout = PdfLayout.Paged(Orientation.Horizontal), // or Continuous / SinglePage
zoomSpec = PdfZoomSpec(maxZoom = 6f), // pinch, double-tap, pan
renderSpec = PdfRenderSpec.Rasterized(), // or Vectorized()
overlay = { PdfNavigationControls(it, Modifier.align(Alignment.BottomCenter)) },
)
// The same state drives widgets anywhere in your UI
PdfPageIndicator(state) // "3 / 12"
PdfThumbnailStrip(state) // tappable thumbnailsSee the viewer guide for layouts, zoom, render modes, navigation and export.
For servers, CI, thumbnails, or non-Compose UIs, render a page straight to image bytes.
implementation("io.github.yuroyami:kitepdf-native-renderer:0.2.0") // or kitepdf-skia-renderer// kitepdf-native-renderer, JVM (AWT). Also CoreGraphics (Apple), android.graphics, Canvas2D (JS).
val awtPng = AwtPdfRasterizer.encodeToPng(doc.pages[0], scale = 2.0)
// kitepdf-skia, one common API via Skiko (JVM, Android, Apple, Linux, web).
val skiaPng = PdfPageRasterizer.encodeToPng(doc.pages[0], scale = 2.0)See the rendering guide to choose between them.
The engine runs on every target Kotlin supports, with no per-platform code. Only the rendering bindings are limited by what their underlying toolkit ships.
| Target | Engine (kitepdf) |
Compose (-compose) |
Native renderer (-native-renderer) |
Skia (-skia) |
|---|---|---|---|---|
| Android | ✓ | ✓ | ✓ | ✓ |
| iOS (arm64 / simulator) | ✓ | ✓ | ✓ | ✓ |
| JVM (Desktop / Server) | ✓ | ✓ | ✓ | ✓ |
| macOS (Apple Silicon) | ✓ | ✓ | ✓ | ✓ |
| JS (Browser / Node) | ✓ | ✓ | ✓ | ✓ |
| wasmJs | ✓ | ✓ | – | ✓ |
| tvOS | ✓ | – | ✓ | ✓ |
| watchOS | ✓ | – | – | – |
| Linux (x64 / arm64) | ✓ | – | – | ✓ |
| Windows (mingwX64) | ✓ | – | – | – |
| Android Native, WASI | ✓ | – | – | – |
Full details and the reasons behind each gap are in the platform support guide.
KitePDF is pre-1.0 and actively developed.
Working today: reading and text extraction, metadata, outlines, annotations, form fields, encrypted documents, the Compose viewer (continuous / paged scroll, pinch zoom), headless rendering, editing and saving, true redaction, and building PDFs from scratch.
On the way: digital signatures, the JBIG2 and JPEG 2000 image codecs, less common form widgets, and advanced colour management.
If a PDF renders incorrectly, please open an issue with the file attached. The project ships a pixel-diff harness against MuPDF, and every fix lands as a regression test.
The sample/ module is a runnable Compose Multiplatform app that opens a PDF and exercises the API across Android, iOS and Desktop. Use it as a copy-paste starting point.
Apache License 2.0. A few encoding tables and font data are derived from MuPDF and keep their AGPL-3.0 headers in those specific source files; see the comments there.
Architectural reference: MuPDF by Artifex Software. Standard 14 font metrics derive from URW++ AFM files. Thanks to everyone who published the PDF specification and the tools that made this possible.