22
33A Kotlin Multiplatform PDF rendering and text-extraction library built on top of
44[ bblanchon/pdfium-binaries] ( https://github.com/bblanchon/pdfium-binaries ) and
5- Compose Multiplatform. Zero-copy render pipeline on JVM / Android / iOS (Web
6- posts pixels from a worker), a Compose-first API, and a sample desktop/mobile
7- reader with thumbnails, progressive rendering, and selectable text.
5+ Compose Multiplatform. Zero-copy render pipeline on every target — on the web
6+ the transferred pixel ` ArrayBuffer ` is written straight into Skia's wasm heap,
7+ no intermediate Kotlin ` ByteArray ` . A Compose-first API and a sample
8+ desktop/mobile reader with thumbnails, progressive rendering, and selectable
9+ text round it out.
810
911## Features
1012
1113- ** Compose Multiplatform composables** — drop ` PdfPage ` or ` PdfThumbnail ` into
1214 any Compose UI.
13- - ** Zero-copy rendering** on JVM / Android / iOS: PDFium writes directly into
14- Skia / Android ` Bitmap ` pixel memory.
15+ - ** Zero-copy rendering** on every target. JVM / Android / iOS hand PDFium a
16+ raw pixel pointer into Skia / Android ` Bitmap ` memory. Web allocates the
17+ destination buffer inside Skia's wasm heap via ` Data.makeUninitialized ` and
18+ writes the worker's transferred ` ArrayBuffer ` straight in — no Kotlin
19+ ` ByteArray ` round-trip, no ` installPixels ` second copy.
1520- ** Progressive rendering** (preview → full) with a debounced size flow, so
1621 scroll and zoom feel instant.
1722- ** Two-tier LRU cache** (reader bitmaps + thumbnails) with off-screen prefetch.
@@ -594,10 +599,13 @@ screenH = (top - bottom) × scaleY
594599│ PdfThumbnail ─┘ │
595600│ PdfRenderCache PageTextLayout textClipEntry (expect) │
596601├──────────────────┬───────────────┬─────────────┬─────────────────────────────┤
597- │ jvmMain │ androidMain │ iosMain │ jsMain / wasmJsMain (web) │
598- │ JNI glue │ JNI + NDK │ cinterop │ pdfium.wasm in a Web Worker │
599- │ → Skia Bitmap │ AndroidBitmap │ libpdfium.a │ RPC via postMessage, │
600- │ zero-copy │ zero-copy │ + Skia │ transferable pixels → Skia │
602+ │ jvmMain │ androidMain │ iosMain │ webMain (js + wasmJs) │
603+ │ JNI glue │ JNI + NDK │ cinterop │ pdfium.wasm in a Web │
604+ │ → Skia Bitmap │ AndroidBitmap │ libpdfium.a │ Worker; RPC via │
605+ │ zero-copy │ zero-copy │ + Skia │ postMessage transferables │
606+ │ │ │ │ → Skia heap zero-copy │
607+ │ │ │ │ jsMain / wasmJsMain just │
608+ │ │ │ │ host a small PlatformBridge │
601609└──────────────────┴───────────────┴─────────────┴─────────────────────────────┘
602610```
603611
@@ -613,6 +621,13 @@ Key facts:
613621 ` Bitmap.peekPixels().addr ` and pass it to ` FPDFBitmap_CreateEx ` . PDFium
614622 writes BGRA pixels straight into Skia's bitmap memory. On Android we lock
615623 the ` android.graphics.Bitmap ` via ` AndroidBitmap_lockPixels ` and do the same.
624+ On web, the pdfium worker transfers the pixel ` ArrayBuffer ` to the main
625+ thread; we allocate the destination inside Skia's own wasm heap via
626+ ` Data.makeUninitialized ` and copy the transferred buffer directly there with
627+ a typed-array ` .set() ` on the Skia memory view obtained through
628+ ` org.jetbrains.skiko.wasm.awaitSkiko ` . One memcpy into the final
629+ destination, no ` installPixels ` round-trip. Pattern cribbed from
630+ [ coil3.decode.WebWorker] ( https://github.com/coil-kt/coil/blob/69b8383a3f95300ddb466afdbe9c54ce2eccb652/coil-core/src/jsCommonMain/kotlin/coil3/decode/WebWorker.kt ) .
616631
617632- ** Native binary delivery.** ` pdfium/build.gradle.kts ` registers a set of
618633 Gradle tasks that download the bblanchon archives, extract them, and stage
@@ -707,12 +722,12 @@ if available.
707722 bounding boxes, not glyph positioning from the embedded PDF font. Copied
708723 text is exact, but highlight rectangles can differ slightly from what
709724 Chrome / PDF.js render when they can access the original font metrics.
710- - ** Web: no zero-copy to Skia .** On wasmJs/JS, ` pdfium.wasm ` runs inside a
711- dedicated Web Worker (so the main thread never blocks). Pixels are posted
712- to the main thread via ` postMessage ` transferables and bulk-copied once
713- into a Skia ` Bitmap ` via ` installPixels ` — the only unavoidable copy
714- in the pipeline, since Skia has its own wasm heap with no direct
715- ` ArrayBuffer ` install .
725+ - ** Web: still one memcpy per render .** "Zero-copy" here means "no
726+ intermediate Kotlin ` ByteArray ` , no ` installPixels ` " — the worker's
727+ transferred ` ArrayBuffer ` is written straight into Skia's wasm heap. A true
728+ zero-memcpy pipeline would need ` SharedArrayBuffer ` (which in turn requires
729+ COOP/COEP headers) so the pdfium worker and Skia share one linear memory.
730+ Not currently implemented .
716731- ** Licensing.** PDFium is dual-licensed BSD-3-Clause / Apache-2.0 (see
717732 PDFium's ` LICENSE ` ). bblanchon's binaries carry that license forward. If
718733 you ship this code, include the upstream PDFium notices.
0 commit comments