Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions pdfium/src/androidMain/cpp/pdfium_android.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <cstring>

#include "fpdfview.h"
#include "fpdf_formfill.h"

#define LOG_TAG "pdfiumjni"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
Expand All @@ -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;

Expand Down Expand Up @@ -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<FPDF_PAGE>(page),
0, 0, width, height, 0, flags | FPDF_REVERSE_BYTE_ORDER);
const int renderFlags = flags | FPDF_REVERSE_BYTE_ORDER;
FPDF_PAGE p = reinterpret_cast<FPDF_PAGE>(page);
FPDF_RenderPageBitmap(bmp, p, 0, 0, width, height, 0, renderFlags);
if (form != 0) {
FPDF_FORMHANDLE fh = reinterpret_cast<FPDF_FORMHANDLE>(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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CoroutineDispatcher>,
private val executors: Array<ExecutorService>,
) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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])
}
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<ByteArray>,
// 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<fpdf_form_handle_t__>?,
// The FPDF_FORMFILLINFO backing the form handle. Must out-live the handle; freed in close().
private val formInfoPtr: CPointer<FPDF_FORMFILLINFO>?,
) {
actual val pageCount: Int = FPDF_GetPageCount(handle)
actual val metadata: PdfMetadata = PdfMetadata(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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<FPDF_FORMFILLINFO>().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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CoroutineDispatcher>,
private val executors: Array<ExecutorService>,
) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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])
}
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 49 additions & 3 deletions pdfium/src/jvmMain/native/pdfium_jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "fpdfview.h"
#include "fpdf_doc.h"
#include "fpdf_formfill.h"
#include "fpdf_text.h"

namespace {
Expand Down Expand Up @@ -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<void*>(address);
Expand All @@ -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<FPDF_PAGE>(page),
0, 0, width, height, 0, flags);
FPDF_PAGE p = reinterpret_cast<FPDF_PAGE>(page);
FPDF_RenderPageBitmap(bmp, p, 0, 0, width, height, 0, flags);
if (form != 0) {
FPDF_FORMHANDLE fh = reinterpret_cast<FPDF_FORMHANDLE>(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<FPDF_DOCUMENT>(doc), &g_formInfo);
return reinterpret_cast<jlong>(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<FPDF_FORMHANDLE>(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
Expand Down
2 changes: 1 addition & 1 deletion pdfium/src/nativeInterop/cinterop/pdfium.def
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading