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
16 changes: 10 additions & 6 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 <hostVersion>} 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'
}
88 changes: 78 additions & 10 deletions android/src/main/java/org/wonday/pdf/PdfView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;
}

Expand Down
43 changes: 6 additions & 37 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down