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] };