From b14e03c944ac4641eba28c65300ecdc8ebdce96c Mon Sep 17 00:00:00 2001 From: Nadeem Iqbal Date: Sun, 17 May 2026 19:01:47 +0500 Subject: [PATCH] fix: render form widgets and signature appearances via FPDF_FFLDraw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PDFium requires a two-pass render for pages with form widgets: FPDF_RenderPageBitmap draws static content, then FPDF_FFLDraw overlays interactive widget annotations (fillable form fields and digital signature appearances). ComposePdfReader only ran the first pass, so signed PDFs showed blank rectangles where signature widgets should be. The FPDF_ANNOT flag (already passed for RenderQuality.FULL) is insufficient on its own — per PDFium's API contract, widget annotations are *always* rendered via FPDF_FFLDraw regardless of that flag. Per-platform changes follow the same pattern: initialize a FPDF_FORMHANDLE once at openPdfDocument(), keep it for the document's lifetime, call the form-fill overlay sequence (FORM_OnAfterLoadPage → FPDF_FFLDraw → FORM_OnBeforeClosePage) after each FPDF_RenderPageBitmap when quality is FULL, and tear it down with FPDFDOC_ExitFormFillEnvironment before FPDF_CloseDocument in close(). PREVIEW renders (used for thumbnails) skip the extra pass to keep them cheap. The FPDF_FORMFILLINFO struct is zero-initialised with version=2 and null callbacks — sufficient for read-only static rendering. No interactive form-fill behaviour (mouse, keyboard, JavaScript actions) is wired up, matching the existing read-only display contract of the library. Implementation surface area: - JVM/Android: new nInitFormEnv/nCloseFormEnv JNI exports; render JNI functions take an extra form-handle parameter (0 = skip overlay). - iOS: cinterop .def now includes fpdf_formfill.h; the actual class stores FPDF_FORMHANDLE + the backing FPDF_FORMFILLINFO allocation in nativeHeap and frees both in close(). - Web: pdfium_worker.mjs keeps a per-document form handle in formByDoc/formInfoByDoc maps; renderPage gates FPDF_FFLDraw on the FPDF_ANNOT flag so PREVIEW thumbnails don't pay the cost. bblanchon's prebuilt PDFium binaries already export the form-fill symbols (verified via nm/grep on linux .so, mac .dylib, and pdfium.wasm glue), so no native-binary rebuild is needed. Verified: - :pdfium:smokeTest passes on a non-form PDF (64-parallel render+text+size stress) — form-fill init/teardown is correct for documents with no widgets. - All target Kotlin compilations succeed (jvm, android, iosArm64, iosSimulatorArm64, wasmJs, js). - Native builds succeed: JNI .dylib for darwin-aarch64/x86_64; Android NDK .so for arm64-v8a/armeabi-v7a/x86/x86_64. Closes #5. --- pdfium/src/androidMain/cpp/pdfium_android.cpp | 14 +++-- .../pdfium/PdfDocument.android.kt | 11 +++- .../pdfium/jvm/PdfiumBridge.android.kt | 11 +++- .../nucleusframework/pdfium/RenderQuality.kt | 7 ++- .../pdfium/PdfDocument.ios.kt | 32 +++++++++++- .../pdfium/PdfDocument.jvm.kt | 15 +++++- .../pdfium/jvm/PdfiumBridge.kt | 15 +++++- pdfium/src/jvmMain/native/pdfium_jni.cpp | 52 +++++++++++++++++-- pdfium/src/nativeInterop/cinterop/pdfium.def | 2 +- .../resources/pdfium/pdfium_worker.mjs | 45 ++++++++++++++++ 10 files changed, 190 insertions(+), 14 deletions(-) diff --git a/pdfium/src/androidMain/cpp/pdfium_android.cpp b/pdfium/src/androidMain/cpp/pdfium_android.cpp index 2405e8e..e1c2a01 100644 --- a/pdfium/src/androidMain/cpp/pdfium_android.cpp +++ b/pdfium/src/androidMain/cpp/pdfium_android.cpp @@ -9,6 +9,7 @@ #include #include "fpdfview.h" +#include "fpdf_formfill.h" #define LOG_TAG "pdfiumjni" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) @@ -17,7 +18,7 @@ extern "C" { JNIEXPORT jboolean JNICALL Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPageToBitmap( - JNIEnv* env, jclass, jlong page, jobject bitmap, + JNIEnv* env, jclass, jlong page, jlong form, jobject bitmap, jint width, jint height, jint flags) { if (page == 0 || bitmap == nullptr) return JNI_FALSE; @@ -48,8 +49,15 @@ Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPageToBitmap( return JNI_FALSE; } FPDFBitmap_FillRect(bmp, 0, 0, width, height, 0xFFFFFFFF); - FPDF_RenderPageBitmap(bmp, reinterpret_cast(page), - 0, 0, width, height, 0, flags | FPDF_REVERSE_BYTE_ORDER); + const int renderFlags = flags | FPDF_REVERSE_BYTE_ORDER; + FPDF_PAGE p = reinterpret_cast(page); + FPDF_RenderPageBitmap(bmp, p, 0, 0, width, height, 0, renderFlags); + if (form != 0) { + FPDF_FORMHANDLE fh = reinterpret_cast(form); + FORM_OnAfterLoadPage(p, fh); + FPDF_FFLDraw(fh, bmp, p, 0, 0, width, height, 0, renderFlags); + FORM_OnBeforeClosePage(p, fh); + } FPDFBitmap_Destroy(bmp); AndroidBitmap_unlockPixels(env, bitmap); diff --git a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt index e1dad46..abdfe65 100644 --- a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt +++ b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt @@ -16,6 +16,9 @@ internal actual class PdfDocument internal constructor( private val bufferAddr: Long, private val bufferSize: Int, private val handles: LongArray, + // Per-document FPDF_FORMHANDLE (parallel to [handles]). May be 0 if PDFium refused; + // render code falls back to no widget overlay in that case. + private val formHandles: LongArray, private val dispatchers: Array, private val executors: Array, ) { @@ -64,6 +67,9 @@ internal actual class PdfDocument internal constructor( try { val ok = PdfiumBridge.nRenderPageToBitmap( page = page, + // PREVIEW skips form-fill to keep thumbnails cheap; FULL passes the form + // handle so signatures + interactive widgets render correctly. + form = if (quality == RenderQuality.FULL) formHandles[slot] else 0L, bitmap = bitmap, width = widthPx, height = heightPx, @@ -139,6 +145,8 @@ internal actual class PdfDocument internal constructor( runBlocking { for (i in handles.indices) { withContext(dispatchers[i]) { + // Form-fill env must be torn down BEFORE its underlying document. + PdfiumBridge.nCloseFormEnv(formHandles[i]) PdfiumBridge.nCloseDocument(handles[i]) } } @@ -175,7 +183,8 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?) val pairs = Array(POOL_SIZE) { Pdfium.newDispatcher() } val dispatchers = Array(POOL_SIZE) { pairs[it].first } val executors = Array(POOL_SIZE) { pairs[it].second } - PdfDocument(bufferAddr, bytes.size, handles, dispatchers, executors) + val formHandles = LongArray(POOL_SIZE) { PdfiumBridge.nInitFormEnv(handles[it]) } + PdfDocument(bufferAddr, bytes.size, handles, formHandles, dispatchers, executors) } catch (t: Throwable) { PdfiumBridge.nFreeBuffer(bufferAddr) throw t diff --git a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.android.kt b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.android.kt index ed0e5c6..5ca16a6 100644 --- a/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.android.kt +++ b/pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.android.kt @@ -28,14 +28,23 @@ internal object PdfiumBridge { height: Int, swapRedBlue: Boolean, ): Boolean - /** Android-only zero-copy render: locks the Bitmap's pixels and writes directly. */ + /** + * Android-only zero-copy render: locks the Bitmap's pixels and writes directly. + * [form] is an optional FPDF_FORMHANDLE from [nInitFormEnv] — pass 0 to skip widget overlay; + * non-zero enables FPDF_FFLDraw rendering of form fields and signature appearances. + */ @JvmStatic external fun nRenderPageToBitmap( page: Long, + form: Long, bitmap: android.graphics.Bitmap, width: Int, height: Int, flags: Int, ): Boolean + /** Init form-fill environment for [doc]. Returns 0 on failure. */ + @JvmStatic external fun nInitFormEnv(doc: Long): Long + /** Tear down a form handle. Must run before the underlying document is closed. */ + @JvmStatic external fun nCloseFormEnv(form: Long) @JvmStatic external fun nGetPageText(page: Long): String? @JvmStatic external fun nCountTextRects(page: Long): Int @JvmStatic external fun nExtractTextRects( diff --git a/pdfium/src/commonMain/kotlin/dev/nucleusframework/pdfium/RenderQuality.kt b/pdfium/src/commonMain/kotlin/dev/nucleusframework/pdfium/RenderQuality.kt index 685875e..cdf0f6b 100644 --- a/pdfium/src/commonMain/kotlin/dev/nucleusframework/pdfium/RenderQuality.kt +++ b/pdfium/src/commonMain/kotlin/dev/nucleusframework/pdfium/RenderQuality.kt @@ -2,7 +2,10 @@ package dev.nucleusframework.pdfium /** * Render quality tiers. Controls which PDFium flags are applied. - * - [PREVIEW] — fastest, no annotations, no LCD text. For thumbnails and initial progressive frames. - * - [FULL] — annotations on, no LCD text. ~15–25% faster than LCD-enabled while still sharp. + * - [PREVIEW] — fastest, no annotations, no form widgets, no LCD text. For thumbnails and + * initial progressive frames. + * - [FULL] — annotations on (FPDF_ANNOT) AND form-widget overlay (FPDF_FFLDraw, used to + * render fillable form fields and digital signature appearances), no LCD text. ~15–25% + * faster than LCD-enabled while still sharp. */ enum class RenderQuality { PREVIEW, FULL } diff --git a/pdfium/src/iosMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.ios.kt b/pdfium/src/iosMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.ios.kt index 3a5a393..466a18d 100644 --- a/pdfium/src/iosMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.ios.kt +++ b/pdfium/src/iosMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.ios.kt @@ -17,9 +17,15 @@ import dev.nucleusframework.pdfium.native.FPDFText_GetUnicode import dev.nucleusframework.pdfium.native.FPDFText_LoadPage import kotlinx.cinterop.DoubleVar import kotlinx.cinterop.value +import dev.nucleusframework.pdfium.native.FORM_OnAfterLoadPage +import dev.nucleusframework.pdfium.native.FORM_OnBeforeClosePage +import dev.nucleusframework.pdfium.native.FPDFDOC_ExitFormFillEnvironment +import dev.nucleusframework.pdfium.native.FPDFDOC_InitFormFillEnvironment import dev.nucleusframework.pdfium.native.FPDF_ANNOT import dev.nucleusframework.pdfium.native.FPDF_CloseDocument import dev.nucleusframework.pdfium.native.FPDF_ClosePage +import dev.nucleusframework.pdfium.native.FPDF_FFLDraw +import dev.nucleusframework.pdfium.native.FPDF_FORMFILLINFO import dev.nucleusframework.pdfium.native.FPDF_GetLastError import dev.nucleusframework.pdfium.native.FPDF_GetMetaText import dev.nucleusframework.pdfium.native.FPDF_GetPageCount @@ -31,6 +37,8 @@ import dev.nucleusframework.pdfium.native.FPDF_LoadMemDocument64 import dev.nucleusframework.pdfium.native.FPDF_LoadPage import dev.nucleusframework.pdfium.native.FPDF_RenderPageBitmap import cnames.structs.fpdf_document_t__ +import cnames.structs.fpdf_form_handle_t__ +import kotlinx.cinterop.nativeHeap import kotlin.concurrent.AtomicReference import kotlinx.cinterop.ByteVar import kotlinx.cinterop.CPointer @@ -81,6 +89,11 @@ internal actual class PdfDocument( // The pin must stay alive until close() or PDFium will dereference freed memory on the // next FPDF_LoadPage / FPDF_GetMetaText call. private val pinnedBuffer: Pinned, + // Per-document FPDF_FORMHANDLE used for FPDF_FFLDraw widget overlay (signatures, form + // fields). May be null if PDFium refused init — render falls back to no overlay. + private val formHandle: CPointer?, + // The FPDF_FORMFILLINFO backing the form handle. Must out-live the handle; freed in close(). + private val formInfoPtr: CPointer?, ) { actual val pageCount: Int = FPDF_GetPageCount(handle) actual val metadata: PdfMetadata = PdfMetadata( @@ -127,6 +140,13 @@ internal actual class PdfDocument( RenderQuality.FULL -> FPDF_ANNOT } FPDF_RenderPageBitmap(bmp, page, 0, 0, widthPx, heightPx, 0, flags) + // Form widget overlay (signatures, fillable fields). Only at FULL quality — + // PREVIEW renders are thumbnails and don't need the extra pass. + if (quality == RenderQuality.FULL && formHandle != null) { + FORM_OnAfterLoadPage(page, formHandle) + FPDF_FFLDraw(formHandle, bmp, page, 0, 0, widthPx, heightPx, 0, flags) + FORM_OnBeforeClosePage(page, formHandle) + } FPDFBitmap_Destroy(bmp) } finally { FPDF_ClosePage(page) @@ -238,6 +258,10 @@ internal actual class PdfDocument( } actual fun close() { + // Form-fill env must be torn down BEFORE the document — PDFium dereferences the + // document inside FPDFDOC_ExitFormFillEnvironment. + if (formHandle != null) FPDFDOC_ExitFormFillEnvironment(formHandle) + if (formInfoPtr != null) nativeHeap.free(formInfoPtr.rawValue) FPDF_CloseDocument(handle) pinnedBuffer.unpin() } @@ -277,5 +301,11 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?) pinned.unpin() error("PDFium refused to open document (err=$err)") } - PdfDocument(handle, pinned) + // Form-fill env: minimal callbacks (version=2, all null) is enough for static widget + // rendering. The struct lives in nativeHeap because PDFium retains a borrowed pointer + // to it for the form handle's lifetime. + val formInfo = nativeHeap.alloc().apply { version = 2 } + val formHandle = FPDFDOC_InitFormFillEnvironment(handle, formInfo.ptr) + if (formHandle == null) nativeHeap.free(formInfo.rawPtr) + PdfDocument(handle, pinned, formHandle, if (formHandle != null) formInfo.ptr else null) } diff --git a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt index 886bab8..3061659 100644 --- a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt +++ b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt @@ -26,6 +26,9 @@ internal actual class PdfDocument internal constructor( private val bufferAddr: Long, private val bufferSize: Int, private val handles: LongArray, + // Per-document FPDF_FORMHANDLE (parallel to [handles]). May be 0 if PDFium refused to + // init the form-fill environment for that handle; render code falls back to no overlay. + private val formHandles: LongArray, private val dispatchers: Array, private val executors: Array, ) { @@ -78,6 +81,9 @@ internal actual class PdfDocument internal constructor( try { val ok = PdfiumBridge.nRenderPageToAddress( page = page, + // PREVIEW skips form-fill to keep thumbnail renders cheap; FULL passes the + // form handle so signatures + interactive widgets render correctly. + form = if (quality == RenderQuality.FULL) formHandles[slot] else 0L, address = addr, width = widthPx, height = heightPx, @@ -155,6 +161,9 @@ internal actual class PdfDocument internal constructor( runBlocking { for (i in handles.indices) { withContext(dispatchers[i]) { + // Form-fill env must be torn down BEFORE its underlying document — + // FPDFDOC_ExitFormFillEnvironment dereferences the FPDF_DOCUMENT. + PdfiumBridge.nCloseFormEnv(formHandles[i]) PdfiumBridge.nCloseDocument(handles[i]) } } @@ -195,7 +204,11 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?) val pairs = Array(POOL_SIZE) { Pdfium.newDispatcher() } val dispatchers = Array(POOL_SIZE) { pairs[it].first } val executors = Array(POOL_SIZE) { pairs[it].second } - PdfDocument(bufferAddr, bytes.size, handles, dispatchers, executors) + // Init one form-fill env per document handle. PDFium tolerates docs without forms + // (the handle still returns non-zero) — FPDF_FFLDraw is a no-op for pages with + // no widgets, so eager init costs only a small struct allocation per document. + val formHandles = LongArray(POOL_SIZE) { PdfiumBridge.nInitFormEnv(handles[it]) } + PdfDocument(bufferAddr, bytes.size, handles, formHandles, dispatchers, executors) } catch (t: Throwable) { PdfiumBridge.nFreeBuffer(bufferAddr) throw t diff --git a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.kt b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.kt index 90f907c..8fdcb43 100644 --- a/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.kt +++ b/pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.kt @@ -28,14 +28,27 @@ internal object PdfiumBridge { height: Int, swapRedBlue: Boolean, ): Boolean - /** Zero-copy render: writes directly at [address]. Flags = `FPDF_ANNOT | FPDF_LCD_TEXT | …`. */ + /** + * Zero-copy render: writes directly at [address]. Flags = `FPDF_ANNOT | FPDF_LCD_TEXT | …`. + * [form] is an optional FPDF_FORMHANDLE from [nInitFormEnv] — pass 0 to skip widget overlay; + * non-zero enables FPDF_FFLDraw rendering of form fields and signature appearances. + */ @JvmStatic external fun nRenderPageToAddress( page: Long, + form: Long, address: Long, width: Int, height: Int, flags: Int, ): Boolean + /** + * Initialize a form-fill environment for the given document handle. Returns 0 if PDFium + * refuses (e.g. document already closed). Must be paired with [nCloseFormEnv] before + * [nCloseDocument] runs. + */ + @JvmStatic external fun nInitFormEnv(doc: Long): Long + /** Tear down the form handle. Safe to call with 0. */ + @JvmStatic external fun nCloseFormEnv(form: Long) @JvmStatic external fun nGetPageText(page: Long): String? /** Count of line-level text rectangles on the given page. */ @JvmStatic external fun nCountTextRects(page: Long): Int diff --git a/pdfium/src/jvmMain/native/pdfium_jni.cpp b/pdfium/src/jvmMain/native/pdfium_jni.cpp index a8f24bc..e99dc85 100644 --- a/pdfium/src/jvmMain/native/pdfium_jni.cpp +++ b/pdfium/src/jvmMain/native/pdfium_jni.cpp @@ -12,6 +12,7 @@ #include "fpdfview.h" #include "fpdf_doc.h" +#include "fpdf_formfill.h" #include "fpdf_text.h" namespace { @@ -184,10 +185,15 @@ Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPage( * Layout: BGRA, stride = width*4. Caller owns the memory; we only write. * [flags] is a bitmask from fpdfview.h (FPDF_ANNOT, FPDF_LCD_TEXT, FPDF_REVERSE_BYTE_ORDER, …). * Passing 0 yields the fastest render (draft quality). + * + * [form] is an optional FPDF_FORMHANDLE (from nInitFormEnv) — pass 0 to skip widget rendering. + * When non-zero, FPDF_FFLDraw overlays form-field appearances (interactive widgets and + * signature appearance streams) on top of the page contents. PDFium documents this as the + * required second pass for displaying form widgets correctly. */ JNIEXPORT jboolean JNICALL Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPageToAddress( - JNIEnv*, jclass, jlong page, jlong address, + JNIEnv*, jclass, jlong page, jlong form, jlong address, jint width, jint height, jint flags) { if (page == 0 || address == 0) return JNI_FALSE; void* buffer = reinterpret_cast(address); @@ -196,12 +202,52 @@ Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPageToAddress( FPDF_BITMAP bmp = FPDFBitmap_CreateEx(width, height, FPDFBitmap_BGRA, buffer, stride); if (!bmp) return JNI_FALSE; FPDFBitmap_FillRect(bmp, 0, 0, width, height, 0xFFFFFFFF); - FPDF_RenderPageBitmap(bmp, reinterpret_cast(page), - 0, 0, width, height, 0, flags); + FPDF_PAGE p = reinterpret_cast(page); + FPDF_RenderPageBitmap(bmp, p, 0, 0, width, height, 0, flags); + if (form != 0) { + FPDF_FORMHANDLE fh = reinterpret_cast(form); + FORM_OnAfterLoadPage(p, fh); + FPDF_FFLDraw(fh, bmp, p, 0, 0, width, height, 0, flags); + FORM_OnBeforeClosePage(p, fh); + } FPDFBitmap_Destroy(bmp); return JNI_TRUE; } +// Minimal FPDF_FORMFILLINFO for read-only widget rendering. All callbacks left null — +// PDFium tolerates this for static-render use cases (no JavaScript, no field interaction). +// Must out-live the FPDF_FORMHANDLE returned by FPDFDOC_InitFormFillEnvironment, so it's +// declared with static storage. A single shared instance across documents is safe: the +// struct holds no per-document state. +namespace { FPDF_FORMFILLINFO g_formInfo = []() { FPDF_FORMFILLINFO i{}; i.version = 2; return i; }(); } + +/** + * Initialize a form-fill environment for [doc] and return its handle. The handle is + * passed to [nRenderPageToAddress] / [nRenderPageToBitmap] so PDFium can overlay + * widget annotations (form fields, signatures) on top of rendered pages. Returns 0 + * if PDFium refuses; caller may continue without form-fill in that case. + */ +JNIEXPORT jlong JNICALL +Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nInitFormEnv( + JNIEnv*, jclass, jlong doc) { + if (doc == 0) return 0; + FPDF_FORMHANDLE form = FPDFDOC_InitFormFillEnvironment( + reinterpret_cast(doc), &g_formInfo); + return reinterpret_cast(form); +} + +/** + * Tear down the form-fill environment created by [nInitFormEnv]. Must be called BEFORE + * the underlying FPDF_DOCUMENT is closed — PDFium will crash if you exit the document + * first. + */ +JNIEXPORT void JNICALL +Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nCloseFormEnv( + JNIEnv*, jclass, jlong form) { + if (form == 0) return; + FPDFDOC_ExitFormFillEnvironment(reinterpret_cast(form)); +} + /** * Allocate a native buffer and copy the Java byte array into it. Returns a raw pointer * that can be passed to [nOpenDocumentFromMemory] — the pointer must outlive every diff --git a/pdfium/src/nativeInterop/cinterop/pdfium.def b/pdfium/src/nativeInterop/cinterop/pdfium.def index 0d48667..3d095c5 100644 --- a/pdfium/src/nativeInterop/cinterop/pdfium.def +++ b/pdfium/src/nativeInterop/cinterop/pdfium.def @@ -1,6 +1,6 @@ package = dev.nucleusframework.pdfium.native language = C -headers = fpdfview.h fpdf_doc.h fpdf_text.h +headers = fpdfview.h fpdf_doc.h fpdf_formfill.h fpdf_text.h headerFilter = fpdf*.h # Headers are staged by the Gradle task `installPdfiumHeaders` under build/pdfium/include. diff --git a/pdfium/src/webMain/resources/pdfium/pdfium_worker.mjs b/pdfium/src/webMain/resources/pdfium/pdfium_worker.mjs index 1de7ac5..d91196e 100644 --- a/pdfium/src/webMain/resources/pdfium/pdfium_worker.mjs +++ b/pdfium/src/webMain/resources/pdfium/pdfium_worker.mjs @@ -20,6 +20,11 @@ M._FPDF_InitLibrary(); // Track the raw-buffer pointer we allocate for each open document so close() can free // it without an extra round-trip. const bufferByDoc = new Map(); +// Form-fill environment handle + the FPDF_FORMFILLINFO backing struct per document. +// PDFium retains a borrowed pointer to the struct for the form handle's lifetime, so +// both must be freed in lockstep with the document. +const formByDoc = new Map(); +const formInfoByDoc = new Map(); // ---- low-level helpers --------------------------------------------------------------- @@ -68,6 +73,24 @@ function getMetaText(doc, tag) { // ---- operations ---------------------------------------------------------------------- const FPDFBitmap_BGRA = 4; +const FPDF_ANNOT_FLAG = 0x01; +// FPDF_FORMFILLINFO is a struct of ~45 function pointers + a leading `int version`. In the +// WASM32 ABI each is 4 bytes, so 1024 bytes is generous. We zero-fill everything and set +// only version=2 — that leaves every callback as null, which PDFium treats as a no-op for +// static (read-only) widget rendering. +const FORMFILLINFO_SIZE = 1024; + +function initFormEnv(doc) { + const infoPtr = M._malloc(FORMFILLINFO_SIZE); + M.HEAPU8.fill(0, infoPtr, infoPtr + FORMFILLINFO_SIZE); + M.HEAP32[infoPtr >> 2] = 2; // FPDF_FORMFILLINFO.version + const formHandle = M._FPDFDOC_InitFormFillEnvironment(doc, infoPtr); + if (!formHandle) { + M._free(infoPtr); + return { formHandle: 0, infoPtr: 0 }; + } + return { formHandle, infoPtr }; +} function openDocument({ buffer, password }) { const u8 = new Uint8Array(buffer); @@ -81,6 +104,11 @@ function openDocument({ buffer, password }) { throw new Error('PDFium refused to open document (err=' + M._FPDF_GetLastError() + ')'); } bufferByDoc.set(doc, ptr); + const { formHandle, infoPtr } = initFormEnv(doc); + if (formHandle) { + formByDoc.set(doc, formHandle); + formInfoByDoc.set(doc, infoPtr); + } return { result: { doc, @@ -99,6 +127,15 @@ function openDocument({ buffer, password }) { } function closeDocument({ doc }) { + // Form-fill env must be torn down before the document — PDFium dereferences the doc + // inside FPDFDOC_ExitFormFillEnvironment. + const formHandle = formByDoc.get(doc); + if (formHandle) { + M._FPDFDOC_ExitFormFillEnvironment(formHandle); + formByDoc.delete(doc); + } + const infoPtr = formInfoByDoc.get(doc); + if (infoPtr) { M._free(infoPtr); formInfoByDoc.delete(doc); } M._FPDF_CloseDocument(doc); const ptr = bufferByDoc.get(doc); if (ptr) { M._free(ptr); bufferByDoc.delete(doc); } @@ -135,6 +172,14 @@ function renderPage({ doc, pageIndex, w, h, flags }) { } try { M._FPDF_RenderPageBitmap(bmp, page, 0, 0, w, h, 0, flags); + // Form widget overlay (signatures, fillable fields). Only when FPDF_ANNOT is set + // (RenderQuality.FULL) — PREVIEW thumbnails skip this extra pass for speed. + const formHandle = (flags & FPDF_ANNOT_FLAG) ? formByDoc.get(doc) : 0; + if (formHandle) { + M._FORM_OnAfterLoadPage(page, formHandle); + M._FPDF_FFLDraw(formHandle, bmp, page, 0, 0, w, h, 0, flags); + M._FORM_OnBeforeClosePage(page, formHandle); + } // Detach via .slice so freeing pdfium's buffer below doesn't dangle. const pixelBuffer = M.HEAPU8.slice(pixels, pixels + size).buffer; return { result: { pixels: pixelBuffer }, transfer: [pixelBuffer] };