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
3 changes: 3 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ package/
PdfView.tsx public component (wrapper over the host component)
PdfCache.ts public cache API: AbortSignal, PdfDownloadError
PdfDocument.ts public document API: open (AbortSignal, PdfOpenError), close
PdfMethodError.ts methods' rejections: native `[code] message` → PdfMethodError
source.ts PdfSource union (or a list) → native [{ uri, base64, headers, cache options, password }]
nativeSource.ts base64 text → native-owned ArrayBuffer (once per distinct text)
test/base64/ the C++ and Swift base64 decoders against shared cases, and each other
Expand All @@ -56,6 +57,7 @@ package/
DocumentSetLoader.swift the view's sources in parallel; several assembled into one PDFDocument
DisplayedDocument.swift the displayed document and its sources (files, passwords, first pages)
PdfLoadFailure.swift Swift error → PdfError
PdfMethodFailure.swift a method's rejection as `[code] message`
DocumentCache.swift fresh copy or shared download/revalidation, stale fallback
DocumentCacheStore.swift copies on disk: session/persistent dirs, metadata, pins
SharedDownload.swift one URLSession request per copy, conditional with a copy
Expand Down Expand Up @@ -93,6 +95,7 @@ package/
DocumentReads.kt outline, metadata, page sizes, page images on duplicated FDs
(view session and document)
PageImageRequest.kt renderPage validation and file write (view and document)
PdfMethodException.kt a method's rejection as `[code] message`
outline/ bounded PDF parser: outline, page tree, /Info (PdfDate)
android/src/main/cpp/
cpp-adapter.cpp JNI_OnLoad: Nitro's natives, then NativeBase64Document's and NativePageCoverage's
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ PDFKit on iOS, the platform `PdfRenderer` on Android: no bundled PDFium, no WebV
- **Forms** — AcroForm fields filled in by touch or from code, read back by name and exported as a filled-in PDF.
- **Page overlays** — React views attached to points of a page, moved natively while it scrolls and zooms.
- **Document APIs** — outline, page sizes, metadata, page images and printing, with or without a view.
- **Password protection, typed errors, accessibility** — stable error codes identical on both platforms; VoiceOver and TalkBack read each page.
- **Password protection, typed errors, accessibility** — stable error codes identical on both platforms, for load failures and method calls alike; VoiceOver and TalkBack read each page.

## Requirements

Expand Down
34 changes: 34 additions & 0 deletions docs/api-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,40 @@ type PdfErrorCode =

`error.nativeDescription` carries the raw platform message for logs.

The ref's promise methods and `PdfDocument`'s reads reject with a
`PdfMethodError` and codes of their own:

```ts
type PdfMethodErrorCode =
| 'not-loaded' // before onLoad, while a new source loads
| 'closed' // closed or replaced while the call waited
| 'invalid-argument' // page, width, form value, export path
| 'not-supported' // several sources, a platform limit
| 'not-permitted' // printing forbidden, read-only field
| 'busy' // a print dialog is open
| 'io-error'
| 'invalid-pdf' // outline, metadata or form not parsable
| 'out-of-memory'
| 'unknown'; // see nativeDescription
```

- **A union of its own.** Adding these to `PdfErrorCode` would break apps
that switch exhaustively over `onError`'s codes.
- **A coded message, not result structs.** Nitro rejections carry only a
message. `open` and `download` resolve a result struct instead, but that
would have changed 11 signatures and every future method. Native code
rejects with `[code] message` (`PdfMethodFailure` in Swift,
`PdfMethodException` in Kotlin), and one JS function parses the first
line: Android puts the exception's class before it (renamed in minified
release builds) and the stack trace after it. A rejection without a
known code is `unknown`, the platform's text in `nativeDescription`.
- **Platform errors get a code by type**: file system errors are
`io-error` on both platforms, `OutOfMemoryError` `out-of-memory`, a parse
failure `invalid-pdf`.
- In 1.1 these methods rejected with plain `Error`s: on Android the message
was the exception's class, message and stack trace, and the same failure
read differently on each platform, so apps could only match on text.

## What we omit from react-native-pdf, and why

| react-native-pdf | Decision | Reason |
Expand Down
28 changes: 28 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ each page. The viewer adds no safe-area insets on either platform, like
| `search(query)` / `searchNext()` / `searchPrevious()` / `clearSearch()` | Full-text search. `search` is ignored before `onLoad` |
| `getStats()` | Render and cache statistics, for diagnostics |

