Skip to content

Commit d6ecace

Browse files
authored
Merge pull request #6 from NadeemIqbal/fix/issue-5-signature-rendering
fix: render form widgets and signature appearances via FPDF_FFLDraw
2 parents 6f0ebb0 + b14e03c commit d6ecace

10 files changed

Lines changed: 190 additions & 14 deletions

File tree

pdfium/src/androidMain/cpp/pdfium_android.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <cstring>
1010

1111
#include "fpdfview.h"
12+
#include "fpdf_formfill.h"
1213

1314
#define LOG_TAG "pdfiumjni"
1415
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
@@ -17,7 +18,7 @@ extern "C" {
1718

1819
JNIEXPORT jboolean JNICALL
1920
Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPageToBitmap(
20-
JNIEnv* env, jclass, jlong page, jobject bitmap,
21+
JNIEnv* env, jclass, jlong page, jlong form, jobject bitmap,
2122
jint width, jint height, jint flags) {
2223
if (page == 0 || bitmap == nullptr) return JNI_FALSE;
2324

@@ -48,8 +49,15 @@ Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPageToBitmap(
4849
return JNI_FALSE;
4950
}
5051
FPDFBitmap_FillRect(bmp, 0, 0, width, height, 0xFFFFFFFF);
51-
FPDF_RenderPageBitmap(bmp, reinterpret_cast<FPDF_PAGE>(page),
52-
0, 0, width, height, 0, flags | FPDF_REVERSE_BYTE_ORDER);
52+
const int renderFlags = flags | FPDF_REVERSE_BYTE_ORDER;
53+
FPDF_PAGE p = reinterpret_cast<FPDF_PAGE>(page);
54+
FPDF_RenderPageBitmap(bmp, p, 0, 0, width, height, 0, renderFlags);
55+
if (form != 0) {
56+
FPDF_FORMHANDLE fh = reinterpret_cast<FPDF_FORMHANDLE>(form);
57+
FORM_OnAfterLoadPage(p, fh);
58+
FPDF_FFLDraw(fh, bmp, p, 0, 0, width, height, 0, renderFlags);
59+
FORM_OnBeforeClosePage(p, fh);
60+
}
5361
FPDFBitmap_Destroy(bmp);
5462

5563
AndroidBitmap_unlockPixels(env, bitmap);

pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.android.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ internal actual class PdfDocument internal constructor(
1616
private val bufferAddr: Long,
1717
private val bufferSize: Int,
1818
private val handles: LongArray,
19+
// Per-document FPDF_FORMHANDLE (parallel to [handles]). May be 0 if PDFium refused;
20+
// render code falls back to no widget overlay in that case.
21+
private val formHandles: LongArray,
1922
private val dispatchers: Array<CoroutineDispatcher>,
2023
private val executors: Array<ExecutorService>,
2124
) {
@@ -64,6 +67,9 @@ internal actual class PdfDocument internal constructor(
6467
try {
6568
val ok = PdfiumBridge.nRenderPageToBitmap(
6669
page = page,
70+
// PREVIEW skips form-fill to keep thumbnails cheap; FULL passes the form
71+
// handle so signatures + interactive widgets render correctly.
72+
form = if (quality == RenderQuality.FULL) formHandles[slot] else 0L,
6773
bitmap = bitmap,
6874
width = widthPx,
6975
height = heightPx,
@@ -139,6 +145,8 @@ internal actual class PdfDocument internal constructor(
139145
runBlocking {
140146
for (i in handles.indices) {
141147
withContext(dispatchers[i]) {
148+
// Form-fill env must be torn down BEFORE its underlying document.
149+
PdfiumBridge.nCloseFormEnv(formHandles[i])
142150
PdfiumBridge.nCloseDocument(handles[i])
143151
}
144152
}
@@ -175,7 +183,8 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?)
175183
val pairs = Array(POOL_SIZE) { Pdfium.newDispatcher() }
176184
val dispatchers = Array(POOL_SIZE) { pairs[it].first }
177185
val executors = Array(POOL_SIZE) { pairs[it].second }
178-
PdfDocument(bufferAddr, bytes.size, handles, dispatchers, executors)
186+
val formHandles = LongArray(POOL_SIZE) { PdfiumBridge.nInitFormEnv(handles[it]) }
187+
PdfDocument(bufferAddr, bytes.size, handles, formHandles, dispatchers, executors)
179188
} catch (t: Throwable) {
180189
PdfiumBridge.nFreeBuffer(bufferAddr)
181190
throw t

pdfium/src/androidMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.android.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,23 @@ internal object PdfiumBridge {
2828
height: Int,
2929
swapRedBlue: Boolean,
3030
): Boolean
31-
/** Android-only zero-copy render: locks the Bitmap's pixels and writes directly. */
31+
/**
32+
* Android-only zero-copy render: locks the Bitmap's pixels and writes directly.
33+
* [form] is an optional FPDF_FORMHANDLE from [nInitFormEnv] — pass 0 to skip widget overlay;
34+
* non-zero enables FPDF_FFLDraw rendering of form fields and signature appearances.
35+
*/
3236
@JvmStatic external fun nRenderPageToBitmap(
3337
page: Long,
38+
form: Long,
3439
bitmap: android.graphics.Bitmap,
3540
width: Int,
3641
height: Int,
3742
flags: Int,
3843
): Boolean
44+
/** Init form-fill environment for [doc]. Returns 0 on failure. */
45+
@JvmStatic external fun nInitFormEnv(doc: Long): Long
46+
/** Tear down a form handle. Must run before the underlying document is closed. */
47+
@JvmStatic external fun nCloseFormEnv(form: Long)
3948
@JvmStatic external fun nGetPageText(page: Long): String?
4049
@JvmStatic external fun nCountTextRects(page: Long): Int
4150
@JvmStatic external fun nExtractTextRects(

pdfium/src/commonMain/kotlin/dev/nucleusframework/pdfium/RenderQuality.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package dev.nucleusframework.pdfium
22

33
/**
44
* Render quality tiers. Controls which PDFium flags are applied.
5-
* - [PREVIEW] — fastest, no annotations, no LCD text. For thumbnails and initial progressive frames.
6-
* - [FULL] — annotations on, no LCD text. ~15–25% faster than LCD-enabled while still sharp.
5+
* - [PREVIEW] — fastest, no annotations, no form widgets, no LCD text. For thumbnails and
6+
* initial progressive frames.
7+
* - [FULL] — annotations on (FPDF_ANNOT) AND form-widget overlay (FPDF_FFLDraw, used to
8+
* render fillable form fields and digital signature appearances), no LCD text. ~15–25%
9+
* faster than LCD-enabled while still sharp.
710
*/
811
enum class RenderQuality { PREVIEW, FULL }

pdfium/src/iosMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.ios.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ import dev.nucleusframework.pdfium.native.FPDFText_GetUnicode
1717
import dev.nucleusframework.pdfium.native.FPDFText_LoadPage
1818
import kotlinx.cinterop.DoubleVar
1919
import kotlinx.cinterop.value
20+
import dev.nucleusframework.pdfium.native.FORM_OnAfterLoadPage
21+
import dev.nucleusframework.pdfium.native.FORM_OnBeforeClosePage
22+
import dev.nucleusframework.pdfium.native.FPDFDOC_ExitFormFillEnvironment
23+
import dev.nucleusframework.pdfium.native.FPDFDOC_InitFormFillEnvironment
2024
import dev.nucleusframework.pdfium.native.FPDF_ANNOT
2125
import dev.nucleusframework.pdfium.native.FPDF_CloseDocument
2226
import dev.nucleusframework.pdfium.native.FPDF_ClosePage
27+
import dev.nucleusframework.pdfium.native.FPDF_FFLDraw
28+
import dev.nucleusframework.pdfium.native.FPDF_FORMFILLINFO
2329
import dev.nucleusframework.pdfium.native.FPDF_GetLastError
2430
import dev.nucleusframework.pdfium.native.FPDF_GetMetaText
2531
import dev.nucleusframework.pdfium.native.FPDF_GetPageCount
@@ -31,6 +37,8 @@ import dev.nucleusframework.pdfium.native.FPDF_LoadMemDocument64
3137
import dev.nucleusframework.pdfium.native.FPDF_LoadPage
3238
import dev.nucleusframework.pdfium.native.FPDF_RenderPageBitmap
3339
import cnames.structs.fpdf_document_t__
40+
import cnames.structs.fpdf_form_handle_t__
41+
import kotlinx.cinterop.nativeHeap
3442
import kotlin.concurrent.AtomicReference
3543
import kotlinx.cinterop.ByteVar
3644
import kotlinx.cinterop.CPointer
@@ -81,6 +89,11 @@ internal actual class PdfDocument(
8189
// The pin must stay alive until close() or PDFium will dereference freed memory on the
8290
// next FPDF_LoadPage / FPDF_GetMetaText call.
8391
private val pinnedBuffer: Pinned<ByteArray>,
92+
// Per-document FPDF_FORMHANDLE used for FPDF_FFLDraw widget overlay (signatures, form
93+
// fields). May be null if PDFium refused init — render falls back to no overlay.
94+
private val formHandle: CPointer<fpdf_form_handle_t__>?,
95+
// The FPDF_FORMFILLINFO backing the form handle. Must out-live the handle; freed in close().
96+
private val formInfoPtr: CPointer<FPDF_FORMFILLINFO>?,
8497
) {
8598
actual val pageCount: Int = FPDF_GetPageCount(handle)
8699
actual val metadata: PdfMetadata = PdfMetadata(
@@ -127,6 +140,13 @@ internal actual class PdfDocument(
127140
RenderQuality.FULL -> FPDF_ANNOT
128141
}
129142
FPDF_RenderPageBitmap(bmp, page, 0, 0, widthPx, heightPx, 0, flags)
143+
// Form widget overlay (signatures, fillable fields). Only at FULL quality —
144+
// PREVIEW renders are thumbnails and don't need the extra pass.
145+
if (quality == RenderQuality.FULL && formHandle != null) {
146+
FORM_OnAfterLoadPage(page, formHandle)
147+
FPDF_FFLDraw(formHandle, bmp, page, 0, 0, widthPx, heightPx, 0, flags)
148+
FORM_OnBeforeClosePage(page, formHandle)
149+
}
130150
FPDFBitmap_Destroy(bmp)
131151
} finally {
132152
FPDF_ClosePage(page)
@@ -238,6 +258,10 @@ internal actual class PdfDocument(
238258
}
239259

240260
actual fun close() {
261+
// Form-fill env must be torn down BEFORE the document — PDFium dereferences the
262+
// document inside FPDFDOC_ExitFormFillEnvironment.
263+
if (formHandle != null) FPDFDOC_ExitFormFillEnvironment(formHandle)
264+
if (formInfoPtr != null) nativeHeap.free(formInfoPtr.rawValue)
241265
FPDF_CloseDocument(handle)
242266
pinnedBuffer.unpin()
243267
}
@@ -277,5 +301,11 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?)
277301
pinned.unpin()
278302
error("PDFium refused to open document (err=$err)")
279303
}
280-
PdfDocument(handle, pinned)
304+
// Form-fill env: minimal callbacks (version=2, all null) is enough for static widget
305+
// rendering. The struct lives in nativeHeap because PDFium retains a borrowed pointer
306+
// to it for the form handle's lifetime.
307+
val formInfo = nativeHeap.alloc<FPDF_FORMFILLINFO>().apply { version = 2 }
308+
val formHandle = FPDFDOC_InitFormFillEnvironment(handle, formInfo.ptr)
309+
if (formHandle == null) nativeHeap.free(formInfo.rawPtr)
310+
PdfDocument(handle, pinned, formHandle, if (formHandle != null) formInfo.ptr else null)
281311
}

pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/PdfDocument.jvm.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ internal actual class PdfDocument internal constructor(
2626
private val bufferAddr: Long,
2727
private val bufferSize: Int,
2828
private val handles: LongArray,
29+
// Per-document FPDF_FORMHANDLE (parallel to [handles]). May be 0 if PDFium refused to
30+
// init the form-fill environment for that handle; render code falls back to no overlay.
31+
private val formHandles: LongArray,
2932
private val dispatchers: Array<CoroutineDispatcher>,
3033
private val executors: Array<ExecutorService>,
3134
) {
@@ -78,6 +81,9 @@ internal actual class PdfDocument internal constructor(
7881
try {
7982
val ok = PdfiumBridge.nRenderPageToAddress(
8083
page = page,
84+
// PREVIEW skips form-fill to keep thumbnail renders cheap; FULL passes the
85+
// form handle so signatures + interactive widgets render correctly.
86+
form = if (quality == RenderQuality.FULL) formHandles[slot] else 0L,
8187
address = addr,
8288
width = widthPx,
8389
height = heightPx,
@@ -155,6 +161,9 @@ internal actual class PdfDocument internal constructor(
155161
runBlocking {
156162
for (i in handles.indices) {
157163
withContext(dispatchers[i]) {
164+
// Form-fill env must be torn down BEFORE its underlying document —
165+
// FPDFDOC_ExitFormFillEnvironment dereferences the FPDF_DOCUMENT.
166+
PdfiumBridge.nCloseFormEnv(formHandles[i])
158167
PdfiumBridge.nCloseDocument(handles[i])
159168
}
160169
}
@@ -195,7 +204,11 @@ internal actual suspend fun openPdfDocument(bytes: ByteArray, password: String?)
195204
val pairs = Array(POOL_SIZE) { Pdfium.newDispatcher() }
196205
val dispatchers = Array(POOL_SIZE) { pairs[it].first }
197206
val executors = Array(POOL_SIZE) { pairs[it].second }
198-
PdfDocument(bufferAddr, bytes.size, handles, dispatchers, executors)
207+
// Init one form-fill env per document handle. PDFium tolerates docs without forms
208+
// (the handle still returns non-zero) — FPDF_FFLDraw is a no-op for pages with
209+
// no widgets, so eager init costs only a small struct allocation per document.
210+
val formHandles = LongArray(POOL_SIZE) { PdfiumBridge.nInitFormEnv(handles[it]) }
211+
PdfDocument(bufferAddr, bytes.size, handles, formHandles, dispatchers, executors)
199212
} catch (t: Throwable) {
200213
PdfiumBridge.nFreeBuffer(bufferAddr)
201214
throw t

pdfium/src/jvmMain/kotlin/dev/nucleusframework/pdfium/jvm/PdfiumBridge.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,27 @@ internal object PdfiumBridge {
2828
height: Int,
2929
swapRedBlue: Boolean,
3030
): Boolean
31-
/** Zero-copy render: writes directly at [address]. Flags = `FPDF_ANNOT | FPDF_LCD_TEXT | …`. */
31+
/**
32+
* Zero-copy render: writes directly at [address]. Flags = `FPDF_ANNOT | FPDF_LCD_TEXT | …`.
33+
* [form] is an optional FPDF_FORMHANDLE from [nInitFormEnv] — pass 0 to skip widget overlay;
34+
* non-zero enables FPDF_FFLDraw rendering of form fields and signature appearances.
35+
*/
3236
@JvmStatic external fun nRenderPageToAddress(
3337
page: Long,
38+
form: Long,
3439
address: Long,
3540
width: Int,
3641
height: Int,
3742
flags: Int,
3843
): Boolean
44+
/**
45+
* Initialize a form-fill environment for the given document handle. Returns 0 if PDFium
46+
* refuses (e.g. document already closed). Must be paired with [nCloseFormEnv] before
47+
* [nCloseDocument] runs.
48+
*/
49+
@JvmStatic external fun nInitFormEnv(doc: Long): Long
50+
/** Tear down the form handle. Safe to call with 0. */
51+
@JvmStatic external fun nCloseFormEnv(form: Long)
3952
@JvmStatic external fun nGetPageText(page: Long): String?
4053
/** Count of line-level text rectangles on the given page. */
4154
@JvmStatic external fun nCountTextRects(page: Long): Int

pdfium/src/jvmMain/native/pdfium_jni.cpp

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
#include "fpdfview.h"
1414
#include "fpdf_doc.h"
15+
#include "fpdf_formfill.h"
1516
#include "fpdf_text.h"
1617

1718
namespace {
@@ -184,10 +185,15 @@ Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPage(
184185
* Layout: BGRA, stride = width*4. Caller owns the memory; we only write.
185186
* [flags] is a bitmask from fpdfview.h (FPDF_ANNOT, FPDF_LCD_TEXT, FPDF_REVERSE_BYTE_ORDER, …).
186187
* Passing 0 yields the fastest render (draft quality).
188+
*
189+
* [form] is an optional FPDF_FORMHANDLE (from nInitFormEnv) — pass 0 to skip widget rendering.
190+
* When non-zero, FPDF_FFLDraw overlays form-field appearances (interactive widgets and
191+
* signature appearance streams) on top of the page contents. PDFium documents this as the
192+
* required second pass for displaying form widgets correctly.
187193
*/
188194
JNIEXPORT jboolean JNICALL
189195
Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPageToAddress(
190-
JNIEnv*, jclass, jlong page, jlong address,
196+
JNIEnv*, jclass, jlong page, jlong form, jlong address,
191197
jint width, jint height, jint flags) {
192198
if (page == 0 || address == 0) return JNI_FALSE;
193199
void* buffer = reinterpret_cast<void*>(address);
@@ -196,12 +202,52 @@ Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nRenderPageToAddress(
196202
FPDF_BITMAP bmp = FPDFBitmap_CreateEx(width, height, FPDFBitmap_BGRA, buffer, stride);
197203
if (!bmp) return JNI_FALSE;
198204
FPDFBitmap_FillRect(bmp, 0, 0, width, height, 0xFFFFFFFF);
199-
FPDF_RenderPageBitmap(bmp, reinterpret_cast<FPDF_PAGE>(page),
200-
0, 0, width, height, 0, flags);
205+
FPDF_PAGE p = reinterpret_cast<FPDF_PAGE>(page);
206+
FPDF_RenderPageBitmap(bmp, p, 0, 0, width, height, 0, flags);
207+
if (form != 0) {
208+
FPDF_FORMHANDLE fh = reinterpret_cast<FPDF_FORMHANDLE>(form);
209+
FORM_OnAfterLoadPage(p, fh);
210+
FPDF_FFLDraw(fh, bmp, p, 0, 0, width, height, 0, flags);
211+
FORM_OnBeforeClosePage(p, fh);
212+
}
201213
FPDFBitmap_Destroy(bmp);
202214
return JNI_TRUE;
203215
}
204216

217+
// Minimal FPDF_FORMFILLINFO for read-only widget rendering. All callbacks left null —
218+
// PDFium tolerates this for static-render use cases (no JavaScript, no field interaction).
219+
// Must out-live the FPDF_FORMHANDLE returned by FPDFDOC_InitFormFillEnvironment, so it's
220+
// declared with static storage. A single shared instance across documents is safe: the
221+
// struct holds no per-document state.
222+
namespace { FPDF_FORMFILLINFO g_formInfo = []() { FPDF_FORMFILLINFO i{}; i.version = 2; return i; }(); }
223+
224+
/**
225+
* Initialize a form-fill environment for [doc] and return its handle. The handle is
226+
* passed to [nRenderPageToAddress] / [nRenderPageToBitmap] so PDFium can overlay
227+
* widget annotations (form fields, signatures) on top of rendered pages. Returns 0
228+
* if PDFium refuses; caller may continue without form-fill in that case.
229+
*/
230+
JNIEXPORT jlong JNICALL
231+
Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nInitFormEnv(
232+
JNIEnv*, jclass, jlong doc) {
233+
if (doc == 0) return 0;
234+
FPDF_FORMHANDLE form = FPDFDOC_InitFormFillEnvironment(
235+
reinterpret_cast<FPDF_DOCUMENT>(doc), &g_formInfo);
236+
return reinterpret_cast<jlong>(form);
237+
}
238+
239+
/**
240+
* Tear down the form-fill environment created by [nInitFormEnv]. Must be called BEFORE
241+
* the underlying FPDF_DOCUMENT is closed — PDFium will crash if you exit the document
242+
* first.
243+
*/
244+
JNIEXPORT void JNICALL
245+
Java_dev_nucleusframework_pdfium_jvm_PdfiumBridge_nCloseFormEnv(
246+
JNIEnv*, jclass, jlong form) {
247+
if (form == 0) return;
248+
FPDFDOC_ExitFormFillEnvironment(reinterpret_cast<FPDF_FORMHANDLE>(form));
249+
}
250+
205251
/**
206252
* Allocate a native buffer and copy the Java byte array into it. Returns a raw pointer
207253
* that can be passed to [nOpenDocumentFromMemory] — the pointer must outlive every

pdfium/src/nativeInterop/cinterop/pdfium.def

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package = dev.nucleusframework.pdfium.native
22
language = C
3-
headers = fpdfview.h fpdf_doc.h fpdf_text.h
3+
headers = fpdfview.h fpdf_doc.h fpdf_formfill.h fpdf_text.h
44
headerFilter = fpdf*.h
55

66
# Headers are staged by the Gradle task `installPdfiumHeaders` under build/pdfium/include.

0 commit comments

Comments
 (0)