From 3d1bade048431ec0d124f1fa7bf83d60a6afb248 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Wed, 29 Jul 2026 14:35:17 -0500 Subject: [PATCH 1/3] wip - saving 5.x effort, pivoting back to main and using 4.0.3 and see if it fixes the reproducable failure. --- android/build.gradle | 34 +++++++++--- .../src/main/java/org/wonday/pdf/PdfView.java | 55 ++++++++++++++++--- index.js | 16 +++--- 3 files changed, 80 insertions(+), 25 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 34da3248..46d9937c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -19,13 +19,9 @@ repositories { maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url "$projectDir/../node_modules/react-native/android" - content { - // Use Jitpack only for AndroidPdfViewer; the rest is hosted at mavenCentral. - includeGroup "com.github.zacharee" - } } - maven { url 'https://jitpack.io' } google() + maven { url 'https://jitpack.io' } } apply plugin: 'com.android.library' @@ -104,7 +100,7 @@ android { compileSdkVersion safeExtGet('compileSdkVersion', 31) defaultConfig { - minSdkVersion safeExtGet('minSdkVersion', 21) + minSdkVersion safeExtGet('minSdkVersion', 24) targetSdkVersion safeExtGet('targetSdkVersion', 31) buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()) } @@ -141,8 +137,28 @@ dependencies { } // NOTE: The original repo at com.github.barteksc is abandoned by the maintainer; there will be no more updates coming from that repo. // The repo from zacharee is based on PdfiumAndroidKt, a much newer fork of PdfiumAndroid, with better maintenance and updated native libraries. - implementation 'com.github.zacharee:AndroidPdfViewer:4.0.1' - // Depend on PdfiumAndroidKt directly so this can be updated independently of AndroidPdfViewer as updates are provided. - implementation 'io.legere:pdfiumandroid:1.0.32' + // AndroidPdfViewer 5.0.0 is not yet published to a registry accessible from Walmart's network; + // the AAR was built from source (tag 5.0.0) and bundled locally under android/libs/. + implementation files('libs/AndroidPdfViewer-5.0.0.aar') + // Transitive deps of AndroidPdfViewer 5.0.0 (not pulled automatically from a local .aar file) + implementation 'androidx.core:core-ktx:1.18.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2' + // runtimeOnly: PdfView.java has no direct pdfiumandroid imports; keeping it off the + // Kotlin compile classpath avoids a Kotlin 2.2.x compiler crash on Kotlin 2.3.x metadata. + // Exclude kotlin-stdlib transitively: Gradle's consistent-resolution propagates the + // highest runtime kotlin-stdlib version as a strict constraint onto the compile + // classpath. With kotlin-stdlib excluded from pdfiumandroid's runtime graph, Gradle + // resolves stdlib from the app's own deps (2.1.x), which the 2.2.x compiler handles. + // pdfiumandroid will use whatever kotlin-stdlib the app provides at runtime (2.1.x+). + runtimeOnly('io.legere:pdfiumandroid:2.0.1') { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib' + } + // pdfiumandroid-api is needed at compile time: PDFView.getTableOfContents() returns + // List and LinkTapEvent.getLink() returns Link — both from this artifact. + // Exclude kotlin-stdlib to keep the compile classpath on 2.2.x and avoid the + // FirIncompatibleClassTypeChecker NPE in the Kotlin 2.2.x compiler. + implementation('io.legere:pdfiumandroid-api:2.0.1') { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib' + } implementation 'com.google.code.gson:gson:2.13.2' } diff --git a/android/src/main/java/org/wonday/pdf/PdfView.java b/android/src/main/java/org/wonday/pdf/PdfView.java index e9a1d16c..46d0dd4f 100644 --- a/android/src/main/java/org/wonday/pdf/PdfView.java +++ b/android/src/main/java/org/wonday/pdf/PdfView.java @@ -26,11 +26,6 @@ import android.graphics.Canvas; import android.graphics.pdf.PdfRenderer; -import io.legere.pdfiumandroid.util.Config; -import io.legere.pdfiumandroid.util.ConfigKt; -import io.legere.pdfiumandroid.util.AlreadyClosedBehavior; -import io.legere.pdfiumandroid.DefaultLogger; - import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIManagerHelper; import com.github.barteksc.pdfviewer.PDFView; @@ -94,9 +89,16 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl private int oldW = 0; private int oldH = 0; + // When a PDF has disconnected /Pages sub-trees (malformed merge), PDFium can open + // only the pages reachable from the catalog root and throws "Unable to open page" + // for any beyond that boundary. We detect this on first error, cap the page count + // to the last successfully opened page, and reload silently so the user sees the + // valid portion of the document instead of a blank error screen. + private int accessiblePageCount = -1; // -1 = unknown / no restriction + private boolean retriedWithPageLimit = false; + public PdfView(Context context, AttributeSet set){ super(context, set); - ConfigKt.setPdfiumConfig(new Config(new DefaultLogger(), AlreadyClosedBehavior.IGNORE)); } @Override @@ -183,11 +185,35 @@ public void loadComplete(int numberOfPages) { @Override public void onError(Throwable t){ + // Graceful recovery for PDFs with disconnected /Pages sub-trees (malformed merges). + // PDFium reports "Unable to open page, pageIndex=N" when it can't reach a page + // through the catalog. On the first such error we parse N, cap the accessible + // page count, and reload – showing whatever the PDF does contain. + String msg = t.getMessage() != null ? t.getMessage() : ""; + if (!retriedWithPageLimit && msg.contains("Unable to open page")) { + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("pageIndex=(\\d+)") + .matcher(msg); + if (m.find()) { + int failedAt = Integer.parseInt(m.group(1)); + if (failedAt > 0) { + showLog("PDF has inaccessible pages from index " + failedAt + + "; reloading with first " + failedAt + " pages only."); + accessiblePageCount = failedAt; + retriedWithPageLimit = true; + // Reset to first page if current page is beyond accessible range + if (this.page > failedAt) this.page = 1; + new Handler(Looper.getMainLooper()).post(this::drawPdf); + return; + } + } + } + WritableMap event = Arguments.createMap(); - if (t.getMessage().contains("Password required or incorrect password")) { + if (msg.contains("Password required or incorrect password")) { event.putString("message", "error|Password required or incorrect password."); } else { - event.putString("message", "error|"+t.getMessage()); + event.putString("message", "error|"+msg); } ThemedReactContext context = (ThemedReactContext) getContext(); @@ -347,6 +373,14 @@ public void drawPdf() { .linkHandler(this) ; + // If we previously hit an "Unable to open page" error, restrict rendering + // to only the pages PDFium can actually reach through the catalog root. + if (accessiblePageCount > 0 && !enableRTL && !singlePage) { + int[] validPages = new int[accessiblePageCount]; + for (int i = 0; i < accessiblePageCount; i++) validPages[i] = i; + configurator.pages(validPages); + } + if (enableRTL) { try { int pageCount = getPdfPageCount(new File(this.path)); @@ -379,6 +413,11 @@ public void setEnableDoubleTapZoom(boolean enableDoubleTapZoom) { } public void setPath(String path) { + // Reset page-limit recovery state when a new PDF is loaded. + if (path != null && !path.equals(this.path)) { + this.accessiblePageCount = -1; + this.retriedWithPageLimit = false; + } this.path = path; } diff --git a/index.js b/index.js index 79994ba7..613a62f4 100644 --- a/index.js +++ b/index.js @@ -7,23 +7,23 @@ */ 'use strict'; -import React, {Component} from 'react'; +import { ViewPropTypes } from 'deprecated-react-native-prop-types'; import PropTypes from 'prop-types'; +import { Component } from 'react'; import { - View, + Image, Platform, StyleSheet, - Image, Text, + View, requireNativeComponent } from 'react-native'; +import ReactNativeBlobUtil from 'react-native-blob-util'; import PdfViewNativeComponent, { Commands as PdfViewCommands, - } from './fabric/RNPDFPdfNativeComponent'; -import ReactNativeBlobUtil from 'react-native-blob-util' -import {ViewPropTypes} from 'deprecated-react-native-prop-types'; -const SHA1 = require('crypto-js/sha1'); +} from './fabric/RNPDFPdfNativeComponent'; import PdfView from './PdfView'; +const SHA1 = require('crypto-js/sha1'); export default class Pdf extends Component { @@ -272,7 +272,7 @@ export default class Pdf extends Component { // open(path) race with the in-flight delete on Android 14 + New Architecture and // surface as `ENOENT (No such file or directory)` on the temp file. See #1018. await this._unlinkFile(tempCacheFile); - + try{ this.lastRNBFTask = null; const responseInfo = res ? res.respInfo : undefined; From b10fd18ada1c46dee874edab6109f243f6a9258c Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Fri, 31 Jul 2026 10:53:15 -0500 Subject: [PATCH 2/3] fix for race condition on initial load of malformed pdfs. Cleanup of gradle repository that does not seem to be needed anymore since it is on maven central and not jitpack now. --- android/build.gradle | 30 +++------ .../src/main/java/org/wonday/pdf/PdfView.java | 63 ++++++++++++++----- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 46d9937c..5aefe429 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -20,8 +20,8 @@ repositories { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url "$projectDir/../node_modules/react-native/android" } - google() maven { url 'https://jitpack.io' } + google() } apply plugin: 'com.android.library' @@ -100,7 +100,7 @@ android { compileSdkVersion safeExtGet('compileSdkVersion', 31) defaultConfig { - minSdkVersion safeExtGet('minSdkVersion', 24) + minSdkVersion safeExtGet('minSdkVersion', 21) targetSdkVersion safeExtGet('targetSdkVersion', 31) buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()) } @@ -137,27 +137,15 @@ dependencies { } // NOTE: The original repo at com.github.barteksc is abandoned by the maintainer; there will be no more updates coming from that repo. // The repo from zacharee is based on PdfiumAndroidKt, a much newer fork of PdfiumAndroid, with better maintenance and updated native libraries. - // AndroidPdfViewer 5.0.0 is not yet published to a registry accessible from Walmart's network; - // the AAR was built from source (tag 5.0.0) and bundled locally under android/libs/. - implementation files('libs/AndroidPdfViewer-5.0.0.aar') - // Transitive deps of AndroidPdfViewer 5.0.0 (not pulled automatically from a local .aar file) - implementation 'androidx.core:core-ktx:1.18.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2' - // runtimeOnly: PdfView.java has no direct pdfiumandroid imports; keeping it off the - // Kotlin compile classpath avoids a Kotlin 2.2.x compiler crash on Kotlin 2.3.x metadata. - // Exclude kotlin-stdlib transitively: Gradle's consistent-resolution propagates the - // highest runtime kotlin-stdlib version as a strict constraint onto the compile - // classpath. With kotlin-stdlib excluded from pdfiumandroid's runtime graph, Gradle - // resolves stdlib from the app's own deps (2.1.x), which the 2.2.x compiler handles. - // pdfiumandroid will use whatever kotlin-stdlib the app provides at runtime (2.1.x+). - runtimeOnly('io.legere:pdfiumandroid:2.0.1') { + // kotlin-stdlib is excluded so that the host app's own kotlin-stdlib version (e.g. 2.1.x) wins + // Gradle's consistent-resolution. pdfviewer and pdfiumandroid require kotlin-stdlib 2.3.x, which + // conflicts with the {strictly } constraint added by the host app's kotlin-gradle-plugin. + // Both libraries are runtime-compatible with older stdlib versions. + implementation('dev.zwander:pdfviewer:5.0.0') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib' } - // pdfiumandroid-api is needed at compile time: PDFView.getTableOfContents() returns - // List and LinkTapEvent.getLink() returns Link — both from this artifact. - // Exclude kotlin-stdlib to keep the compile classpath on 2.2.x and avoid the - // FirIncompatibleClassTypeChecker NPE in the Kotlin 2.2.x compiler. - implementation('io.legere:pdfiumandroid-api:2.0.1') { + // Depend on PdfiumAndroidKt directly so this can be updated independently of AndroidPdfViewer as updates are provided. + implementation('io.legere:pdfiumandroid:2.0.1') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib' } implementation 'com.google.code.gson:gson:2.13.2' diff --git a/android/src/main/java/org/wonday/pdf/PdfView.java b/android/src/main/java/org/wonday/pdf/PdfView.java index 46d0dd4f..ec29a944 100644 --- a/android/src/main/java/org/wonday/pdf/PdfView.java +++ b/android/src/main/java/org/wonday/pdf/PdfView.java @@ -96,6 +96,10 @@ public class PdfView extends PDFView implements OnPageChangeListener,OnLoadCompl // valid portion of the document instead of a blank error screen. private int accessiblePageCount = -1; // -1 = unknown / no restriction private boolean retriedWithPageLimit = false; + // Set to true once loadComplete fires for the recovery load; used to detect stale + // loadError() callbacks (from concurrent loads) that arrive after a successful render + // and recycle() the view, wiping the PDF from screen. + private boolean loadCompleted = false; public PdfView(Context context, AttributeSet set){ super(context, set); @@ -152,6 +156,8 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) { @Override public void loadComplete(int numberOfPages) { + loadCompleted = true; + showLog("loadComplete pages=" + numberOfPages + " accessiblePageCount=" + accessiblePageCount + " retriedWithPageLimit=" + retriedWithPageLimit); SizeF pageSize = getPageSize(0); float width = pageSize.getWidth(); float height = pageSize.getHeight(); @@ -181,6 +187,18 @@ public void loadComplete(int numberOfPages) { //Log.e("ReactNative", gson.toJson(this.getTableOfContents())); + // When a malformed PDF triggered page-limit recovery, concurrent cancelled loads + // may call the library's internal loadError() → recycle() on the main thread + // shortly after this loadComplete fires, wiping the freshly rendered pages. + // Poll isRecycled() after a short delay and re-draw if the view was cleared. + if (retriedWithPageLimit) { + postDelayed(() -> { + if (isRecycled()) { + showLog("View was recycled after recovery loadComplete — re-drawing"); + drawPdf(); + } + }, 200); + } } @Override @@ -190,22 +208,31 @@ public void onError(Throwable t){ // through the catalog. On the first such error we parse N, cap the accessible // page count, and reload – showing whatever the PDF does contain. String msg = t.getMessage() != null ? t.getMessage() : ""; - if (!retriedWithPageLimit && msg.contains("Unable to open page")) { - java.util.regex.Matcher m = java.util.regex.Pattern - .compile("pageIndex=(\\d+)") - .matcher(msg); - if (m.find()) { - int failedAt = Integer.parseInt(m.group(1)); - if (failedAt > 0) { - showLog("PDF has inaccessible pages from index " + failedAt + - "; reloading with first " + failedAt + " pages only."); - accessiblePageCount = failedAt; - retriedWithPageLimit = true; - // Reset to first page if current page is beyond accessible range - if (this.page > failedAt) this.page = 1; - new Handler(Looper.getMainLooper()).post(this::drawPdf); - return; + if (msg.contains("Unable to open page")) { + if (!retriedWithPageLimit) { + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("pageIndex=(\\d+)") + .matcher(msg); + if (m.find()) { + int failedAt = Integer.parseInt(m.group(1)); + if (failedAt > 0) { + showLog("PDF has inaccessible pages from index " + failedAt + + "; reloading with first " + failedAt + " pages only."); + accessiblePageCount = failedAt; + retriedWithPageLimit = true; + // Reset to first page if current page is beyond accessible range + if (this.page > failedAt) this.page = 1; + new Handler(Looper.getMainLooper()).post(this::drawPdf); + return; + } } + // regex didn't match or failedAt == 0 — fall through to dispatch error + } else { + // Stale loadError() from a concurrent cancelled load — suppress it so the + // JS onError handler is not triggered. The postDelayed in loadComplete() + // handles any view-recycle side-effect from the library's internal loadError. + showLog("Suppressed stale 'Unable to open page' (retriedWithPageLimit=true): " + msg); + return; } } @@ -326,9 +353,10 @@ private int getPdfPageCount(File pdfFile) throws IOException { } public void drawPdf() { - showLog(format("drawPdf path:%s %s", this.path, this.page)); + loadCompleted = false; + showLog(format("drawPdf path:%s page=%s accessiblePageCount=%s retriedWithPageLimit=%s", this.path, this.page, accessiblePageCount, retriedWithPageLimit)); - if (this.path != null){ + if (this.path != null && !this.path.isEmpty()){ // set scale this.setMinZoom(this.minScale); @@ -417,6 +445,7 @@ public void setPath(String path) { if (path != null && !path.equals(this.path)) { this.accessiblePageCount = -1; this.retriedWithPageLimit = false; + this.loadCompleted = false; } this.path = path; } From 409eb5ccfe577e469f21e8ea9cf9fdad388280e3 Mon Sep 17 00:00:00 2001 From: Jim Porter Date: Mon, 10 Aug 2026 13:08:58 -0500 Subject: [PATCH 3/3] resync with main via -X ours duplicated these variables, removed. --- index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/index.js b/index.js index 9ffa18f2..54ab0e09 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,6 @@ */ 'use strict'; -import { ViewPropTypes } from 'deprecated-react-native-prop-types'; import PropTypes from 'prop-types'; import { Component } from 'react'; import { @@ -22,7 +21,6 @@ import ReactNativeBlobUtil from 'react-native-blob-util'; import PdfViewNativeComponent, { Commands as PdfViewCommands, } from './fabric/RNPDFPdfNativeComponent'; -import PdfView from './PdfView'; const SHA1 = require('crypto-js/sha1'); export default class Pdf extends Component { @@ -272,9 +270,6 @@ export default class Pdf extends Component { // surface as `ENOENT (No such file or directory)` on the temp file. See #1018. await this._unlinkFile(tempCacheFile); try{ - this.lastRNBFTask = null; - const responseInfo = res ? res.respInfo : undefined; - const res = await this.lastRNBFTask; this.lastRNBFTask = null; const responseInfo = res ? res.respInfo : undefined;