The promise methods reject with a `PdfMethodError`; see
[Method errors](#method-errors).

`goToPage`, `zoomTo` and `goToViewport` ignore `NaN` and `±Infinity`, e.g.
`goToPage(Number(text) - 1)` while a page field holds a letter. A fractional
page is rounded to the nearest page on both platforms: `goToPage(1.6)` opens
Expand Down Expand Up @@ -115,6 +118,7 @@ pdf.current?.goToViewport(saved); // a position from onViewportChan
| `PdfCache` | Stored downloads: `download()`, `getEntry()`, `remove()`, `getByteSize()`, `clear()` |
| `PdfOpenError` | Rejection of `PdfDocument.open`, with the view's error `code` |
| `PdfDownloadError` | Rejection of `PdfCache.download`, with a `code` and `httpStatus` |
| `PdfMethodError` | Rejection of the ref's promise methods and `PdfDocument`'s reads, with a `code` |

## Errors

Expand Down Expand Up @@ -145,6 +149,29 @@ open and the other pages keep working:
`error.page !== undefined` tells them apart (it narrows the type to
`PdfPageError`). iOS reports load failures only (PDFKit draws the pages).

### Method errors

The ref's promise methods (`getOutline`, `getPageSizes`, `renderPage`,
`print`, `getFormFields`, `setFormFieldValues`, `exportDocument`) and
`PdfDocument`'s reads reject with a `PdfMethodError`. Its `code`
(`PdfMethodErrorCode`) is stable and identical on both platforms:

| Code | When |
| --- | --- |
| `not-loaded` | Before `onLoad`, also while a new `source` loads; before the view mounts |
| `closed` | `PdfDocument` after `close()`; the view closed or its `source` changed while the call waited for its turn (a call already running finishes) |
| `invalid-argument` | `renderPage`: a page outside the document, a `width` outside 1–4096 px, an image over 4096 × 4096 px. `setFormFieldValues`: an unknown field, a value its type does not take, text over its maximum length, an option it does not have. `exportDocument`: a `filePath` that is a directory, the displayed file or in the download cache |
| `not-supported` | Several sources (`print`, the form methods, `exportDocument`); a device that cannot print (iOS); filling forms on Android below 15 without SDK extension 13; selecting several options of a list box on iOS; clearing a radio group that cannot be turned off on Android; the outline and metadata of encrypted documents on Android |
| `not-permitted` | The document does not permit printing (iOS); a read-only form field |
| `busy` | A print dialog of the view is open; on Android, the app is not in the foreground |
| `io-error` | A file could not be read or written: a copy of the document, an export, a page image. The print job failed |
| `invalid-pdf` | The outline, metadata or form could not be parsed |
| `out-of-memory` | Out of memory, e.g. while rendering a page image |
| `unknown` | Anything else; see `nativeDescription` |

`error.message` is one sentence and may change; `error.nativeDescription`
is the platform's text (on Android with the stack trace), for logging.

## Types

