diff --git a/docs/api.md b/docs/api.md index 80eb860..f25544f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 | @@ -123,7 +123,7 @@ 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 @@ -131,6 +131,20 @@ carries the platform error for logging. Load failures carry 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 | @@ -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 }` | diff --git a/docs/guides/sources-and-loading.md b/docs/guides/sources-and-loading.md index 74ca861..ff86257 100644 --- a/docs/guides/sources-and-loading.md +++ b/docs/guides/sources-and-loading.md @@ -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 }) => @@ -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. diff --git a/docs/limitations.md b/docs/limitations.md index 33b1c46..63636a3 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -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 diff --git a/docs/migrating-from-react-native-pdf.md b/docs/migrating-from-react-native-pdf.md index 8313d91..61e5141 100644 --- a/docs/migrating-from-react-native-pdf.md +++ b/docs/migrating-from-react-native-pdf.md @@ -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. | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 08c5fb4..b97f985 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -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. | diff --git a/example/screens/ViewerScreen.tsx b/example/screens/ViewerScreen.tsx index c2d9462..0ac1e7e 100644 --- a/example/screens/ViewerScreen.tsx +++ b/example/screens/ViewerScreen.tsx @@ -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 && ( { ]); }); + 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(); + render( + { + 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( {}} />); expect(lastHost().onLoad).toBeDefined(); diff --git a/package/src/PdfView.tsx b/package/src/PdfView.tsx index baa641e..9924650 100644 --- a/package/src/PdfView.tsx +++ b/package/src/PdfView.tsx @@ -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'; @@ -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 @@ -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), ), diff --git a/package/src/index.ts b/package/src/index.ts index ed6e9bd..0377e02 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -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'; diff --git a/package/src/types/PdfError.ts b/package/src/types/PdfError.ts index b568ddf..632a2cc 100644 --- a/package/src/types/PdfError.ts +++ b/package/src/types/PdfError.ts @@ -8,13 +8,17 @@ 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`. */ @@ -22,3 +26,22 @@ export interface PdfError { /** 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; + 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; +}