# Migrating from MuPDF.js

Source: https://sdk.pdf.net/docs/v1/migration/from-mupdf



This guide moves a browser application built on MuPDF.js to `@sdk-pdf/react`. MuPDF.js renders and edits documents but has no viewer: the application renders pages into pixmaps, lays them out, builds its own text, link, and form layers, and turns pointer events into annotations. `@sdk-pdf/react` ships the page view and its interaction, so most of that code goes away and the rest moves to hooks. The guide covers MuPDF.js running in the browser, on the main thread or in a Web Worker. If you also run MuPDF.js on a server, the server keeps fetching and storing the PDF bytes, and server-side jobs such as rendering image files or redaction are listed in [Features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-mupdf#features-without-an-equivalent). Work through the checklist below; each step links to the section that covers it.

> **Beta**: `@sdk-pdf/react` is in beta, and its exports and APIs may change between releases. Pin the exact package version and review the changes when you update.

## Migration checklist [#migration-checklist]

* [ ] Read the [overview](https://sdk.pdf.net/docs/v1/migration/from-mupdf#overview): how the two integrations differ, and the API map for every MuPDF.js call you use.
* [ ] [Install the package and replace the `mupdf` import and its worker with `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-mupdf#install-and-set-up).
* [ ] [Load and reset documents](https://sdk.pdf.net/docs/v1/migration/from-mupdf#load-and-reset-a-document) with `loadPdf` and `resetPdf`, including the password flow.
* [ ] [Replace the pixmap render loop and your page layers with `Viewport`](https://sdk.pdf.net/docs/v1/migration/from-mupdf#display-the-document).
* [ ] [Move page navigation from indices to page ids](https://sdk.pdf.net/docs/v1/migration/from-mupdf#pages-and-navigation).
* [ ] [Move page insertion, deletion, and reordering to the page hooks](https://sdk.pdf.net/docs/v1/migration/from-mupdf#page-manipulation).
* [ ] [Map your zoom level and fit calculations to the viewport scale](https://sdk.pdf.net/docs/v1/migration/from-mupdf#zoom-and-fit-modes).
* [ ] [Rebuild thumbnails with `PageCanvas`](https://sdk.pdf.net/docs/v1/migration/from-mupdf#thumbnails).
* [ ] [Move text extraction and search](https://sdk.pdf.net/docs/v1/migration/from-mupdf#text-extraction-and-search).
* [ ] [Move widget reading and filling to the widget hooks](https://sdk.pdf.net/docs/v1/migration/from-mupdf#forms).
* [ ] [Map annotation creation to interaction tools](https://sdk.pdf.net/docs/v1/migration/from-mupdf#editing-and-interaction-tools).
* [ ] [Replace the journal and `hasUnsavedChanges()`](https://sdk.pdf.net/docs/v1/migration/from-mupdf#undo-redo-and-change-tracking).
* [ ] [Replace `saveToBuffer()` and downloads](https://sdk.pdf.net/docs/v1/migration/from-mupdf#saving-and-downloading).
* [ ] Convert stored positions using [coordinates](https://sdk.pdf.net/docs/v1/migration/from-mupdf#coordinates).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-mupdf#errors-and-logging) and [drop the `destroy()` calls](https://sdk.pdf.net/docs/v1/migration/from-mupdf#cleanup-and-lifecycle).
* [ ] [Plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-mupdf#features-without-an-equivalent).
* [ ] Remove `mupdf`, the worker script and its message protocol, and the WebAssembly file you serve. Test encrypted, malformed, large, and form-heavy documents, and the save round-trip.

## Overview [#overview]

<details>
  <summary>
    How the two integrations differ
  </summary>

  **MuPDF.js** is the MuPDF C library compiled to WebAssembly, with an object-oriented JavaScript API on top. It renders and edits documents and leaves every piece of UI to you:

  * It installs as `mupdf`, an ES module that ships its WebAssembly binary and TypeScript definitions, and runs in browsers, Node, and Bun.
  * Calls are synchronous: `Document.openDocument()` opens the bytes, `loadPage(index)` loads a page by zero-based index, and `page.toPixmap(matrix, colorspace)` renders it into a `Pixmap` that you draw on a canvas or encode as PNG. Rendering blocks the thread it runs on, so viewers run MuPDF.js in a Web Worker and post the images to the page.
  * For a PDF file, `openDocument` returns a `PDFDocument` and `loadPage` a `PDFPage`, which add widgets, annotations, the page tree, the undo journal, and saving.
  * The viewer is your code: page layout and scrolling, zoom by rendering again at another matrix, text selection and links as HTML layers built from `toStructuredText()` and `getLinks()`, inputs over the form widgets, and the pointer handlers that create annotations.
  * Edits change the loaded document in place, `saveToBuffer()` writes the PDF, and objects hold WebAssembly memory that you release with `destroy()`.
  * Coordinates are points with the origin at the top-left corner of the page.

  **@sdk-pdf/react** owns the document, and you own the UI:

  * [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the document and viewport state; every hook and component renders inside it.
  * [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) loads a document from bytes and reports a load state.
  * [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages and handles scrolling, selection, text editing, form filling, link navigation, and zoom and drag gestures on its own. [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders single pages anywhere else, such as thumbnails.
  * Toolbars and sidebars are your React components: state hooks re-render them on change, and command hooks change the document.
  * Pages are addressed by id, not by index: an id keeps pointing at the same page while pages are added, removed, and rearranged.
  * Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers undo/redo; the final PDF is built when you save.

  | Concern           | MuPDF.js                                                                   | @sdk-pdf/react                                                                 |
  | ----------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
  | Integration style | Synchronous objects, usually behind a Web Worker and your message protocol | A React provider, state hooks, and command hooks                               |
  | Rendering         | `toPixmap()` per page, drawn and laid out by your code                     | `Viewport` for the main view, `PageCanvas` for thumbnails and previews         |
  | Interaction       | Your own text, link, and form layers                                       | Built into `Viewport`, governed by the content policy                          |
  | Page addressing   | Zero-based page indices                                                    | Session-scoped page ids from `usePages`                                        |
  | Editing model     | Annotations, widget values, the page tree, and low-level PDF objects       | Page text, form widgets, images, and pages; no tools for markup annotations    |
  | Undo              | An opt-in journal of named operations                                      | Always on, one history for the whole document                                  |
  | Saving            | `saveToBuffer(options)`                                                    | `prepareDocument()` and `downloadDocument()`                                   |
  | Memory            | `destroy()` on every object                                                | Nothing to release                                                             |
  | File types        | PDF, XPS, CBZ, EPUB, FictionBook 2, and images                             | PDF                                                                            |
  | Diagnostics       | Thrown exceptions, `setLog()` for warnings and errors                      | Typed errors thrown before any change; `logMessages`, never printed to console |
  | Environment       | Browsers, Node, and Bun; framework-agnostic                                | React in the browser                                                           |

  What this means for your code:

  1. **Delete the render pipeline.** The worker and its message protocol, pixmap rendering, `ImageData` and PNG handling, lazy rendering with an `IntersectionObserver`, and the text and link layers move into `Viewport`.
  2. **Replace indices with ids.** Every page reference in your state becomes a page id; convert at the edges (deep links, stored positions) with `usePages` and `usePageIndexGetter`.
  3. **Replace rendering again with a scale.** A zoom level that you passed into a render matrix becomes the viewport scale, and the viewport zooms on pinch and wheel gestures too.
  4. **Rethink forms as widgets.** Loops over `getWidgets()` become `useWidgets`, and values are read and written through `useWidgetValue`, which re-renders as the value changes.
  5. **Rethink editing.** MuPDF.js creates annotations and writes page content through PDF objects; `@sdk-pdf/react` edits page text, widgets, image stamps, and pages through interaction tools and hooks, and creates ink annotations from code.
  6. **Drop memory management.** There are no pages, pixmaps, or documents to destroy.
  7. **Choose the policy.** The default is `PDF_EDITOR`; a reader needs `PDF_VIEWER` or `PDF_PRINTER`.
  8. **Move into React.** Code that drives MuPDF.js from outside React needs a React boundary.
</details>

<details>
  <summary>
    API map
  </summary>

  Rows marked **No equivalent** are covered in [Features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-mupdf#features-without-an-equivalent).

  | MuPDF.js                                                                    | @sdk-pdf/react                                                                                                                               | Notes                                                                 |
  | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
  | `import * as mupdf from 'mupdf'`, the worker and its messages               | [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders)                                                                                            | Nothing else to set up.                                               |
  | `Document.openDocument(data, 'application/pdf')`                            | `loadPdf(new Uint8Array(data))` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                           | Asynchronous, with a load state.                                      |
  | `needsPassword()`, `authenticatePassword(password)`                         | `loadPdf(data, { password })`, catch `InvalidPasswordError`                                                                                  | A rejection replaces the check.                                       |
  | `doc.destroy()` before opening another document                             | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf)                                                                            | Returns the load state to `'initial'`.                                |
  | `countPages()`                                                              | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                            | `0` while no document is loaded.                                      |
  | `loadPage(index)`                                                           | `usePages()[index].id` with [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages)                                                                        | Both zero-based.                                                      |
  | `page.getBounds()`                                                          | `width` and `height` of the `usePages` entry                                                                                                 | 1/72 of an inch, rotation applied.                                    |
  | `page.toPixmap()`, `getPixels()`, `asPNG()`, your canvases                  | [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport)                                                                                                    | One component, no layers to assemble.                                 |
  | Text and link layers from `toStructuredText()` and `getLinks()`             | `Viewport`, with the `textSelect` tool and interactive links                                                                                 | See [Display the document](https://sdk.pdf.net/docs/v1/migration/from-mupdf#display-the-document).                    |
  | `link.isExternal()`, `doc.resolveLink(link)`                                | Link navigation inside `Viewport`                                                                                                            | When the policy's `links` level is `'interactive'`.                   |
  | Scrolling your page element into view                                       | `scrollToPage(pageId)` from [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage)                                                          |                                                                       |
  | Tracking the visible page with an `IntersectionObserver`                    | [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId)                                                                                    | Re-renders with the page in the middle of the viewport.               |
  | `loadOutline()`, `page.getLabel()`                                          | **No equivalent**                                                                                                                            |                                                                       |
  | `insertPage(at, addPage(mediabox, rotate, resources, contents))`            | `createPage(width, height, insertAfterPageId?)` from [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate)                                     | Inserts after a page, or appends.                                     |
  | `deletePage(index)`                                                         | `deletePages(pageIdOrIds)` from [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete)                                                          |                                                                       |
  | `rearrangePages(order)`                                                     | `moveBefore` or `moveAfter` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove)                                                             | Moves pages, no full order.                                           |
  | A page listed twice in `rearrangePages`                                     | `duplicatePage(sourcePageId)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate)                                                 |                                                                       |
  | `graftPage(to, srcDoc, srcPage)`, `setPageBox()`                            | **No equivalent**                                                                                                                            | No page import or cropping.                                           |
  | A new document with `graftPage()` to extract or split pages                 | `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                              | Saves only the listed pages, in the listed order.                     |
  | A zoom level in the render matrix, `Matrix.scale(dpi / 72, dpi / 72)`       | `setScale(dpi / 72)` from [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale)                                                          | Scale `1` is one user-space unit per CSS pixel.                       |
  | A fit-width zoom computed from `getBounds()`                                | [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale), [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale)     | A one-time fit, not a persistent mode.                                |
  | Small pixmaps or PNGs as thumbnails                                         | [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas)                                                                                                | Lazy; shows the page as loaded. Showing session edits is coming soon. |
  | `toStructuredText().asText()`                                               | `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)                                       | Paragraph text and bounds.                                            |
  | `asJSON()`, `walk()`, structured text options                               | **No equivalent**                                                                                                                            | No lines, characters, or fonts.                                       |
  | `page.search(needle)`, `StructuredText.search()`                            | [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch)                                                                                  | The SDK highlights the matches; no hit quads.                         |
  | `StructuredText.highlight()`, `copy()`                                      | The `textSelect` tool in `Viewport`                                                                                                          | No API reads the selected text.                                       |
  | `page.getWidgets()`                                                         | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets), [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter)                                     | Every widget of the document, in reading order.                       |
  | `getValue()`, `setTextValue()`, `setChoiceValue()`, `toggle()`              | [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                                                                                        | One widget per hook call.                                             |
  | `getName()`, `getFieldType()`                                               | `fieldName` and `widgetType` of the widget snapshot                                                                                          | See [Forms](https://sdk.pdf.net/docs/v1/migration/from-mupdf#forms).                                                  |
  | `page.update()` and rendering the page again                                | Nothing to call                                                                                                                              | The view follows the document.                                        |
  | `enableJS()`                                                                | **No equivalent**                                                                                                                            | Acrobat date formats are recognized without running scripts.          |
  | `createAnnotation('FreeText')`                                              | `{ type: 'addText' }`                                                                                                                        | The nearest tool; it creates a text paragraph.                        |
  | Image content written through `PDFObject`, `Stamp` annotations              | `{ type: 'addImage', image }` with [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage)                                                             | Places a stamp annotation.                                            |
  | `createAnnotation('Stamp')` from code                                       | [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate)                                                                    | At a user-space position, with a staged image.                        |
  | `createAnnotation('Ink')`, `setInkList()`                                   | [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate)                                                                        | Strokes your code collects; there is no drawing tool.                 |
  | `createAnnotation()` for markup, shapes, notes, and attachments             | **No equivalent**                                                                                                                            | Existing shapes and lines display.                                    |
  | `getAnnotations()`                                                          | **No equivalent**                                                                                                                            | `useSelectedBlocks` returns what the user selected.                   |
  | Redaction annotations, `applyRedactions()`                                  | **No equivalent**                                                                                                                            |                                                                       |
  | `enableJournal()`, `undo()`, `redo()`, `canUndo()`, `canRedo()`             | [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo)                                                                                              | Always on.                                                            |
  | `beginOperation()`, `endOperation()`                                        | `markUndoBoundary()` from [`useUndoBoundary`](https://sdk.pdf.net/docs/v1/reference/useUndoBoundary)                                                            | Keeps an operation a separate step.                                   |
  | `getJournal()`                                                              | **No equivalent**                                                                                                                            | Steps are recorded without names.                                     |
  | `hasUnsavedChanges()`                                                       | [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision), [`useDocumentRevisionGetter`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevisionGetter) | Compare revisions to tell unsaved changes.                            |
  | `saveToBuffer(options).asUint8Array()`                                      | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                         | Resolves with a `Uint8Array`.                                         |
  | Write options (`incremental`, `garbage`, `compress`, `encrypt`)             | **No equivalent**                                                                                                                            | The options are `returnOriginalIfUnchanged` and `pageIds`.            |
  | A `Blob` and an object URL for downloads                                    | `downloadDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                        |                                                                       |
  | `bake()`                                                                    | **No equivalent**                                                                                                                            | No flattening.                                                        |
  | `setLog({ error, warning })`                                                | [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages), `LogLevel`                                                                                  | The SDK never prints to the console.                                  |
  | Exceptions relayed from the worker                                          | Errors thrown or rejected at the call                                                                                                        | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-mupdf#errors-and-logging).                        |
  | `destroy()`, `emptyStore()`, `worker.terminate()`                           | Nothing to call                                                                                                                              | See [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-mupdf#cleanup-and-lifecycle).                  |
  | `getMetaData()`, `setMetaData()`, `hasPermission()`, layers, embedded files | **No equivalent**                                                                                                                            |                                                                       |
  | `PDFPage.getTransform()`                                                    | **No equivalent**                                                                                                                            | See [Coordinates](https://sdk.pdf.net/docs/v1/migration/from-mupdf#coordinates).                                      |
  | `page.createLink()`                                                         | **No equivalent**                                                                                                                            |                                                                       |
  | `PDFObject`, `getTrailer()`, `addStream()`                                  | **No equivalent**                                                                                                                            | No low-level PDF objects or content streams.                          |
</details>

## Install and set up [#install-and-set-up]

### Before: MuPDF.js [#before-mupdfjs]

MuPDF.js installs as `mupdf` and loads only as an ES module. Importing it loads the WebAssembly binary that ships in the package, which you serve with your app. Viewers import it in a module worker, so rendering doesn't block the page:

```sh
npm install mupdf
```

```js
// mupdf.worker.js: MuPDF.js runs off the main thread.
import * as mupdf from 'mupdf'

let doc = null
```

```js
// main.js
const worker = new Worker(new URL('./mupdf.worker.js', import.meta.url), {
  type: 'module',
})
```

### After: @sdk-pdf/react [#after-sdk-pdfreact]

The package comes from a private registry. Add the `@sdk-pdf` scope to an `.npmrc` next to your `package.json`, keep the token in an environment variable, and install; [Getting started](https://sdk.pdf.net/docs/v1/guides/getting-started#installation) has the details:

```ini
@sdk-pdf:registry=https://registry.sdk.pdf.net
//registry.sdk.pdf.net/:_authToken=${PDF_NET_REGISTRY_TOKEN}
```

```sh
npm install @sdk-pdf/react
```

Set up the project as [Getting started](https://sdk.pdf.net/docs/v1/guides/getting-started#project-setup) describes: import the stylesheet, render the SDK on the client only, alias `canvas` in the bundler, and turn off React Strict Mode.

Then wrap the part of your app that shows documents in [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders). Its props are optional, and the PDF application re-renders whenever a prop changes identity, so define static configuration outside the component:

```tsx
import {
  CONTENT_POLICY_PRESET,
  Config,
  InteractionTool,
  PdfProviders,
} from '@sdk-pdf/react'
import '@sdk-pdf/react/style.css'

// Defined outside the component, so their identity never changes.
const theme: Config['theme'] = { viewportPadding: 16 }
const initialInteractionTool: InteractionTool = { type: 'textSelect' }

export function App() {
  return (
    <PdfProviders
      theme={theme}
      initialInteractionTool={initialInteractionTool}
      initialContentPolicy={CONTENT_POLICY_PRESET.PDF_VIEWER}
    >
      <DocumentLoader url="/files/document.pdf" />
    </PdfProviders>
  )
}
```

### What to know [#what-to-know]

* **Pick the content policy.** In a MuPDF.js viewer, users can do whatever your layers let them do. `PdfProviders` defaults to the `select` tool and [`CONTENT_POLICY_PRESET.PDF_EDITOR`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET), which makes text, annotations, links, and widgets editable. For a reader that fills forms, start with `PDF_VIEWER` (text and annotations read-only, links and widgets interactive) and the `textSelect` tool, as above. Values that change at runtime go through `useMemo`, as [Getting started](https://sdk.pdf.net/docs/v1/guides/getting-started#set-up-the-providers) shows.
* **No worker or WebAssembly file.** There is no worker script or message protocol to write and no WebAssembly file to point at.
* **React in the browser.** `@sdk-pdf/react` is a React library for the browser; code that drives MuPDF.js from outside React moves into components and hooks.

## Load and reset a document [#load-and-reset-a-document]

### Before: MuPDF.js [#before-mupdfjs-1]

This snippet and the others that mix MuPDF.js calls with page code call `doc` directly, as on the main thread; behind a worker, those calls go through your message protocol.

`Document.openDocument()` takes the file bytes and a MIME type or file extension, `'application/pdf'` by default. It opens an encrypted document without a password: `needsPassword()` reports that one is required, and `authenticatePassword()` returns a bit field that is `0` when the password is wrong:

```js
async function openDocument(url) {
  const response = await fetch(url)
  const data = await response.arrayBuffer()
  const doc = mupdf.Document.openDocument(data, 'application/pdf')

  if (doc.needsPassword()) {
    let password = await askForPassword({ isRetry: false })
    while (doc.authenticatePassword(password) === 0) {
      password = await askForPassword({ isRetry: true })
    }
  }

  return doc
}
```

To open another document, destroy the current one first:

```js
doc.destroy()
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-1]

[`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) returns the load state and `loadPdf(pdfFileContent: Uint8Array, options?: { password?: string }): Promise<void>`. The password goes into the load call, and a wrong one rejects:

```tsx
import { useEffect, useRef, useState } from 'react'
import { InvalidPasswordError, useLoadPdf, useResetPdf } from '@sdk-pdf/react'

function DocumentLoader({ url }: { url: string }) {
  const [loadState, loadPdf] = useLoadPdf()
  const resetPdf = useResetPdf()
  const bytesRef = useRef<Uint8Array | null>(null)
  const bytesUrlRef = useRef(url)
  const [isPasswordRetry, setIsPasswordRetry] = useState<boolean>()

  // A new url is a new document: drop the cached bytes and unload the old document.
  useEffect(() => {
    if (bytesUrlRef.current === url) return
    bytesUrlRef.current = url
    bytesRef.current = null
    setIsPasswordRetry(undefined)
    resetPdf()
  }, [url, resetPdf])

  async function open(password?: string) {
    try {
      if (!bytesRef.current) {
        const response = await fetch(url)
        if (!response.ok) throw new Error(`Cannot fetch the document: ${response.status}`)
        bytesRef.current = new Uint8Array(await response.arrayBuffer())
      }

      // loadPdf reads from the buffer it receives for the document's lifetime,
      // so keep the original for a retry and pass a copy.
      await loadPdf(bytesRef.current.slice(), { password })
      setIsPasswordRetry(undefined)
    } catch (error) {
      if (error instanceof InvalidPasswordError) {
        // Replaces needsPassword() and authenticatePassword(): ask, then call
        // open(password).
        setIsPasswordRetry(password !== undefined)
      } else {
        showError(error)
      }
    }
  }

  if (isPasswordRetry !== undefined)
    return <PasswordPrompt isRetry={isPasswordRetry} onSubmit={open} />
  if (loadState.status === 'initial')
    return <button onClick={() => void open()}>Open</button>
  if (loadState.status === 'loading') return <p>Loading the document…</p>

  return <PdfApplication />
}
```

To open another document, unload the current one with [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf):

```tsx
import { useResetPdf } from '@sdk-pdf/react'

function CloseDocumentButton() {
  const resetPdf = useResetPdf()

  // Replaces doc.destroy(): the load state returns to 'initial'.
  return <button onClick={() => resetPdf()}>Close</button>
}
```

### What to know [#what-to-know-1]

* **PDF only.** MuPDF.js also opens XPS, CBZ, EPUB, FictionBook 2, and image files, and lays out reflowable documents; `useLoadPdf` loads PDF documents, with no MIME type or extension to pass.
* **A Uint8Array you don't reuse.** Wrap an `ArrayBuffer` with `new Uint8Array(data)`. `loadPdf` reads from the given buffer for the document's whole lifetime, and anything else touching that buffer, such as another `loadPdf` call or a transfer to a worker, can detach it. To reuse the bytes, pass a copy: `loadPdf(data.slice())`.
* **Passwords are a rejection, not a check.** Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`, and you call `loadPdf` again with `{ password }`. The promise resolves with nothing, so there is no counterpart to the bit field that tells a user password from an owner password.
* **Asynchronous loading.** `openDocument()` returns an open document; `loadPdf` returns a promise, and `loadState.status` (`'initial'`, `'loading'`, or `'ready'`) decides what to render.
* **Reset invalidates ids.** After `resetPdf()`, page and block ids, widget snapshots, and document revisions from the old document are no longer valid.

## Display the document [#display-the-document]

### Before: MuPDF.js [#before-mupdfjs-2]

A MuPDF.js viewer renders each page into a pixmap and draws it on a canvas of its own. The zoom is a resolution in DPI: a point is 1/72 of an inch, so the render matrix scales by `dpi / 72`. In the worker:

```js
function drawPage(pageIndex, dpi) {
  const page = doc.loadPage(pageIndex)
  const matrix = mupdf.Matrix.scale(dpi / 72, dpi / 72)
  // With alpha, the pixels are RGBA, the layout ImageData expects, and the page draws
  // on a transparent background, so give the canvases a white CSS background.
  const pixmap = page.toPixmap(matrix, mupdf.ColorSpace.DeviceRGB, true)
  const imageData = new ImageData(
    pixmap.getPixels().slice(),
    pixmap.getWidth(),
    pixmap.getHeight()
  )

  pixmap.destroy()
  page.destroy()
  return imageData
}
```

On the page, one canvas per page, sized in CSS from `getBounds()` (`width * zoom / 72` pixels) and drawn when it scrolls into view:

```js
let zoom = 96

const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) drawCanvas(entry.target)
  }
})

async function drawCanvas(canvas) {
  const pageIndex = Number(canvas.dataset.pageIndex)
  // callWorker posts a message to the worker and resolves with its reply.
  const imageData = await callWorker('drawPage', pageIndex, zoom * devicePixelRatio)
  canvas.width = imageData.width
  canvas.height = imageData.height
  canvas.getContext('2d').putImageData(imageData, 0, 0)
}
```

Text selection, link hit areas, and search hits are HTML layers you position over each canvas from `toStructuredText().asJSON()`, `getLinks()`, and `search()`.

### After: @sdk-pdf/react [#after-sdk-pdfreact-2]

[`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) takes no props: `PdfProviders` configures it, the theme styles it, and hooks control it:

```tsx
import { useViewportAutoFitScale, Viewport } from '@sdk-pdf/react'

function PdfApplication() {
  // Opens each document fitted to the viewport width; Zoom and fit modes maps fixed DPI
  // levels.
  useViewportAutoFitScale('width')

  return (
    // The viewport fills its parent, so give the parent an explicit size.
    <div style={{ height: '100vh' }}>
      <Viewport />
    </div>
  )
}
```

### What to know [#what-to-know-2]

* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded, so render it only when the load state is `'ready'`.
* **No layers to build.** Text selection, link navigation, and form filling happen inside `Viewport`, governed by the [interaction mode](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application#interaction-tool-and-content-policy): text selection is the `textSelect` tool, and widgets and links are levels of the [content policy](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) (`widgets: 'interactive'` lets users fill fields, `links: 'interactive'` keeps links clickable, and `'hidden'` doesn't render that kind of content). A level can also be a function of the block id, such as `widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive')`. The HTML layers you built from `asJSON()` and `getLinks()` go away.
* **No pixels to move.** There is no pixmap, `ImageData`, PNG, or object URL, and nothing to transfer between threads.
* **Styling replaces your page CSS.** The SDK renders unstyled: attach class names and inline styles through `theme.classNames` and `theme.styles`, and set the gap between the pages and the viewport edges with `theme.viewportPadding`, as [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization) describes.
* **Scrolling is built in.** `Viewport` shows the pages in one scrollable area, with no page containers of your own.

## Pages and navigation [#pages-and-navigation]

### Before: MuPDF.js [#before-mupdfjs-3]

Pages are zero-based indices. Navigation scrolls your own page elements, and an internal link resolves to a page index:

```js
const pageCount = doc.countPages()

function goToPage(pageIndex) {
  pageElements[pageIndex].scrollIntoView()
}

function followLink(link) {
  if (link.isExternal()) {
    window.open(link.getURI(), '_blank')
  } else {
    goToPage(doc.resolveLink(link))
  }
}
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-3]

[`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) returns the pages in document order, so an index in that list is the same zero-based index you passed to `loadPage()`. [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage) scrolls to a page id, and [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId) re-renders with the page in the middle of the viewport:

```tsx
import { useCurrentPageId, usePages, useScrollToPage } from '@sdk-pdf/react'

function PageNavigation() {
  const pages = usePages()
  const scrollToPage = useScrollToPage()
  // Replaces tracking the visible page with your own observer.
  const currentPageId = useCurrentPageId()

  // While there is no current page yet, treat the first page as the current one.
  const currentIndex = Math.max(
    0,
    pages.findIndex((page) => page.id === currentPageId)
  )

  function goToPage(pageIndex: number) {
    const page = pages[pageIndex]
    if (page) scrollToPage(page.id, { behavior: 'smooth' })
  }

  return (
    <div role="toolbar">
      <button disabled={currentIndex === 0} onClick={() => goToPage(currentIndex - 1)}>
        Previous
      </button>
      <span>
        Page {currentIndex + 1} of {pages.length}
      </span>
      <button
        disabled={currentIndex >= pages.length - 1}
        onClick={() => goToPage(currentIndex + 1)}
      >
        Next
      </button>
    </div>
  )
}
```

`scrollToPage(pageId, options?)` takes `behavior` (`'auto' | 'smooth' | 'instant'`), `vertical` (`'top' | 'center' | 'bottom' | 'nearest'`), and `horizontal` (`'left' | 'center' | 'right' | 'nearest'`).

### Converting between indices and ids [#converting-between-indices-and-ids]

| You have                | You need  | Use                                                                                                                          |
| ----------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
| A zero-based page index | A page id | `usePages()[index].id`                                                                                                       |
| A page id               | Its index | `getPageIndex(pageId)` from [`usePageIndexGetter`](https://sdk.pdf.net/docs/v1/reference/usePageIndexGetter): a zero-based index or `undefined` |
| A page id, rendered     | Its index | `usePageIndexGetter` paired with [`useRenderOnPageOrderChange`](https://sdk.pdf.net/docs/v1/reference/useRenderOnPageOrderChange)               |
| Only the count          |           | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount), the counterpart of `countPages()`                                         |

### What to know [#what-to-know-3]

* **Ids are session-scoped.** Don't persist page ids or reuse them across reloads. Keep persisting page indices or numbers, and resolve them to ids once the document is ready.
* **Ids can go stale.** `usePageIndexGetter` returns `undefined` for an unknown id, and `scrollToPage` throws `InvalidInputError`.
* **Page sizes.** The `width` and `height` of a `usePages` entry replace `getBounds()`: user-space units of 1/72 of an inch, the same unit as MuPDF.js points, with the page rotation applied.
* **Links.** Links work inside `Viewport` when `links` is `'interactive'`; there is no `getLinks()` or `resolveLink()` to call, and creating links has no equivalent.
* **Outline and labels.** `loadOutline()` and `page.getLabel()` have no equivalent.

## Page manipulation [#page-manipulation]

### Before: MuPDF.js [#before-mupdfjs-4]

A `PDFDocument` changes its page tree by index. `addPage()` creates a page object, and `insertPage()` adds it to the tree:

```js
// Append a blank A4 page: -1 inserts at the end.
doc.insertPage(-1, doc.addPage([0, 0, 595, 842], 0, null, ''))

// Delete the first page.
doc.deletePage(0)

// The full new order: unlisted pages are removed, and a page listed twice is duplicated.
doc.rearrangePages([2, 0, 1, 1])
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-4]

Each page operation is a hook that returns a command taking page ids: [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate), [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete), [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove), [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate), and [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation):

```tsx
import {
  usePageCreate,
  usePageDelete,
  usePageDuplicate,
  usePageMove,
  usePages,
  useSetPageRotation,
} from '@sdk-pdf/react'

function PageActions({ pageId }: { pageId: string }) {
  const pages = usePages()
  const createPage = usePageCreate()
  const deletePages = usePageDelete()
  const duplicatePage = usePageDuplicate()
  const { moveAfter } = usePageMove()
  const setPageRotation = useSetPageRotation()

  const page = pages.find((page) => page.id === pageId)
  if (!page) return null

  return (
    <div role="toolbar">
      {/* Like addPage([0, 0, 595, 842]): A4 in user-space units, inserted after this
         page. */}
      <button onClick={() => createPage(595, 842, pageId)}>
        Insert blank page after
      </button>
      <button disabled={pages.length <= 1} onClick={() => deletePages(pageId)}>
        Delete
      </button>
      <button onClick={() => void duplicatePage(pageId)}>Duplicate</button>
      {/* Without a target page, moveAfter moves the page to the end of the document. */}
      <button onClick={() => moveAfter(pageId)}>Move to end</button>
      {/* Rotation is absolute: add to the current angle yourself. */}
      <button onClick={() => setPageRotation(pageId, page.rotation + 90)}>
        Rotate clockwise
      </button>
    </div>
  )
}
```

### What to know [#what-to-know-4]

| Operation    | MuPDF.js                                                                        | @sdk-pdf/react                                                                                             |
| ------------ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| Insert       | `insertPage(at, addPage(...))` at an index, `-1` for the end                    | `createPage(width, height, insertAfterPageId?)`, after a page or appended; no index                        |
| Delete       | `deletePage(index)`, one page per call                                          | `deletePages(pageIdOrIds)`, several pages in one call                                                      |
| Reorder      | `rearrangePages(order)` with the full new order                                 | `moveBefore(ids, beforePageId?)` (start when omitted) or `moveAfter(ids, afterPageId?)` (end when omitted) |
| Duplicate    | A page listed twice in `rearrangePages`                                         | `duplicatePage(sourcePageId, insertAfterPageId?)`, asynchronous, inserted after the source by default      |
| Rotate       | No method for existing pages; the `rotate` argument of `addPage` for a new page | `setPageRotation(pageIdOrIds, rotation)` for any page, an absolute angle on a quarter turn                 |
| Extract      | `graftPage()` into a new document                                               | `prepareDocument({ pageIds })` saves only the listed pages, in the listed order                            |
| Copy or crop | `graftPage(to, srcDoc, srcPage)`, `setPageBox('CropBox', rect)`                 | No equivalent                                                                                              |

* **Undo.** Deleting, moving, or rotating several pages in one call is a single undo step, and an undo can restore a deleted page; separate edits within 500 ms merge into one step.
* **No events to re-read.** Instead of re-reading `countPages()` after a change, let `usePages` re-render your component. Page operations emit no events: track your own calls where you make them.
* **No merging.** Copying pages from another document with `graftPage()` has no equivalent: page operations edit the loaded document. To write a subset of the pages as a new file, pass `pageIds` to `prepareDocument()`.

## Zoom and fit modes [#zoom-and-fit-modes]

### Before: MuPDF.js [#before-mupdfjs-5]

The zoom lives in your code as a DPI value. Changing it resizes the canvases and renders every visible page again at the new matrix, and a fit is your own calculation from the page bounds and the container width:

```js
// In the worker.
function getPageBounds(pageIndex) {
  const page = doc.loadPage(pageIndex)
  const bounds = page.getBounds()
  page.destroy()
  return bounds
}
```

```js
// On the page.
let zoom = 96

function zoomTo(newZoom) {
  zoom = Math.min(Math.max(newZoom, 48), 384)
  for (const canvas of visibleCanvases) {
    resizeCanvas(canvas)
    drawCanvas(canvas)
  }
}

async function fitWidth() {
  const [x0, , x1] = await callWorker('getPageBounds', 0)
  // The CSS width of a page is its width in points * zoom / 72.
  zoomTo((viewerElement.clientWidth / (x1 - x0)) * 72)
}
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-5]

[`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale) reads and sets the scale, [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale) calculates fit scales on demand, and [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale) fits once per loaded document. A DPI zoom maps to the scale as `dpi / 72`:

```tsx
import { useEffect } from 'react'
import { useViewportFitScale, useViewportScale } from '@sdk-pdf/react'

function ZoomControls() {
  const [scale, setScale] = useViewportScale()
  const getFitScale = useViewportFitScale()

  // The zoom your render loop used, in DPI: scale 1 is 72 DPI.
  const dpi = scale * 72

  // Every scale change, whatever the source: these controls, gestures, or an auto fit.
  useEffect(() => saveZoomPreference(scale * 72), [scale])

  function zoomTo(newDpi: number) {
    // Your buttons keep the limits your render loop had.
    setScale(Math.min(Math.max(newDpi, 48), 384) / 72)
  }

  function fitPage() {
    const { fitWidthScale, fitHeightScale } = getFitScale()
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

  return (
    <div role="toolbar">
      <button onClick={() => zoomTo(dpi - 12)}>−</button>
      <span>{Math.round(dpi)} DPI</span>
      <button onClick={() => zoomTo(dpi + 12)}>+</button>
      <button onClick={() => zoomTo(96)}>96 DPI</button>
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={fitPage}>Fit page</button>
    </div>
  )
}
```

### What to know [#what-to-know-5]

* **Scale 1 is one user-space unit per CSS pixel,** the page size a MuPDF.js viewer shows at a zoom of 72 DPI. A stored zoom of `z` DPI becomes `setScale(z / 72)`, and there is no `devicePixelRatio` in the calculation.
* **Fits are one-time calculations.** `useViewportAutoFitScale` applies when a document loads and doesn't re-apply on viewport resizes or manual zooming; `useViewportFitScale` reflects the viewport size at the moment of the call. `fitWidthScale` fits the widest page into the viewport width and `fitHeightScale` the tallest page into its height, both accounting for `viewportPadding`.
* **Gestures are built in.** Pinch and wheel zoom go through the same scale and are tuned with `zoomGestures` on [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders). Its `minScale` (`0.5` by default) and `maxScale` (`4`) bound the gestures; to give them the limits of your buttons, pass `minScale: 48 / 72` and `maxScale: 384 / 72` in a configuration defined outside the component or memoized.
* **Zoom around a point.** `setScale(scale, pivot?)` keeps the document still at `pivot`, a point `{ x, y }` given as fractions of the viewport area (`0` the top or left edge, `1` the bottom or right edge), the center by default. The third item of `useViewportScale()`, `animateScale(scale, pivot?)`, animates the change and resolves when it ends. Both throw `InvalidInputError` unless the scale is a positive finite number.
* **The scale updates after a gesture.** During a pinch or wheel zoom, `scale` keeps its value until the gesture ends; `zoomGestureStart` and `zoomGestureEnd` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) mark the gesture.

## Thumbnails [#thumbnails]

### Before: MuPDF.js [#before-mupdfjs-6]

Thumbnails are small renders: the matrix scales the page to the thumbnail width, and the pixmap comes back as PNG bytes for an image element:

```js
// In the worker.
function drawThumbnail(pageIndex, width) {
  const page = doc.loadPage(pageIndex)
  const [x0, , x1] = page.getBounds()
  const scale = width / (x1 - x0)
  const pixmap = page.toPixmap(
    mupdf.Matrix.scale(scale, scale),
    mupdf.ColorSpace.DeviceRGB,
    false
  )
  const png = pixmap.asPNG()

  pixmap.destroy()
  page.destroy()
  return png
}
```

```js
// On the page.
const png = await callWorker('drawThumbnail', pageIndex, 120 * devicePixelRatio)
image.src = URL.createObjectURL(new Blob([png], { type: 'image/png' }))
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-6]

[`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders one page onto a canvas anywhere outside the main view. With `usePages`, `useScrollToPage`, and `useCurrentPageId`, it makes a sidebar that marks the current page:

```tsx
import { PageCanvas, useCurrentPageId, usePages, useScrollToPage } from '@sdk-pdf/react'

function ThumbnailsSidebar() {
  const pages = usePages()
  const scrollToPage = useScrollToPage()
  const currentPageId = useCurrentPageId()

  return (
    <nav>
      {pages.map((page, index) => (
        <button
          key={page.id}
          aria-current={page.id === currentPageId ? 'page' : undefined}
          onClick={() => scrollToPage(page.id)}
        >
          {/* The render resolution follows the canvas CSS size: style .thumbnail to the
             width you rendered at. */}
          <PageCanvas
            pageId={page.id}
            className="thumbnail"
            role="img"
            aria-label={`Page ${index + 1}`}
          />
        </button>
      ))}
    </nav>
  )
}
```

### What to know [#what-to-know-6]

* **Size with CSS, not a matrix.** The render resolution follows the canvas's displayed CSS size, or an explicit `scale` prop, and the pixels stay sharp; there is no scale to compute from `getBounds()` and no `devicePixelRatio` factor.
* **Lazy by default.** An off-screen canvas does no work until it becomes visible. In a virtualized list, pass `isVisible` to skip the per-canvas intersection observer.
* **Other props:** `renderOnResize` for window-relative sizes, `hideAnnotations` (form widgets included) where you passed `false` as the `showExtras` argument of `toPixmap()`, and `hideExtractableText` for drawing your own text on top.
* **Thumbnails that show session edits are coming soon.** `PageCanvas` draws the page as loaded from the file; the main view shows the edits. Rotation is applied and followed live.
* **No image files.** `PageCanvas` draws onto a canvas element; there is no API that returns a page as PNG or JPEG bytes, as `asPNG()` and `asJPEG()` do.
* **Render only when ready.** `PageCanvas` throws `DocumentNotLoadedError` when no document is loaded.

## Text extraction and search [#text-extraction-and-search]

### Before: MuPDF.js [#before-mupdfjs-7]

`toStructuredText()` analyzes a page into blocks, lines, and characters, which come out as plain text, as JSON with bounding boxes and fonts, or through a walker. `search()` returns each hit as a list of quads to highlight:

```js
const page = doc.loadPage(pageIndex)
const stext = page.toStructuredText('preserve-whitespace')

const text = stext.asText()
const { blocks } = JSON.parse(stext.asJSON())
const hits = page.search('total due')

stext.destroy()
page.destroy()
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-7]

[`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) reads the text as paragraphs: `getParagraphsByPage(pageId)` resolves with paragraph entries in content order, and `getAllParagraphIds()` with `getPlainTextParagraphs()` reads the whole document. Each entry carries `id`, `content`, `pageNumber`, the user-space box `bounds` (`{ x, y, width, height }`), and the paragraph layout, such as `textAlign` and `lineHeight`:

```tsx
import { useProgrammaticEditor } from '@sdk-pdf/react'

function useDocumentText() {
  const { getParagraphsByPage, getAllParagraphIds, getPlainTextParagraphs } =
    useProgrammaticEditor()

  // Replaces toStructuredText().asText() for one page.
  async function readPage(pageId: string) {
    const paragraphs = await getParagraphsByPage(pageId)
    return paragraphs.map((paragraph) => paragraph.content).join('\n\n')
  }

  // Loads every page in turn, which can take a long time for a large document.
  async function readDocument() {
    const paragraphs = await getPlainTextParagraphs(await getAllParagraphIds())
    return paragraphs.map((paragraph) => paragraph.content).join('\n\n')
  }

  return { readPage, readDocument }
}
```

[`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch) replaces `page.search()`: set the query and the options, and the SDK finds the matches across the document, highlights them on the pages, and scrolls to the active one:

```tsx
import { useDocumentSearch } from '@sdk-pdf/react'

function SearchBar() {
  const {
    criteria,
    matches,
    activeMatchIndex,
    status,
    setSearchCriteria,
    goToNextMatch,
    goToPreviousMatch,
  } = useDocumentSearch()

  return (
    <div role="search">
      <input
        value={criteria.query}
        onChange={(event) =>
          setSearchCriteria({ ...criteria, query: event.currentTarget.value })
        }
      />
      <label>
        <input
          type="checkbox"
          checked={criteria.isCaseSensitive}
          onChange={(event) =>
            setSearchCriteria({
              ...criteria,
              isCaseSensitive: event.currentTarget.checked,
            })
          }
        />
        Match case
      </label>
      <span>
        {status === 'searching'
          ? 'Searching…'
          : `${activeMatchIndex + 1} of ${matches.length}`}
      </span>
      <button disabled={matches.length === 0} onClick={goToPreviousMatch}>
        Previous
      </button>
      <button disabled={matches.length === 0} onClick={goToNextMatch}>
        Next
      </button>
    </div>
  )
}
```

### What to know [#what-to-know-7]

* **Paragraphs, not blocks, lines, and characters.** An entry carries the paragraph text, the 1-based `pageNumber`, and the paragraph `bounds` in user space. There are no lines, character quads, fonts, or image blocks, no `walk()`, and no structured text options such as `preserve-whitespace` or `dehyphenate`.
* **Matches, not quads.** Each match carries `id`, `pageId`, `paragraphId`, and the matched `text`, in document order, and the SDK draws the highlights: there are no hit quads to position or turn into `Highlight` annotations. `goToMatch(match.id)` activates a match from a results list, and `isWholeWord` matches whole words only. A changed query is searched once typing pauses, and `status` is `'searching'` until the whole document is searched.
* **Selection is native.** The `highlight()` and `copy()` helpers behind a custom text layer give way to the `textSelect` tool, which uses the browser's own text selection for reading and copying. No API reads the selected text.
* **Ids are session-scoped,** like page ids, and a paragraph can be deleted after you read it.

## Forms [#forms]

### Before: MuPDF.js [#before-mupdfjs-8]

MuPDF.js has no form UI. Each `PDFPage` lists its widgets, a widget reports its field name and type, and setters change the value; after a change, `page.update()` regenerates the appearances and tells you whether to render the page again. Users type into inputs you position over `getBounds()`, and the same setters write their values back:

```js
// Fill a page's fields from saved values, keyed by field name.
const page = doc.loadPage(pageIndex)

for (const widget of page.getWidgets()) {
  const value = savedValues[widget.getName()]
  if (value === undefined || widget.isReadOnly()) continue

  if (widget.getFieldType() === 'text') widget.setTextValue(value)
  else if (widget.getFieldType() === 'combobox') widget.setChoiceValue(value)
}

if (page.update()) redrawPage(pageIndex)
```

Checkboxes and radio buttons change with `toggle()`.

### After: @sdk-pdf/react [#after-sdk-pdfreact-8]

Users fill fields in `Viewport` with nothing to build when the content policy allows interacting with forms. For form UI outside the main view, [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets) lists the widgets of the whole document in reading order and [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue) binds one widget's value like a `useState` backed by the document. [Working with forms](https://sdk.pdf.net/docs/v1/guides/working-with-forms) builds the full sidebar:

```tsx
import {
  CheckboxWidget,
  TextWidget,
  useWidgets,
  useWidgetValue,
  Widget,
} from '@sdk-pdf/react'

function FormSidebar() {
  const { widgets, isLoaded } = useWidgets()

  // The list is partial until every page containing widgets has been loaded.
  if (!isLoaded) return <p>Loading the form…</p>

  return (
    <form>
      {widgets.map((widget) => (
        <FormField key={widget.id} widget={widget} />
      ))}
    </form>
  )
}

function FormField({ widget }: { widget: Widget }) {
  // widgetType replaces getFieldType().
  switch (widget.widgetType) {
    case 'text':
      return <TextField widget={widget} />
    case 'checkbox':
      return <CheckboxField widget={widget} />
    default:
      return null
  }
}

function TextField({ widget }: { widget: TextWidget }) {
  // Replaces getValue() and setTextValue(); there is no page.update() to call.
  const [value, setValue] = useWidgetValue(widget)

  return (
    <label>
      {widget.fieldName}
      <input value={value} onChange={(event) => setValue(event.currentTarget.value)} />
    </label>
  )
}

function CheckboxField({ widget }: { widget: CheckboxWidget }) {
  // A boolean instead of toggle().
  const [isChecked, setIsChecked] = useWidgetValue(widget)

  return (
    <label>
      <input
        type="checkbox"
        checked={isChecked}
        onChange={(event) => setIsChecked(event.currentTarget.checked)}
      />
      {widget.fieldName}
    </label>
  )
}
```

### From field types to widget kinds [#from-field-types-to-widget-kinds]

| MuPDF.js `getFieldType()` | @sdk-pdf/react `widgetType`                                                                    | Value                                    |
| ------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------- |
| `text`                    | `text`                                                                                         | `string`                                 |
| `combobox`, `listbox`     | `choice` for a single selection (`isCombo` tells a dropdown from a list box), or `multiChoice` | `string`, or selected option indices     |
| `checkbox`                | `checkbox`                                                                                     | `boolean`: whether the widget is checked |
| `radiobutton`             | `radio`                                                                                        | `boolean`: whether the widget is checked |
| `button`                  | `pushButton`                                                                                   | No value                                 |
| `signature`               | `signature`                                                                                    | No value; `isCryptoSigned` is read-only  |

### What to know [#what-to-know-8]

* **Values follow the document.** Filling in the main view and undo/redo show up as they happen, with no `page.update()` and no page to render again. With nothing to read, the value is the kind's empty value: `''`, `false`, or `[]`.
* **Values are read and written per widget.** `useWidgetValue` binds one widget per hook call; there is no setter by field name. The field name is the snapshot's `fieldName`, and every widget with the same field name shows and synchronizes the same value.
* **The whole document, not one page.** `useWidgets` lists every widget of the document; `await getWidgets()` from [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter) loads every page with widgets first and suits one-time reads, such as collecting values to submit.
* **Read-only fields.** `isReadOnly()` becomes the `isReadOnly` widget property. To lock a submitted form, call `setWidgetProps(ids, { isReadOnly: true }, { excludeFromHistory: true })` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps).
* **No document JavaScript.** `enableJS()` has no equivalent. The SDK recognizes Acrobat date formats without running scripts and exposes them as the text widget's `format` (see [`FormatType`](https://sdk.pdf.net/docs/v1/reference/FormatType)); fields driven by hand-written scripts read as plain text fields.
* **Form building.** [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate), the `addWidget` tool, `useSetWidgetProps`, and [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) create, restyle, and delete widgets. The SDK never signs.

## Editing and interaction tools [#editing-and-interaction-tools]

### Before: MuPDF.js [#before-mupdfjs-9]

MuPDF.js edits a page through annotations: `createAnnotation(type)` on a `PDFPage`, setters such as `setRect()`, `setContents()`, `setColor()`, and `setQuadPoints()`, and `page.update()` to regenerate the appearances. Deciding where an annotation goes is your pointer handler's job, which converts the click from CSS pixels to page points:

```js
// A FreeText box where the user clicked.
function addNote(pageIndex, event) {
  const page = doc.loadPage(pageIndex)
  const [x0, y0] = page.getBounds()
  const x = x0 + (event.offsetX * 72) / zoom
  const y = y0 + (event.offsetY * 72) / zoom

  const annotation = page.createAnnotation('FreeText')
  annotation.setRect([x, y, x + 200, y + 50])
  annotation.setContents('Note')
  page.update()
  redrawPage(pageIndex)
}

// A highlight over the first search hit.
function highlightFirst(page, needle) {
  const [hit] = page.search(needle)
  if (!hit) return

  const highlight = page.createAnnotation('Highlight')
  highlight.setQuadPoints(hit)
  highlight.setColor([1, 1, 0])
  page.update()
}
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-9]

What users may do is the **interaction mode**: a **content policy** (a permission level per content kind) and an **interaction tool** (what a click, tap, or drag does). [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) switches both at once, and [`useInteractionTool`](https://sdk.pdf.net/docs/v1/reference/useInteractionTool) reads the tool back. The tool places content at the click position, so the pointer handler and its coordinate conversion go away:

```tsx
import {
  CONTENT_POLICY_PRESET,
  InteractionTool,
  stageImage,
  useInteractionTool,
  useSetInteractionMode,
} from '@sdk-pdf/react'

const textSelectTool: InteractionTool = { type: 'textSelect' }
const selectTool: InteractionTool = { type: 'select' }
const addTextTool: InteractionTool = { type: 'addText' }

function EditingToolbar() {
  const interactionTool = useInteractionTool()
  const setInteractionMode = useSetInteractionMode()

  async function placeImage(file: File) {
    const image = await stageImage(file)
    // The next click on a page places the image as a stamp annotation.
    // Placing images is editing, so switch the policy with the tool.
    setInteractionMode({ type: 'addImage', image }, CONTENT_POLICY_PRESET.PDF_EDITOR)
  }

  return (
    <div role="toolbar">
      <button
        aria-pressed={interactionTool.type === 'textSelect'}
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_VIEWER)
        }
      >
        Read
      </button>
      <button
        aria-pressed={interactionTool.type === 'select'}
        onClick={() => setInteractionMode(selectTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Edit
      </button>
      {/* The nearest counterpart of a FreeText box: a paragraph at the click
         position. */}
      <button
        aria-pressed={interactionTool.type === 'addText'}
        onClick={() => setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Add text
      </button>
      <input
        type="file"
        accept="image/*"
        onChange={(event) => {
          const file = event.currentTarget.files?.[0]
          if (file) placeImage(file).catch(showError)
        }}
      />
    </div>
  )
}
```

Where your code wrote text into a page without user interaction, [`useParagraphCreate`](https://sdk.pdf.net/docs/v1/reference/useParagraphCreate) creates a paragraph and the paragraph editor of `useProgrammaticEditor` types into it:

```tsx
import { useParagraphCreate, useProgrammaticEditor } from '@sdk-pdf/react'

function useAddTextAt() {
  const createParagraph = useParagraphCreate()
  const { getParagraphEditor } = useProgrammaticEditor()

  return async (pageId: string, x: number, y: number, text: string) => {
    // x and y are in user-space units.
    const paragraphId = await createParagraph(pageId, x, y)
    const editor = getParagraphEditor(paragraphId)
    if (!editor) return

    editor.selectAll()
    editor.insertText(text)
  }
}
```

### Mapping the annotation types [#mapping-the-annotation-types]

| MuPDF.js                                           | @sdk-pdf/react                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FreeText`                                         | The nearest tool is `{ type: 'addText' }`: it creates a text paragraph, not an annotation, at the click position and focuses it for typing.                                                                                                                                                                                                                                                                                                                                |
| `Stamp`, image content written through `PDFObject` | `{ type: 'addImage', image, width?, height? }` places a staged image as a stamp annotation at the click position; [`stageImage(blob)`](https://sdk.pdf.net/docs/v1/reference/stageImage) validates and measures it first. The size is in user-space units: a single dimension keeps the aspect ratio, and with none the image keeps its natural size, fit to the page. From code, [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate) places it at a position you give. |
| `Highlight`, `Underline`, `StrikeOut`, `Squiggly`  | No equivalent.                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `Ink`                                              | No drawing tool. [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from strokes your code collects, like `setInkList()`.                                                                                                                                                                                                                                                                                                     |
| `Line`, `Square`, `Circle`, `Polygon`, `PolyLine`  | No tool creates them. Existing shapes and lines display, and their border color and width can be changed.                                                                                                                                                                                                                                                                                                                                                                  |
| `Text` notes, `FileAttachment`                     | No equivalent.                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Redaction annotations and `applyRedactions()`      | No equivalent.                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

### What to know [#what-to-know-9]

* **Switch the policy with the tool.** Pass `PDF_EDITOR` with the editing tools, as above. `textSelect` needs read-only text: with editable text, the combination is unsupported and reports a message through [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages).
* **Editing page text is built in.** Under `PDF_EDITOR`, users edit existing text in place, and `Viewport` handles typing. [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle) drives a style toolbar (`fontSize`, `textColor`, `fontFamily`, `isBold`, `isItalic`, and more), and [`useFocusSelection`](https://sdk.pdf.net/docs/v1/reference/useFocusSelection) returns the focus after a toolbar click.
* **Restyling instead of setters.** [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps) changes `borderColor` and `borderWidth` of selected shapes, lines, ink, and widgets, where you called `setColor()` and `setBorderWidth()`. Contents and quad points have no counterpart; opacity carries over only as the alpha channel of `borderColor`.
* **Selection commands.** [`useSelection`](https://sdk.pdf.net/docs/v1/reference/useSelection) tells a paragraph in text editing (`{ type: 'text', blockId }`) from selected blocks (`{ type: 'blocks', blockIds }`). [`useDeleteSelection`](https://sdk.pdf.net/docs/v1/reference/useDeleteSelection) (a selected text range or selected blocks), [`useDuplicateSelection`](https://sdk.pdf.net/docs/v1/reference/useDuplicateSelection), [`useMoveSelection`](https://sdk.pdf.net/docs/v1/reference/useMoveSelection), [`useSelectBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectBlocks), and [`useSelectedBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectedBlocks) read and act on the selection.
* **Keyboard shortcuts are your code.** Wire undo, redo, delete, and arrow keys with [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys).

## Undo, redo, and change tracking [#undo-redo-and-change-tracking]

### Before: MuPDF.js [#before-mupdfjs-10]

The journal is off until you enable it. Once it's on, annotation and widget edits are recorded as named undo steps, and `beginOperation()` and `endOperation()` group several edits into one named step. After an undo or a redo, the pages render again:

```js
doc.enableJournal()

function createNote(page, rect, text) {
  // One named undo step for the three edits.
  doc.beginOperation('Add note')
  try {
    const annotation = page.createAnnotation('FreeText')
    annotation.setRect(rect)
    annotation.setContents(text)
    doc.endOperation()
  } catch (error) {
    doc.abandonOperation()
    throw error
  }
  updateHistoryControls()
}

undoButton.onclick = () => {
  doc.undo()
  redrawVisiblePages()
  updateHistoryControls()
}

function updateHistoryControls() {
  undoButton.disabled = !doc.canUndo()
  redoButton.disabled = !doc.canRedo()
  unsavedIndicator.hidden = !doc.hasUnsavedChanges()
}
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-10]

[`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo) returns the commands and their availability; `canUndo` and `canRedo` re-render the component as the history changes, which replaces calling `updateHistoryControls()` after every edit:

```tsx
import { useMemo } from 'react'
import { useHotkeys, useUndoRedo } from '@sdk-pdf/react'

function HistoryControls() {
  const { undo, redo, canUndo, canRedo } = useUndoRedo()

  // The mapping must be memoized, or the listener re-installs on every render.
  useHotkeys(useMemo(() => ({ 'Mod-z': undo, 'Shift-Mod-z': redo }), [undo, redo]))

  return (
    <div role="toolbar">
      <button disabled={!canUndo} onClick={undo}>
        Undo
      </button>
      <button disabled={!canRedo} onClick={redo}>
        Redo
      </button>
    </div>
  )
}
```

`hasUnsavedChanges()` becomes a comparison of document revisions: [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) re-renders with a new revision on every edit, and undo and redo bring back the revision of that point in the history. Keep the revision you last saved and compare: a different revision means unsaved changes. A save records the revision it saved, so the unsaved-changes indicator lives with the save button in [Saving and downloading](https://sdk.pdf.net/docs/v1/migration/from-mupdf#saving-and-downloading).

### What to know [#what-to-know-10]

* **Always on, nothing to name.** Your edits enter the history without `enableJournal()`. Commands that act on several pages or widgets in one call are one step, and edits made within 500 ms of each other merge into a single step, even across hook calls. Where you called `beginOperation()` to keep an operation apart, call `markUndoBoundary()` from [`useUndoBoundary`](https://sdk.pdf.net/docs/v1/reference/useUndoBoundary) before it starts and after its edits finish; it doesn't guarantee a single step for edits spread over time or pages. The named steps of `getJournal()` have no equivalent.
* **One history for the whole document.** Deleting, moving, or rotating pages are undo steps, widget property changes are undo steps unless excluded, and form values follow undo and redo.
* **Local history.** Undo reverts your own changes.
* **Opting out.** Changes applied with the `excludeFromHistory` option of [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps) never enter the history.
* **Events for user actions only.** [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports what users do in the main view, such as `textEditorInput`, `addToolApplied`, `blocksDragEnd`, and `widgetInput`. Page operations, undo, and redo that your code calls emit no events: track them where you call them, and watch `useDocumentRevision` for any change.

## Saving and downloading [#saving-and-downloading]

### Before: MuPDF.js [#before-mupdfjs-11]

`saveToBuffer(options)` writes the document into a MuPDF `Buffer`, with write options such as `incremental`, `garbage`, and `compress`; `asUint8Array()` turns it into bytes for an upload or a download:

```js
// Upload, appending the changes to the original file.
const buffer = doc.saveToBuffer('incremental')
await fetch('/api/documents', { method: 'PUT', body: buffer.asUint8Array() })
buffer.destroy()

// Download.
const output = doc.saveToBuffer('garbage,compress')
const blob = new Blob([output.asUint8Array()], { type: 'application/pdf' })
output.destroy()
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = 'document.pdf'
link.click()
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-11]

[`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) builds the final PDF from the loaded document and the recorded changes. Saving doesn't reset any modified state, so the component that saves also records the saved revision:

```tsx
import { useRef, useState } from 'react'
import {
  useDocumentRevision,
  useDocumentRevisionGetter,
  useDocumentSaving,
} from '@sdk-pdf/react'

function SaveControls() {
  const revision = useDocumentRevision()
  const getRevision = useDocumentRevisionGetter()
  // The revision on the server: the loaded document's revision until the first save.
  const [savedRevision, setSavedRevision] = useState(revision)
  // One save at a time, so an overlapping save can't mark a failed upload as saved.
  const isSavingRef = useRef(false)
  const { prepareDocument, downloadDocument } = useDocumentSaving()

  // Replaces hasUnsavedChanges(): undoing back to the saved state clears it too.
  const hasUnsavedChanges = revision !== savedRevision

  async function saveToServer() {
    if (!hasUnsavedChanges || isSavingRef.current) return
    isSavingRef.current = true
    try {
      // Edits made after the save starts may be missing from the saved bytes, so only
      // this revision becomes saved.
      const startRevision = getRevision()
      // The buffer is transferred to fetch, so don't hand out the original.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })
      const response = await fetch('/api/documents', {
        method: 'PUT',
        body: bytes as BodyInit,
      })
      // A failed upload keeps the unsaved-changes indicator.
      if (!response.ok) return

      setSavedRevision(startRevision)
    } finally {
      isSavingRef.current = false
    }
  }

  return (
    <div role="toolbar">
      <button onClick={() => void saveToServer()}>Save</button>
      <button onClick={() => void downloadDocument()}>Download</button>
      {hasUnsavedChanges && <span>Unsaved changes</span>}
    </div>
  )
}
```

One component owns the saved revision, so the save and the indicator compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's.

### What to know [#what-to-know-11]

* **Bytes, not a Buffer.** `prepareDocument()` resolves with a `Uint8Array`; there is no `asUint8Array()` to call and no buffer to destroy. `downloadDocument()` builds the same bytes and saves them on the user's device, which replaces the `Blob` and link code.
* **returnOriginalIfUnchanged** defaults to `true`: with no changes, the original buffer passed to `loadPdf` comes back with no copy. Set it to `false` when you transfer or mutate the result, or the loaded document's buffer is detached and later reads throw.
* **Page subsets.** `pageIds` saves only the listed pages, in the listed order, where you grafted pages into a new document to extract or split them.
* **No write options.** Incremental saves, garbage collection, compression, encryption, and a download file name have no equivalent.
* **No flattening.** `bake()` has no equivalent.
* **Both functions throw DocumentNotLoadedError** when no document is loaded.

## Coordinates [#coordinates]

MuPDF.js and `@sdk-pdf/react` measure in the same unit but from different corners:

| Topic          | MuPDF.js                                         | @sdk-pdf/react                                                          |
| -------------- | ------------------------------------------------ | ----------------------------------------------------------------------- |
| Units          | Points, 1/72 of an inch                          | User-space units, 1/72 of an inch                                       |
| Origin         | The top-left corner of the page, y pointing down | The bottom-left corner of the page, such as for widget positions        |
| Page reference | Zero-based page indices                          | Session-scoped page ids; the index is the position in `usePages()`      |
| Page size      | `getBounds()`, with the page rotation applied    | `width` and `height` of the `usePages()` entries, with rotation applied |
| Conversion     | `PDFPage.getTransform()` to PDF user space       | No conversion helpers                                                   |

On an unrotated page, flip the y axis of rectangles you stored from MuPDF.js, such as annotation or widget positions, against the page height before you pass them to [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate) or `setWidgetProps`. Check the placement on a test document before converting stored positions in bulk.

## Errors and logging [#errors-and-logging]

### Before: MuPDF.js [#before-mupdfjs-12]

MuPDF.js calls throw when they fail, such as a `TypeError` for a color with the wrong number of components. In a worker, the dispatcher catches the error and posts it to the page, and `setLog()` installs a logger for MuPDF's warnings and errors:

```js
mupdf.setLog({
  error(message) {
    reportToMonitoring('error', message)
  },
  warning(message) {
    reportToMonitoring('warning', message)
  },
})

onmessage = (event) => {
  const [method, id, args] = event.data
  try {
    postMessage(['RESULT', id, methods[method](...args)])
  } catch (error) {
    postMessage([
      'ERROR',
      id,
      { name: error.name, message: error.message, stack: error.stack },
    ])
  }
}
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-12]

The SDK reports problems through two channels. When an operation is impossible, it throws a typed error and changes nothing. When it hits a problem it can work around, such as a malformed document, it completes the operation and emits a log message through [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages). [Error handling and logging](https://sdk.pdf.net/docs/v1/guides/error-handling-and-logging) has the full rules:

```tsx
import { useEffect } from 'react'
import { InvalidPasswordError, logMessages, LogLevel, useLoadPdf } from '@sdk-pdf/react'

function useSdkDiagnostics() {
  // Replaces setLog(): subscribe, and unsubscribe on unmount.
  useEffect(() => {
    const unsubscribe = logMessages.on('message', (logMessage) => {
      // The numeric levels order by severity, so a threshold is a single comparison.
      if (logMessage.level >= LogLevel.Warning)
        reportToMonitoring(logMessage.message, logMessage.data)
    })
    return () => {
      unsubscribe()
    }
  }, [])
}

function useOpenDocument() {
  const [, loadPdf] = useLoadPdf()

  return async (data: Uint8Array) => {
    try {
      // Pass a copy and keep `data` for a password retry.
      await loadPdf(data.slice())
    } catch (error) {
      // The error arrives at the call, with no worker to relay it.
      if (error instanceof InvalidPasswordError) askForPassword()
      else showError(error)
    }
  }
}
```

### Error classes [#error-classes]

Every SDK error extends `GenericError` and carries a static `message` and an optional `data` object:

| @sdk-pdf/react           | Thrown when                                                             | Closest MuPDF.js situation                                    |
| ------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------- |
| `InvalidPasswordError`   | The document is encrypted, and the password is missing or wrong.        | `needsPassword()`, and `authenticatePassword()` returning `0` |
| `DocumentFormatError`    | The PDF content is malformed beyond what the SDK can work around.       | —                                                             |
| `NotSupportedError`      | The document uses a PDF feature the SDK doesn't support yet.            | —                                                             |
| `DocumentNotLoadedError` | An operation requires a document, and none is loaded.                   | —                                                             |
| `InvalidInputError`      | The input passed to the API is invalid.                                 | A `TypeError` for an invalid argument                         |
| `UninitializedError`     | An object is used before its asynchronous initialization has completed. | —                                                             |

### What to know [#what-to-know-12]

* **Throw-versus-return rules.** Functions that modify the document validate first and throw before touching anything; functions that only read return empty values instead of throwing; hooks never throw during render. `Viewport` and `PageCanvas` are the exception: they throw `DocumentNotLoadedError` when rendered without a document.
* **Repairs become log messages.** MuPDF.js repairs a broken file when it opens it and reports that through `wasRepaired()`. In `@sdk-pdf/react`, problems the SDK works around arrive as log messages instead.
* **Structured logs, opt-in.** The SDK never prints to the browser console. Where `setLog()` receives a message string per warning or error, each `logMessages` entry carries `level` (`LogLevel.Debug`, `Info`, `Warning`, `Error`), `reason` (a `LogReason` member), a static `message`, `data`, and `stack`. There is no verbosity setting: filter by `level` in your subscriber.

## Cleanup and lifecycle [#cleanup-and-lifecycle]

### Before: MuPDF.js [#before-mupdfjs-13]

MuPDF.js objects hold WebAssembly memory, so you destroy every document, page, pixmap, structured text, and buffer when you're done with it, empty the cache when you need the memory back, and stop the worker when the viewer goes away:

```js
// In the worker.
pixmap.destroy()
stext.destroy()
page.destroy()
doc.destroy()
mupdf.emptyStore()
```

```js
// On the page.
worker.terminate()
```

### After: @sdk-pdf/react [#after-sdk-pdfreact-13]

Most of this goes away, and the rest follows React's component lifecycle:

| Concern                    | @sdk-pdf/react                                                                                                                            |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Pages, pixmaps, and text   | Nothing to destroy: there is no page, pixmap, structured text, or buffer object to release.                                               |
| Rendering off-screen pages | Nothing to manage: [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders lazily and does no work off-screen.                              |
| The worker                 | No worker of your own to start or terminate.                                                                                              |
| Closing a document         | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) unloads the document and invalidates its ids, snapshots, and revisions. |
| Event listeners            | [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) subscriptions follow the component lifecycle.                                   |
| Log subscriptions          | `logMessages.on()` returns an unsubscribe function: call it in the effect cleanup, as in [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-mupdf#errors-and-logging).       |
| Keyboard listeners         | [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) installs window-level shortcuts; pass a falsy value instead of the mapping to disable them. |
| Buffers                    | Don't reuse a buffer passed to `loadPdf`; copy it first.                                                                                  |

### What to know [#what-to-know-13]

* **No release calls.** The API has no counterpart to `destroy()`, `emptyStore()`, or `shrinkStore()`.
* **Reset before you reuse.** Where you destroyed a document before opening the next one, call `resetPdf()` and then `loadPdf`, and drop the ids and snapshots you kept from the old document.

## Features without an equivalent [#features-without-an-equivalent]

Plan around these before you migrate:

| MuPDF.js feature                                                                        | In @sdk-pdf/react                                                                                                                                       |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Structured text: lines, characters, fonts, image blocks, `walk()`, HTML and JSON output | Paragraph text and bounds from `useProgrammaticEditor`                                                                                                  |
| Reading the selected text (`highlight()`, `copy()`)                                     | No equivalent                                                                                                                                           |
| Rendering pages to image files (`toPixmap()`, `asPNG()`, `asJPEG()`)                    | `PageCanvas` draws onto a canvas, as loaded from the file, without session edits; no image export                                                       |
| Creating markup, shape, note, and file attachment annotations, and drawing ink by hand  | No equivalent; existing shapes, lines, and ink display, and their border color and width can be changed; `useInkAnnotationCreate` creates ink from code |
| Listing a page's annotations (`getAnnotations()`)                                       | No equivalent                                                                                                                                           |
| Redaction (`applyRedactions()`)                                                         | No equivalent                                                                                                                                           |
| Flattening (`bake()`)                                                                   | No equivalent                                                                                                                                           |
| Write options: incremental saves, garbage collection, compression, encryption           | No equivalent                                                                                                                                           |
| Outline, page labels, link creation (`createLink()`)                                    | No equivalent                                                                                                                                           |
| Metadata, permissions, layers, and embedded files                                       | No equivalent                                                                                                                                           |
| Copying pages from another document (`graftPage()`), cropping (`setPageBox()`)          | No equivalent                                                                                                                                           |
| Document JavaScript (`enableJS()`)                                                      | No equivalent; Acrobat date formats are recognized without running scripts                                                                              |
| Named undo steps (`beginOperation()`, `getJournal()`)                                   | One automatic history                                                                                                                                   |
| Low-level PDF objects and content streams (`PDFObject`, `getTrailer()`, `addStream()`)  | No equivalent                                                                                                                                           |
| Coordinate conversion (`getTransform()`)                                                | No equivalent; see [Coordinates](https://sdk.pdf.net/docs/v1/migration/from-mupdf#coordinates)                                                                                                          |
| XPS, CBZ, EPUB, FictionBook 2, and image files; reflowable layout                       | PDF only                                                                                                                                                |
| Running in Node or Bun, server-side jobs                                                | `@sdk-pdf/react` runs in the browser                                                                                                                    |