| Type | Description |
Expand All @@ -155,6 +182,7 @@ open and the other pages keep working:
| `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`) |
| `PdfMethodErrorCode` | The stable codes of `PdfMethodError`, see [Method errors](#method-errors) |
| `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
8 changes: 4 additions & 4 deletions docs/guides/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const { filePath } = await pdf.current!.exportDocument(); // share or upload it
over the form field (kept above the keyboard) and a list to pick
choices from. `enableFormFilling={false}` leaves them as they are.
- A view showing several sources has no form to fill in: fields are drawn
but not editable, and the form methods reject (see
but not editable, and the form methods reject with `not-supported` (see
[Several documents as one](sources-and-loading.md#several-documents-as-one)).
- `onFormChange` reports the reader's changes only — at once for a toggle
or choice, when editing ends for text, never per keystroke — not
Expand All @@ -37,14 +37,14 @@ const { filePath } = await pdf.current!.exportDocument(); // share or upload it
coordinates as `onPageTap` — e.g. to place a `PdfPageOverlay` on a
signature field. Choices are their displayed labels.
- `setFormFieldValues()` sets fields by name, all or none: it rejects,
changing nothing, for an unknown or read-only field or a value its type
does not take.
changing nothing, for an unknown field or a value its type does not take
(`invalid-argument`) and for a read-only field (`not-permitted`).
- `exportDocument()` writes the document as shown to a new file (the
cache directory by default, or `{ filePath }`, which must name a file,
not a directory), keeping its password protection, off the main thread;
the source file is never changed. A remote document's `onLoad`
`filePath` is the library's downloaded copy: exporting over it, or
anywhere else in that cache, is rejected.
anywhere else in that cache, is rejected (`invalid-argument`).
Printing and `renderPage` show the form as filled in too.
- Password protection of an export: the password that opened the document
opens the export, with the same permissions (printing, copying, form
Expand Down
3 changes: 2 additions & 1 deletion docs/guides/page-images-and-documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,5 @@ try {
once renders them one at a time.
- **Call `close()`** when done: an open document keeps its file open until
then (or until the JS garbage collector releases it, which can take
long). Calls after `close()` reject.
long). Calls after `close()` reject with a `PdfMethodError` whose `code`
is `closed`.
17 changes: 9 additions & 8 deletions docs/guides/printing.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ if (result?.isSubmitted) showToast('Sent to the printer');
Works for `content://` sources, which have no file path.
- `jobName` names the job in the print queue and the file when saving as
PDF; by default the source's file name, or `'Document'`.
- Rejects before `onLoad`, while the view's print dialog is open, when the
view closes or its `source` changes before the dialog opens (a filled-in
form, and on iOS a protected document, is prepared first: seconds when
the document is long),
when the app is not in the foreground, and on iOS when the device cannot
print or the document's permissions forbid printing (Android has no API
to read them and prints anyway). Also rejects for a view showing several
sources (see
- Rejects with a `PdfMethodError` before `onLoad` (`not-loaded`), while
the view's print dialog is open or, on Android, when the app is not in
the foreground (`busy`), when the view closes or its `source` changes
before the dialog opens (`closed`; a filled-in form, and on iOS a
protected document, is prepared first: seconds when the document is
long), and on iOS when the device cannot print (`not-supported`) or the
document's permissions forbid printing (`not-permitted`; Android has no
API to read them and prints anyway). Also rejects for a view showing
several sources (`not-supported`, see
[Several documents as one](sources-and-loading.md#several-documents-as-one)).
On iPad the dialog opens as a popover from the view.
- Printing a file the app already has needs no viewer: `onLoad`'s
Expand Down
3 changes: 2 additions & 1 deletion docs/guides/sources-and-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ smaller than its base64 form.
shows none of them and `onError` reports `error.sourceIndex`. An empty
list is an `invalid-source` error. `onLoadProgress` sums the downloads.
- **Reading only:** forms cannot be filled in, and `print`,
`exportDocument`, `getFormFields` and `setFormFieldValues` reject.
`exportDocument`, `getFormFields` and `setFormFieldValues` reject with
`not-supported`.
Printing and exporting need one file, and Android has no API that merges
PDF files.
- Links within a source lead to its pages, and "next page" / "previous
Expand Down
4 changes: 2 additions & 2 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ For an overview of what each platform supports, see the
that page, copying ends the selection (as in a text field), dragging a
handle to the edge does not scroll, and a handle dragged past the other
end stops at the first character.
- **Outline (`getOutline`)**: on Android, encrypted documents reject (their
titles are encrypted). iOS (PDFKit) leaves PDF 1.1 named destinations
- **Outline (`getOutline`)**: on Android, encrypted documents reject with
`not-supported` (their titles are encrypted). iOS (PDFKit) leaves PDF 1.1 named destinations
(`/Dest /name`) without a page.
- **Metadata (`PdfDocument.getMetadata`)**: on Android, encrypted documents
reject, like the outline. XMP metadata is not read, only the information
Expand Down
8 changes: 8 additions & 0 deletions example/describeError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PdfMethodError, PdfOpenError } from 'react-native-better-pdf';

/** A rejection for the event line: an open error's code, a method error's code and message. */
export function describeError(error: unknown): string {
if (error instanceof PdfOpenError) return error.code;
if (error instanceof PdfMethodError) return `${error.code}: ${error.message}`;
return String(error);
}
4 changes: 2 additions & 2 deletions example/e2e/pdf-document.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ appId: software.gmi.betterpdf.example
- tapOn: "Thumbnails"
- extendedWaitUntil: { visible: "thumbnails: 2 in .* ms", timeout: 10000 }
- tapOn: "Close"
- extendedWaitUntil: { visible: "after close: The document is closed", timeout: 5000 }
- extendedWaitUntil: { visible: "after close: closed: The document is closed", timeout: 5000 }

# Downloaded like a PdfView source.
- tapOn: "metadata \\(remote\\)"
Expand Down Expand Up @@ -61,7 +61,7 @@ appId: software.gmi.betterpdf.example
- runFlow:
when: { platform: Android }
commands:
- assertVisible: "metadata failed: .*Encrypted documents are not supported.*"
- assertVisible: "metadata failed: not-supported: .*"
- assertVisible: "sizes: 595x842 595x842"
- assertVisible: "cover: 300x425"

Expand Down
2 changes: 1 addition & 1 deletion example/e2e/stress-close-during-work.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ appId: software.gmi.betterpdf.example
- tapOn: "Thumbnails"
- tapOn: "Close"
# Below the bottom edge on Android's screen.
- scrollUntilVisible: { element: "after close: The document is closed", timeout: 30000 }
- scrollUntilVisible: { element: "after close: closed: The document is closed", timeout: 30000 }
# Still alive.
- tapOn: "Viewer"
- runFlow: { file: _open-list-entry.yaml, env: { ENTRY: "Bundled sample.*" } }
Expand Down
6 changes: 3 additions & 3 deletions example/e2e/viewer-forms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ env:
# Rejected while the form is as loaded, when iOS exports a copy of the file:
# an export into a directory used to delete it with everything in it.
- tapOn: "Bad"
- extendedWaitUntil: { visible: "bad: .*read-only.* \\| .*cannot replace the displayed.* \\| .*export path is a directory.*; keep.txt kept", timeout: 10000 }
- extendedWaitUntil: { visible: "bad: not-permitted: .* \\| invalid-argument: .* \\| invalid-argument: .*; keep.txt kept", timeout: 10000 }

# By touch: text is reported once, when editing ends; toggles at once.
- tapOn: { point: "${NAME}" }
Expand Down Expand Up @@ -85,7 +85,7 @@ env:
visible: "fields 13: name=Ewa email=jan@example.com notes=First line/Second line agree=on plan=pro country=France colours=Blue id=A-1234 phone= address.city=Kraków postcode=00-950 date= signature=unsigned"
timeout: 5000
- tapOn: "Bad"
- extendedWaitUntil: { visible: "bad: .*read-only.* \\| .*cannot replace the displayed.* \\| .*export path is a directory.*; keep.txt kept", timeout: 10000 }
- extendedWaitUntil: { visible: "bad: not-permitted: .* \\| invalid-argument: .* \\| invalid-argument: .*; keep.txt kept", timeout: 10000 }
- tapOn: "Fields"
- extendedWaitUntil: { visible: "fields 13: name=Ewa .*", timeout: 5000 }

Expand Down Expand Up @@ -122,7 +122,7 @@ env:
- tapOn: "Fill"
- extendedWaitUntil: { visible: "fill: done", timeout: 5000 }
- tapOn: "Bad"
- extendedWaitUntil: { visible: "bad: .*read-only.* \\| .*cannot replace the displayed.* \\| .*export path is a directory.* \\| .*in the cache of downloaded documents.*; keep.txt kept", timeout: 10000 }
- extendedWaitUntil: { visible: "bad: not-permitted: .* \\| invalid-argument: .* \\| invalid-argument: .* \\| invalid-argument: .*; keep.txt kept", timeout: 10000 }
- tapOn: "‹ Back"
- runFlow: { file: _open-list-entry.yaml, env: { ENTRY: "Remote form(, .*)?" } }
- extendedWaitUntil: { visible: "1 / 2", timeout: 15000 }
Expand Down
2 changes: 1 addition & 1 deletion example/e2e/viewer-sources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ appId: software.gmi.betterpdf.example

# Viewing only: printing a set is rejected.
- tapOn: "Print"
- extendedWaitUntil: { visible: "print: .*several sources.*", timeout: 5000 }
- extendedWaitUntil: { visible: "print: not-supported: Printing is not available with several sources", timeout: 5000 }
- tapOn: "‹ Back"

# All or nothing: a failing source fails the load and is named.
Expand Down
9 changes: 1 addition & 8 deletions example/screens/DocumentsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { useEffect, useRef, useState } from 'react';
import { Image, ScrollView, StyleSheet, Text, View } from 'react-native';
import {
PdfDocument,
PdfOpenError,
type PdfDocumentMetadata,
type PdfOutlineItem,
type PdfPageImage,
type PdfSource,
} from 'react-native-better-pdf';
import { describeError } from '../describeError';
import { ensureFixture, FIXTURE_SERVER, type FixtureName } from '../documents';
import { Button, colors } from '../ui';

Expand Down Expand Up @@ -50,13 +50,6 @@ const TARGETS: Target[] = [

const MAX_THUMBNAILS = 12;

/** An open error's code, else the message's first line (on Android the stack follows). */
function describeError(error: unknown): string {
if (error instanceof PdfOpenError) return error.code;
const message = error instanceof Error ? error.message : String(error);
return message.split('\n')[0] ?? message;
}

const date = (ms: number | undefined) =>
ms === undefined ? '-' : new Date(ms).toISOString();

Expand Down
Loading
Loading