From 15bcd2f92878e658a20e3038389e0ace35eaeefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= Date: Tue, 21 Apr 2026 13:07:27 +0200 Subject: [PATCH 1/3] refactor: update PDFium library version to 0.15.0 and improve native library loading with system property support --- build.gradle.kts | 2 +- .../org/grimmory/pdfium4j/PdfiumLibrary.java | 149 ++++++++++++++---- .../pdfium4j/internal/NativeLoader.java | 36 ++++- 3 files changed, 153 insertions(+), 34 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f382283..9c389dd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ plugins { allprojects { group = "org.grimmory" - version = "0.14.0" + version = "0.15.0" repositories { mavenCentral() diff --git a/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java b/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java index 87532e4..c6c1c5d 100644 --- a/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java +++ b/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java @@ -13,53 +13,144 @@ *

PDFium requires a single call to {@code FPDF_InitLibraryWithConfig} before any document * operations, and {@code FPDF_DestroyLibrary} at shutdown. This class handles both automatically. * - *

Thread safety: initialization is synchronized. After init, independent {@link PdfDocument} - * instances on separate threads are safe. A single document/page handle must not be accessed + *

Concurrency model

+ * + *

Initialization is driven by the Initialization-on-demand holder idiom + * (JLS §12.4.2): the JVM class-loader lock guarantees {@code FPDF_InitLibraryWithConfig} + * is invoked exactly once per JVM, under the class-loader initialization lock, + * before any thread can observe a successful state. No explicit synchronized + * block, volatile read, or double-checked locking is needed on the hot path. + * + *

This makes {@link #initialize()} and {@link #isAvailable()} safe to call + * from any thread at any time, including concurrent {@code @BeforeAll} hooks + * in a parallel JUnit test run. + * + *

After initialization, independent {@link PdfDocument} instances on separate + * threads are safe. A single document or page handle must not be accessed * concurrently. + * + *

System properties

+ * + * */ public final class PdfiumLibrary { - private static volatile boolean initialized = false; + /** See class javadoc for usage. */ + public static final String PROP_LIBRARY_PATH = "pdfium4j.library.path"; + + /** See class javadoc for usage. */ + public static final String PROP_AUTOINIT = "pdfium4j.autoinit"; private PdfiumLibrary() {} /** * Ensure PDFium is initialized. Safe to call multiple times. Called automatically by {@link - * PdfDocument#open} methods. + * PdfDocument#open} methods (unless {@link #PROP_AUTOINIT} is set to {@code "false"}). + * + * @throws PdfiumException if the native library cannot be loaded or initialized */ - public static synchronized void initialize() { - if (initialized) return; + public static void initialize() { + Holder.STATE.require(); + } - NativeLoader.ensureLoaded(); + /** + * Non-throwing availability probe. + * + *

Returns {@code true} if the native library has been successfully loaded and + * {@code FPDF_InitLibraryWithConfig} has been invoked. Returns {@code false} if + * initialization has failed, the cause is suppressed, never re-thrown from this + * method. Safe to call from any thread. + * + *

This is the preferred way to gate optional functionality: + * + *

{@code
+   * if (PdfiumLibrary.isAvailable()) {
+   *   try (PdfDocument doc = PdfDocument.open(path)) { ... }
+   * }
+   * }
+ * + * @return {@code true} iff PDFium is fully initialized in this JVM + */ + public static boolean isAvailable() { + return Holder.STATE.loaded; + } - try (Arena arena = Arena.ofConfined()) { - MemorySegment config = arena.allocate(ViewBindings.LIBRARY_CONFIG_LAYOUT); - config.set(ValueLayout.JAVA_INT, 0, 2); + /** + * Returns the Throwable that caused initialization to fail, or {@code null} + * if initialization succeeded or has not yet been attempted for reasons + * other than failure. Useful for diagnostics/logging without forcing a throw. + */ + public static Throwable loadError() { + return Holder.STATE.loadError; + } - ViewBindings.FPDF_InitLibraryWithConfig.invokeExact(config); - } catch (PdfiumException e) { - throw e; - } catch (Throwable t) { - throw new PdfiumException("Failed to initialize PDFium", t); + static void ensureInitialized() { + if (!autoInitEnabled()) { + // Honour the explicit opt-out; require the caller has already run initialize(). + if (!Holder.STATE.loaded) { + throw new PdfiumException( + "PDFium auto-init is disabled (" + PROP_AUTOINIT + "=false); call PdfiumLibrary.initialize() explicitly"); + } + return; } + Holder.STATE.require(); + } - initialized = true; + private static boolean autoInitEnabled() { + String v = System.getProperty(PROP_AUTOINIT); + return v == null || !"false".equalsIgnoreCase(v.trim()); + } - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - try { - ViewBindings.FPDF_DestroyLibrary.invokeExact(); - } catch (Throwable ignored) { - } - }, - "pdfium4j-shutdown")); + private static final class Holder { + static final State STATE = new State(); } - static void ensureInitialized() { - if (!initialized) { - initialize(); + private static final class State { + final boolean loaded; + final Throwable loadError; + + State() { + Throwable err = null; + boolean ok = false; + try { + NativeLoader.ensureLoaded(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment config = arena.allocate(ViewBindings.LIBRARY_CONFIG_LAYOUT); + config.set(ValueLayout.JAVA_INT, 0, 2); + ViewBindings.FPDF_InitLibraryWithConfig.invokeExact(config); + } + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + try { + ViewBindings.FPDF_DestroyLibrary.invokeExact(); + } catch (Throwable ignored) { + } + }, + "pdfium4j-shutdown")); + ok = true; + } catch (Throwable t) { + err = t; + } + this.loaded = ok; + this.loadError = err; + } + + void require() { + if (loaded) return; + if (loadError instanceof PdfiumException pe) { + throw pe; + } + throw new PdfiumException("Failed to initialize PDFium", loadError); } } } diff --git a/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java b/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java index 657abff..5936f68 100644 --- a/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java +++ b/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java @@ -16,6 +16,13 @@ public final class NativeLoader { + /** + * System property: absolute filesystem path to a pdfium native library. When set, + * {@link #ensureLoaded()} loads this path directly via {@code System.load} and + * skips both classpath extraction and {@code System.loadLibrary("pdfium")} lookup. + */ + public static final String PROP_LIBRARY_PATH = "pdfium4j.library.path"; + private static volatile boolean loaded = false; private static volatile Throwable loadError = null; @@ -32,6 +39,26 @@ public static void ensureLoaded() { throw new NativeLoadException("Native library failed to load previously", loadError); } try { + String overridePath = System.getProperty(PROP_LIBRARY_PATH); + if (overridePath != null && !overridePath.isBlank()) { + try { + System.load(overridePath); + loaded = true; + } catch (UnsatisfiedLinkError e) { + NativeLoadException ex = + new NativeLoadException( + "PDFium override path set via -D" + + PROP_LIBRARY_PATH + + "=" + + overridePath + + " but loading that file failed", + e); + loadError = ex; + throw ex; + } + return; + } + tryLoadFromClasspath(); loaded = true; } catch (NativeLoadException classpathMiss) { @@ -43,7 +70,10 @@ public static void ensureLoaded() { new NativeLoadException( "PDFium native library not found for " + detectPlatform() - + ". Also tried System.loadLibrary(\"pdfium\") and failed.", + + ". Also tried System.loadLibrary(\"pdfium\") and failed. " + + "Set -D" + + PROP_LIBRARY_PATH + + "=/path/to/libpdfium. to load a system copy explicitly.", classpathMiss); ex.addSuppressed(e); loadError = ex; @@ -51,9 +81,7 @@ public static void ensureLoaded() { } } catch (Throwable t) { loadError = t; - throw (t instanceof NativeLoadException nativeLoadException) - ? nativeLoadException - : new NativeLoadException("Failed to load native library", t); + throw new NativeLoadException("Failed to load native library", t); } } } From 492dcee05efe8f4d176fe8d879cc1a1d6b0e8603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= Date: Tue, 21 Apr 2026 13:13:34 +0200 Subject: [PATCH 2/3] refactor: simplify native library loading logic in NativeLoader --- .../pdfium4j/internal/NativeLoader.java | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java b/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java index 5936f68..ca568d8 100644 --- a/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java +++ b/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java @@ -39,46 +39,11 @@ public static void ensureLoaded() { throw new NativeLoadException("Native library failed to load previously", loadError); } try { - String overridePath = System.getProperty(PROP_LIBRARY_PATH); - if (overridePath != null && !overridePath.isBlank()) { - try { - System.load(overridePath); - loaded = true; - } catch (UnsatisfiedLinkError e) { - NativeLoadException ex = - new NativeLoadException( - "PDFium override path set via -D" - + PROP_LIBRARY_PATH - + "=" - + overridePath - + " but loading that file failed", - e); - loadError = ex; - throw ex; - } - return; - } - - tryLoadFromClasspath(); + performLoad(); loaded = true; - } catch (NativeLoadException classpathMiss) { - try { - System.loadLibrary("pdfium"); - loaded = true; - } catch (UnsatisfiedLinkError e) { - NativeLoadException ex = - new NativeLoadException( - "PDFium native library not found for " - + detectPlatform() - + ". Also tried System.loadLibrary(\"pdfium\") and failed. " - + "Set -D" - + PROP_LIBRARY_PATH - + "=/path/to/libpdfium. to load a system copy explicitly.", - classpathMiss); - ex.addSuppressed(e); - loadError = ex; - throw ex; - } + } catch (NativeLoadException e) { + loadError = e; + throw e; } catch (Throwable t) { loadError = t; throw new NativeLoadException("Failed to load native library", t); @@ -86,6 +51,44 @@ public static void ensureLoaded() { } } + private static void performLoad() { + try { + String overridePath = System.getProperty(PROP_LIBRARY_PATH); + if (overridePath != null && !overridePath.isBlank()) { + try { + System.load(overridePath); + return; + } catch (UnsatisfiedLinkError e) { + throw new NativeLoadException( + "PDFium override path set via -D" + + PROP_LIBRARY_PATH + + "=" + + overridePath + + " but loading that file failed", + e); + } + } + + tryLoadFromClasspath(); + } catch (NativeLoadException classpathMiss) { + try { + System.loadLibrary("pdfium"); + } catch (UnsatisfiedLinkError e) { + NativeLoadException ex = + new NativeLoadException( + "PDFium native library not found for " + + detectPlatform() + + ". Also tried System.loadLibrary(\"pdfium\") and failed. " + + "Set -D" + + PROP_LIBRARY_PATH + + "=/path/to/libpdfium. to load a system copy explicitly.", + classpathMiss); + ex.addSuppressed(e); + throw ex; + } + } + } + private static void tryLoadFromClasspath() { String platform = detectPlatform(); String resourceBase = "/natives/" + platform + "/"; From 02c0ec7bbf4b8af00d4010e7218dba53a82c9fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= Date: Tue, 21 Apr 2026 13:15:36 +0200 Subject: [PATCH 3/3] refactor: improve formatting and readability of comments in NativeLoader and PdfiumLibrary --- .../org/grimmory/pdfium4j/PdfiumLibrary.java | 51 +++++++++---------- .../pdfium4j/internal/NativeLoader.java | 6 +-- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java b/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java index c6c1c5d..22596fe 100644 --- a/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java +++ b/src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java @@ -15,30 +15,27 @@ * *

Concurrency model

* - *

Initialization is driven by the Initialization-on-demand holder idiom - * (JLS §12.4.2): the JVM class-loader lock guarantees {@code FPDF_InitLibraryWithConfig} - * is invoked exactly once per JVM, under the class-loader initialization lock, - * before any thread can observe a successful state. No explicit synchronized - * block, volatile read, or double-checked locking is needed on the hot path. + *

Initialization is driven by the Initialization-on-demand holder idiom (JLS §12.4.2): + * the JVM class-loader lock guarantees {@code FPDF_InitLibraryWithConfig} is invoked exactly once + * per JVM, under the class-loader initialization lock, before any thread can observe a successful + * state. No explicit synchronized block, volatile read, or double-checked locking is needed on the + * hot path. * - *

This makes {@link #initialize()} and {@link #isAvailable()} safe to call - * from any thread at any time, including concurrent {@code @BeforeAll} hooks - * in a parallel JUnit test run. + *

This makes {@link #initialize()} and {@link #isAvailable()} safe to call from any thread at + * any time, including concurrent {@code @BeforeAll} hooks in a parallel JUnit test run. * - *

After initialization, independent {@link PdfDocument} instances on separate - * threads are safe. A single document or page handle must not be accessed - * concurrently. + *

After initialization, independent {@link PdfDocument} instances on separate threads are safe. + * A single document or page handle must not be accessed concurrently. * *

System properties

* *
    - *
  • {@value #PROP_LIBRARY_PATH}, absolute filesystem path to - * a {@code libpdfium} binary. When set, this path is loaded directly via - * {@code System.load} and classpath extraction is skipped. Useful when - * pdfium is installed system-wide (e.g. an Alpine Docker image).
  • - *
  • {@value #PROP_AUTOINIT}, when {@code "false"}, {@link PdfDocument} - * operations will not auto-trigger initialization. Callers must invoke - * {@link #initialize()} explicitly. Default: {@code "true"}.
  • + *
  • {@value #PROP_LIBRARY_PATH}, absolute filesystem path to a {@code libpdfium} binary. + * When set, this path is loaded directly via {@code System.load} and classpath extraction is + * skipped. Useful when pdfium is installed system-wide (e.g. an Alpine Docker image). + *
  • {@value #PROP_AUTOINIT}, when {@code "false"}, {@link PdfDocument} operations will + * not auto-trigger initialization. Callers must invoke {@link #initialize()} explicitly. + * Default: {@code "true"}. *
*/ public final class PdfiumLibrary { @@ -64,10 +61,10 @@ public static void initialize() { /** * Non-throwing availability probe. * - *

Returns {@code true} if the native library has been successfully loaded and - * {@code FPDF_InitLibraryWithConfig} has been invoked. Returns {@code false} if - * initialization has failed, the cause is suppressed, never re-thrown from this - * method. Safe to call from any thread. + *

Returns {@code true} if the native library has been successfully loaded and {@code + * FPDF_InitLibraryWithConfig} has been invoked. Returns {@code false} if initialization has + * failed, the cause is suppressed, never re-thrown from this method. Safe to call from any + * thread. * *

This is the preferred way to gate optional functionality: * @@ -84,9 +81,9 @@ public static boolean isAvailable() { } /** - * Returns the Throwable that caused initialization to fail, or {@code null} - * if initialization succeeded or has not yet been attempted for reasons - * other than failure. Useful for diagnostics/logging without forcing a throw. + * Returns the Throwable that caused initialization to fail, or {@code null} if initialization + * succeeded or has not yet been attempted for reasons other than failure. Useful for + * diagnostics/logging without forcing a throw. */ public static Throwable loadError() { return Holder.STATE.loadError; @@ -97,7 +94,9 @@ static void ensureInitialized() { // Honour the explicit opt-out; require the caller has already run initialize(). if (!Holder.STATE.loaded) { throw new PdfiumException( - "PDFium auto-init is disabled (" + PROP_AUTOINIT + "=false); call PdfiumLibrary.initialize() explicitly"); + "PDFium auto-init is disabled (" + + PROP_AUTOINIT + + "=false); call PdfiumLibrary.initialize() explicitly"); } return; } diff --git a/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java b/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java index ca568d8..5762a64 100644 --- a/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java +++ b/src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java @@ -17,9 +17,9 @@ public final class NativeLoader { /** - * System property: absolute filesystem path to a pdfium native library. When set, - * {@link #ensureLoaded()} loads this path directly via {@code System.load} and - * skips both classpath extraction and {@code System.loadLibrary("pdfium")} lookup. + * System property: absolute filesystem path to a pdfium native library. When set, {@link + * #ensureLoaded()} loads this path directly via {@code System.load} and skips both classpath + * extraction and {@code System.loadLibrary("pdfium")} lookup. */ public static final String PROP_LIBRARY_PATH = "pdfium4j.library.path";