Skip to content
Merged
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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {

allprojects {
group = "org.grimmory"
version = "0.14.0"
version = "0.15.0"

repositories {
mavenCentral()
Expand Down
150 changes: 120 additions & 30 deletions src/main/java/org/grimmory/pdfium4j/PdfiumLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,53 +13,143 @@
* <p>PDFium requires a single call to {@code FPDF_InitLibraryWithConfig} before any document
* operations, and {@code FPDF_DestroyLibrary} at shutdown. This class handles both automatically.
*
* <p>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
* concurrently.
* <h2>Concurrency model</h2>
*
* <p>Initialization is driven by the <b>Initialization-on-demand holder idiom</b> (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.
*
* <p>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.
*
* <p>After initialization, independent {@link PdfDocument} instances on separate threads are safe.
* A single document or page handle must not be accessed concurrently.
*
* <h2>System properties</h2>
*
* <ul>
* <li><b>{@value #PROP_LIBRARY_PATH}</b>, 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).
* <li><b>{@value #PROP_AUTOINIT}</b>, when {@code "false"}, {@link PdfDocument} operations will
* not auto-trigger initialization. Callers must invoke {@link #initialize()} explicitly.
* Default: {@code "true"}.
* </ul>
*/
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();
}
Comment thread
balazs-szucs marked this conversation as resolved.

NativeLoader.ensureLoaded();
/**
* Non-throwing availability probe.
*
* <p>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.
*
* <p>This is the preferred way to gate optional functionality:
*
* <pre>{@code
* if (PdfiumLibrary.isAvailable()) {
* try (PdfDocument doc = PdfDocument.open(path)) { ... }
* }
* }</pre>
*
* @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;
}
Comment thread
balazs-szucs marked this conversation as resolved.

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);
}
}
}
67 changes: 49 additions & 18 deletions src/main/java/org/grimmory/pdfium4j/internal/NativeLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -32,28 +39,52 @@ public static void ensureLoaded() {
throw new NativeLoadException("Native library failed to load previously", loadError);
}
try {
tryLoadFromClasspath();
performLoad();
loaded = true;
} catch (NativeLoadException classpathMiss) {
} catch (NativeLoadException e) {
loadError = e;
throw e;
} catch (Throwable t) {
loadError = t;
throw new NativeLoadException("Failed to load native library", t);
}
}
}

private static void performLoad() {
try {
String overridePath = System.getProperty(PROP_LIBRARY_PATH);
if (overridePath != null && !overridePath.isBlank()) {
try {
System.loadLibrary("pdfium");
loaded = true;
System.load(overridePath);
return;
} catch (UnsatisfiedLinkError e) {
NativeLoadException ex =
new NativeLoadException(
"PDFium native library not found for "
+ detectPlatform()
+ ". Also tried System.loadLibrary(\"pdfium\") and failed.",
classpathMiss);
ex.addSuppressed(e);
loadError = ex;
throw ex;
throw new NativeLoadException(
"PDFium override path set via -D"
+ PROP_LIBRARY_PATH
+ "="
+ overridePath
+ " but loading that file failed",
e);
}
} catch (Throwable t) {
loadError = t;
throw (t instanceof NativeLoadException nativeLoadException)
? nativeLoadException
: new NativeLoadException("Failed to load native library", t);
}

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.<ext> to load a system copy explicitly.",
classpathMiss);
ex.addSuppressed(e);
throw ex;
}
}
}
Expand Down