diff --git a/android/build.gradle b/android/build.gradle index 34da3248..5aefe429 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -19,10 +19,6 @@ 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() @@ -141,8 +137,16 @@ 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' + // 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' + } // Depend on PdfiumAndroidKt directly so this can be updated independently of AndroidPdfViewer as updates are provided. - implementation 'io.legere:pdfiumandroid:1.0.32' + 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 c3d32451..afbab95e 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; @@ -103,9 +98,20 @@ 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; + // 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); - ConfigKt.setPdfiumConfig(new Config(new DefaultLogger(), AlreadyClosedBehavior.IGNORE)); } @Override @@ -159,6 +165,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(); @@ -204,15 +212,60 @@ 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 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 (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; + } + } + 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(); @@ -340,9 +393,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); @@ -387,6 +441,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)); @@ -419,6 +481,12 @@ 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.loadCompleted = false; + } this.path = path; } diff --git a/index.js b/index.js index 916faa50..54ab0e09 100644 --- a/index.js +++ b/index.js @@ -7,33 +7,22 @@ */ 'use strict'; -import React, {Component} from 'react'; 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' +} from './fabric/RNPDFPdfNativeComponent'; const SHA1 = require('crypto-js/sha1'); -let PdfView; - -const getPdfView = () => { - if (!PdfView) { - const module = require('./PdfView'); - PdfView = module.default || module; - } - - return PdfView; -}; - export default class Pdf extends Component { static propTypes = { @@ -280,27 +269,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 = ReactNativeBlobUtil.config({ - // response data will be saved to this path if it has access right. - path: tempCacheFile, - trusty: this.props.trustAllCerts, - }) - .fetch( - source.method ? source.method : 'GET', - source.uri, - source.headers ? source.headers : {}, - source.body ? source.body : "" - ) - // listen to download progress event - .progress((received, total) => { - this.props.onLoadProgress && this.props.onLoadProgress(received / total); - if (this._mounted) { - this.setState({progress: received / total}); - } - }); - + try{ const res = await this.lastRNBFTask; this.lastRNBFTask = null; const responseInfo = res ? res.respInfo : undefined;