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
19 changes: 17 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Every component, object and type is exported from the package root. The
| `onLoad` | `({ pageCount, firstPageWidth, firstPageHeight, filePath?, isStale, sources }) => void` | | first page's size in PDF points, as displayed (crop box, rotation applied); `filePath`: the local file being displayed (downloaded copy, local file or extracted asset; not for `{ base64 }`, `content://` or several sources); `isStale`: an outdated stored copy shown because the server could not be reached (see [Offline reading and caching](guides/offline-and-caching.md)); `sources`: each source's `{ pageCount, firstPage, filePath?, isStale }` |
| `onLoadProgress` | `({ loadedBytes, totalBytes? }) => void` | | remote downloads only (not when a stored copy is used); at most every 100 ms plus once on completion |
| `onPageChange` | `({ page, pageCount }) => void` | | zero-based |
| `onError` | `(error: PdfError) => void` | | |
| `onError` | `(error: PdfLoadError \| PdfPageError) => void` | | without `error.page`: the load failed and nothing is shown; with `error.page` (Android only): a page could not be rendered after `onLoad`, and the document stays open; see [Errors](#errors) |
| `onLinkPress` | `({ url }) => void` | | tap on a URL link; the viewer never opens URLs itself |
| `onPageTap` | `({ page, x, y }) => void` | | tap on a page outside links; zero-based page, `x`/`y` in PDF points from the top-left of the page as displayed (crop box, rotation applied) |
| `onScaleChange` | `({ scale }) => void` | | once per settled zoom change (pinch/double-tap end, `zoomTo`); relative to the fitted page |
Expand Down Expand Up @@ -123,14 +123,28 @@ pdf.current?.goToViewport(saved); // a position from onViewportChan
`invalid-source` · `file-not-found` · `network-error` · `http-error`
(`error.httpStatus`) · `invalid-pdf` · `password-required` ·
`invalid-password` · `unsupported-pdf` · `page-render-failed` (`error.page`) ·
`out-of-memory`
`out-of-memory` (`error.page` while rendering)

`error.message` is human-readable and may change; `error.nativeDescription`
carries the platform error for logging. Load failures carry
`error.sourceIndex`, the source that could not be loaded (`0` for a single
source; unset when the set failed as a whole, as when Android runs out of
memory laying out many sources' pages).

On Android, `onError` also comes after `onLoad` when a page cannot be
rendered: `page-render-failed`, or `out-of-memory` while rendering, both
with `error.page` and without `error.sourceIndex`. The document stays
open and the other pages keep working:

- `page-render-failed`: the page stays blank until the document is loaded
again, e.g. with a new `key`. Each page is reported once.
- `out-of-memory`: the viewer frees memory and tries the page again later.
Reported once per document.

`onError` receives a `PdfLoadError` or a `PdfPageError`, and
`error.page !== undefined` tells them apart (it narrows the type to
`PdfPageError`). iOS reports load failures only (PDFKit draws the pages).

## Types

| Type | Description |
Expand All @@ -140,6 +154,7 @@ memory laying out many sources' pages).
| `PdfSource` / `PdfUriSource` / `PdfFileSource` / `PdfBase64Source` | Where a document comes from: a URI (with cache options for downloads), a file path, a base64 string or a bundled asset; all but the asset can carry their own `password` |
| `PdfLoadedSource` | One source as `onLoad` reports it: `{ pageCount, firstPage, filePath?, isStale }` |
| `PdfError` / `PdfErrorCode` | Errors and their stable codes |
| `PdfLoadError` / `PdfPageError` | `onError`'s argument: a load failure, or a page that could not be rendered after `onLoad` (Android, with `page`) |
| `PdfViewport` | A reading position: `{ page, x, y, scale }` |
| `PdfFormField` | A form field, discriminated by `type` (`PdfTextFormField`, `PdfCheckboxFormField`, …) |
| `PdfOutlineItem` | An outline entry: `{ title, page?, children }` |
Expand Down
19 changes: 17 additions & 2 deletions docs/guides/sources-and-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ smaller than its base64 form.

## Loading state

There is no built-in spinner. Show your own until `onLoad` or `onError`;
for remote documents `onLoadProgress` reports the download:
There is no built-in spinner. Show your own until `onLoad`, or `onError`
without `error.page`; for remote documents `onLoadProgress` reports the
download:

```tsx
onLoadProgress={({ loadedBytes, totalBytes }) =>
Expand All @@ -162,4 +163,18 @@ onLoadProgress={({ loadedBytes, totalBytes }) =>

`totalBytes` is `undefined` when the server does not report a size.

An `onError` with `error.page` is not a load failure: on Android, a page
that cannot be rendered is reported after `onLoad`, and the rest of the
document stays open. Keep the viewer on screen:

```tsx
onError={(error) => {
if (error.page !== undefined) {
console.warn(`${error.code} on page ${error.page + 1}`);
} else {
setLoadError(error); // nothing is shown
}
}}
```

See the [API reference](../api.md#errors) for the error codes.
4 changes: 4 additions & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ For an overview of what each platform supports, see the
from the viewport is visited.
- Documents are parsed in-process; the platform recommends an isolated
process for untrusted input, which this library does not provide.
- A page that cannot be rendered, or running out of memory while
rendering, is reported through `onError` after `onLoad` (with
`error.page`), and the document stays open (see
[Errors](api.md#errors)). iOS reports load failures only.
- **iOS**: PDFKit does not expose cache statistics; `getStats()` reports
`-1` for them.
- A base64 source's text is held twice while the document is shown (your
Expand Down
2 changes: 1 addition & 1 deletion docs/migrating-from-react-native-pdf.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ users see.
| **Page numbers** | 1-based everywhere (`page`, `setPage`, `onPageChanged`, `onPageSingleTap`) | 0-based everywhere | Add or subtract 1 at the boundary. |
| **TLS certificates** | `trustAllCerts` defaults to **`true`** — invalid certificates are accepted | Certificates are always validated | Fix the server's certificate. Downloads that "worked" before may now fail with `network-error`. |
| **Download progress value** | `onLoadProgress(fraction)` — `0…1`, despite the name | `onLoadProgress({ loadedBytes, totalBytes? })` | `totalBytes === undefined ? undefined : loadedBytes / totalBytes` |
| **Loading UI** | Shows `xx.xx%` text (or `renderActivityIndicator`) while downloading | Shows nothing | Render your own indicator until `onLoad` / `onError`. |
| **Loading UI** | Shows `xx.xx%` text (or `renderActivityIndicator`) while downloading | Shows nothing | Render your own indicator until `onLoad`, or `onError` without `error.page`. |
| **Background** | `#EEE` behind the pages | Transparent | `style={{ backgroundColor: '#EEE' }}` |
| **Fit** | `fitPolicy` default `2` (fit whole page) | `'width'` when vertical and continuous; `'page'` with `horizontal` or `enablePaging` | Pass `fitPolicy="page"` to keep react-native-pdf's default. |
| **Max zoom** | `maxScale` default `3` | `4` | Pass `maxScale={3}` to keep the old limit. |
Expand Down
1 change: 1 addition & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| Errors about the legacy architecture | Enable the New Architecture; Nitro Views need Fabric. |
| A password-protected document fails with `unsupported-pdf` on Android | The device is below Android 15 without SDK extension 13, where the platform renderer cannot open protected files. |
| Search reports `isSupported: false`, long presses select nothing | Same devices as above; see the [capability matrix](../README.md#capability-matrix). |
| On Android, `onError` arrives while the document is shown | It has `error.page`: one page could not be rendered after `onLoad` (`page-render-failed`, or `out-of-memory`), and the document stays open. Keep the viewer on screen; only `onError` without `page` ends the load. See [Errors](api.md#errors). |
| A remote document fails with `invalid-pdf` | The server answered with something else, often a login page: check the URL and `headers`. |
| A remote document fails with `invalid-source` | Check that the URL is `http(s)://` and the `headers`: often a token read with a trailing newline. `error.message` names the header. |
| A signed URL or rotating token downloads the document on every open | Set `cacheKey` on the source to identify the stored copy. |
Expand Down
12 changes: 9 additions & 3 deletions example/screens/ViewerScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -552,9 +552,15 @@ export function ViewerScreen() {
`tap: page ${event.page + 1} at ${Math.round(event.x)}, ${Math.round(event.y)} pt`,
)
}
onError={(pdfError) =>
setState({ kind: 'failed', source, error: pdfError })
}
onError={(pdfError) => {
if (pdfError.page != null) {
setLastEvent(
`error: ${pdfError.code} on page ${pdfError.page + 1}`,
);
} else {
setState({ kind: 'failed', source, error: pdfError });
}
}}
>
{entry.overlays === true && (
<DemoOverlays
Expand Down
27 changes: 27 additions & 0 deletions package/src/PdfView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ mock.module(
const { PdfView } = await import('./PdfView');
const { PdfPageOverlay } = await import('./PdfPageOverlay');
import type { PdfViewRef } from './PdfView';
import type { PdfError } from './types/PdfError';
import type { PdfViewStats } from './types/PdfViewStats';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -365,6 +366,32 @@ describe('PdfView', () => {
]);
});

test('forwards load and page errors, typed apart by page', () => {
const received: string[] = [];
// Handlers typed with PdfError, written before the split, still compile.
const untyped: (error: PdfError) => void = () => {};
render(<PdfView source={{ uri: 'x' }} onError={untyped} />);
render(
<PdfView
source={{ uri: 'x' }}
onError={(error) => {
if (error.page !== undefined) {
const code: 'page-render-failed' | 'out-of-memory' = error.code;
received.push(`${code} on page ${error.page + 1}`);
return;
}
// @ts-expect-error A load failure is never page-render-failed.
if (error.code === 'page-render-failed') return;
received.push(`load ${error.code}`);
}}
/>,
);
const onError = lastHost().onError as { f: (error: PdfError) => void };
onError.f({ code: 'invalid-pdf', message: 'Not a PDF', sourceIndex: 0 });
onError.f({ code: 'out-of-memory', message: 'Out of memory', page: 2 });
expect(received).toEqual(['load invalid-pdf', 'out-of-memory on page 3']);
});

test('passes no native callback for events the app does not listen to', () => {
render(<PdfView source={{ uri: 'x' }} onLoad={() => {}} />);
expect(lastHost().onLoad).toBeDefined();
Expand Down
22 changes: 19 additions & 3 deletions package/src/PdfView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import type { NativePdfFormChangeEvent } from './types/NativePdfFormChangeEvent'
import type { NativePdfSource } from './types/NativePdfSource';
import type { PdfColorScheme } from './types/PdfColorScheme';
import type { PdfContentInset } from './types/PdfContentInset';
import type { PdfError } from './types/PdfError';
import type { PdfError, PdfLoadError, PdfPageError } from './types/PdfError';
import type { PdfExportOptions } from './types/PdfExportOptions';
import type { PdfExportResult } from './types/PdfExportResult';
import type { PdfFitPolicy } from './types/PdfFitPolicy';
Expand Down Expand Up @@ -404,7 +404,20 @@ export interface PdfViewProps extends ViewProps {
* server (see `PdfUriSource.cache` and `maxAgeMs`).
*/
onLoadProgress?: (event: PdfLoadProgressEvent) => void;
onError?: (error: PdfError) => void;
/**
* The document could not be loaded, or (Android only) one of its pages
* could not be rendered. `error.page !== undefined` tells them apart.
*
* - {@linkcode PdfLoadError}, without `page`: the load failed and nothing
* is shown.
* - {@linkcode PdfPageError}, with `page`, Android only: it comes after
* {@linkcode PdfViewProps.onLoad}, and the document stays open.
* `page-render-failed`: the page stays blank until the document is
* loaded again (e.g. a new `key`); each page is reported once.
* `out-of-memory`: the viewer frees memory and tries the page again
* later; reported once per document.
*/
onError?: (error: PdfLoadError | PdfPageError) => void;
onPageChange?: (event: PdfPageChangeEvent) => void;
/**
* The user tapped a link to a URL. Links to other pages of the document
Expand Down Expand Up @@ -625,7 +638,10 @@ export function PdfView({
onLoadProgress: callback((event: PdfLoadProgressEvent) =>
handlers.current.onLoadProgress?.(event),
),
onError: callback((error: PdfError) => handlers.current.onError?.(error)),
// Native sets `page` only for page failures.
onError: callback((error: PdfError) =>
handlers.current.onError?.(error as PdfLoadError | PdfPageError),
),
onPageChange: callback((event: PdfPageChangeEvent) =>
handlers.current.onPageChange?.(event),
),
Expand Down
2 changes: 1 addition & 1 deletion package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type { PdfCacheMode } from './types/PdfCacheMode';
export type { PdfColorScheme } from './types/PdfColorScheme';
export type { PdfContentInset } from './types/PdfContentInset';
export type { PdfDocumentMetadata } from './types/PdfDocumentMetadata';
export type { PdfError } from './types/PdfError';
export type { PdfError, PdfLoadError, PdfPageError } from './types/PdfError';
export type { PdfErrorCode } from './types/PdfErrorCode';
export type { PdfExportOptions } from './types/PdfExportOptions';
export type { PdfExportResult } from './types/PdfExportResult';
Expand Down
31 changes: 27 additions & 4 deletions package/src/types/PdfError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,40 @@ export interface PdfError {
code: PdfErrorCode;
/** Human-readable English description. Not stable — do not match on it. */
message: string;
/** Zero-based page index, set for `page-render-failed`. */
/**
* Zero-based page index, set when one page failed after `onLoad`
* (`page-render-failed`, and `out-of-memory` while rendering; Android
* only).
*/
page?: number;
/**
* Zero-based index in `PdfView`'s `source` of the document that could not
* be loaded, set by `PdfView` for every code but `page-render-failed`
* (`0` for a single source). Unset when several sources failed as a whole,
* as when Android runs out of memory laying out their pages.
* be loaded, set by `PdfView` for load failures (`0` for a single source).
* Unset for page failures, and when several sources failed as a whole, as
* when Android runs out of memory laying out their pages.
*/
sourceIndex?: number;
/** HTTP status code, set for `http-error`. */
httpStatus?: number;
/** Raw platform error description, for logging only. */
nativeDescription?: string;
}

/**
* `PdfView`'s `onError` argument for a document that could not be loaded:
* nothing is shown.
*/
export interface PdfLoadError extends PdfError {
code: Exclude<PdfErrorCode, 'page-render-failed'>;
page?: undefined;
}

/**
* `PdfView`'s `onError` argument for a page that could not be rendered after
* `onLoad` (Android only): the document stays open.
*/
export interface PdfPageError extends PdfError {
code: 'page-render-failed' | 'out-of-memory';
page: number;
sourceIndex?: undefined;
}
Loading