# Migrating from pdf.js

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



This guide moves a pdf.js integration to `@sdk-pdf/react`. It covers both common setups: the display layer alone (`pdfjsLib.getDocument` and your own canvas rendering) and the viewer components (`pdfjsViewer.PDFViewer` with an `EventBus` and a `PDFLinkService`). 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-pdfjs#overview): how the two integrations differ, and the API map for every pdf.js API you use.
* [ ] [Install the package and set up `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#install-and-set-up), with a viewer content policy if you ship a reader.
* [ ] [Load and reset documents](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#load-and-reset-a-document) with `loadPdf` and `resetPdf`, including the password flow.
* [ ] [Replace canvas rendering or `PDFViewer` with `Viewport`](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#display-the-document).
* [ ] [Move page navigation from numbers to page ids](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#pages-and-navigation).
* [ ] [Move zoom and the fit presets](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#zoom-and-fit-modes).
* [ ] [Rebuild thumbnails with `PageCanvas`](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#thumbnails).
* [ ] [Move form reading and filling to widgets](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#forms).
* [ ] [Map annotation editor modes to interaction tools](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#editing-and-interaction-tools).
* [ ] [Replace undo/redo and modified-state tracking](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#undo-redo-and-change-tracking).
* [ ] [Replace `saveDocument()` and downloads](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#saving-and-downloading).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#errors-and-logging).
* [ ] [Remove manual cleanup](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#cleanup-and-lifecycle).
* [ ] [Move text extraction](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#text-extraction-and-search) and [plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#features-without-an-equivalent).
* [ ] Remove `pdfjs-dist`, the worker file, cMaps, fonts, `pdf_viewer.css`, and the event bus wiring. Test encrypted, malformed, large, and form-heavy documents, and the save round-trip.

## Overview [#overview]

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

  **pdf.js** is split into three layers: the core layer (`pdf.worker.mjs`) parses the PDF in a worker, the display layer (`pdf.mjs`) renders pages and reads document data, and the viewer layer is the Firefox viewer UI, whose building blocks (`PDFViewer`, `EventBus`, `PDFLinkService`) ship as components in `pdfjs-dist/web/pdf_viewer.mjs`. An integration built on it is imperative and object-based:

  * you point `GlobalWorkerOptions.workerSrc` at the worker file and keep it on exactly the same version as the library;
  * `getDocument()` returns a `PDFDocumentLoadingTask` whose `promise` resolves with a `PDFDocumentProxy`;
  * you get pages by 1-based number (`getPage(pageNumber)`), compute a `PageViewport`, and either render each page into a canvas yourself or hand the document to `PDFViewer`, which manages page views, text and annotation layers, scrolling, and zoom;
  * state lives in objects you hold (the document proxy, its `annotationStorage`, the viewer), observed through promises, callbacks, and `EventBus` events;
  * you release resources yourself: `RenderTask.cancel()`, `PDFPageProxy.cleanup()`, `PDFDocumentProxy.cleanup()`, `PDFDocumentLoadingTask.destroy()`.

  pdf.js is built for reading; its annotation editor adds a subset of annotation types on top 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 selection, text editing, form filling, link navigation, and zoom and drag gestures on its own. There is no per-page render call, text layer, or annotation layer to assemble.
  * Toolbars, sidebars, and shortcuts are your React components: state hooks re-render them on change, and command hooks change the document.
  * Pages are addressed by id, not by number: 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            | pdf.js                                                           | @sdk-pdf/react                                                                             |
  | ------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
  | Integration style  | Imperative objects, promises, callbacks, `EventBus`              | A React provider, state hooks, and command hooks                                           |
  | Rendering          | Your render loop (display layer) or `PDFViewer`                  | `Viewport` for the main view, `PageCanvas` for thumbnails and previews                     |
  | Page addressing    | 1-based page numbers                                             | Session-scoped page ids from `usePages`                                                    |
  | Document state     | Objects you hold: `PDFDocumentProxy`, `annotationStorage`        | Owned by the SDK, read through hooks                                                       |
  | Default capability | Reading and form filling; the annotation editor idle (`NONE`)    | Everything editable (`PDF_EDITOR`) unless you configure a read-only policy                 |
  | Editing model      | Annotation editors (FreeText, Highlight, Stamp, Ink, and others) | Page text, form widgets, image stamps, and pages; ink from code; no highlights or comments |
  | Saving             | `saveDocument()`                                                 | `prepareDocument()` and `downloadDocument()`                                               |
  | Diagnostics        | Rejected promises, `verbosity` console logging                   | Typed errors thrown before any change; `logMessages`, never printed to the console         |
  | Framework          | Framework-agnostic                                               | React                                                                                      |

  What this means for your code:

  1. **Delete the render loop.** Page rendering, render queues (such as `pageRendering` and `pageNumPending` flags), HiDPI canvas sizing, and text and annotation layers move into `Viewport`.
  2. **Replace numbers with ids.** Every page reference in your state becomes a page id; convert at the edges (deep links, analytics) with `usePages` and `usePageIndexGetter`.
  3. **Replace event-bus listeners with hooks.** State you mirrored from events (current page, page count, scale, undo availability) comes from state hooks, so `pagechanging` and `scalechanging` become `useCurrentPageId` and `useViewportScale`. `useViewportEvent` reports only user interactions, such as drags and zoom gestures.
  4. **Rethink forms as widgets.** Field maps become widget lists; values are read and written through `useWidgetValue`.
  5. **Rethink editing.** pdf.js adds annotations on top of the page; `@sdk-pdf/react` edits page text, widgets, images, and pages, and restyles the borders of selected shapes, lines, ink, and widgets.
  6. **Choose the policy.** The default is `PDF_EDITOR`; a reader needs `PDF_VIEWER` or `PDF_PRINTER`.
  7. **Move into React.** Code that drives pdf.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-pdfjs#features-without-an-equivalent).

  | pdf.js                                                                                               | @sdk-pdf/react                                                                                                                               | Notes                                                                                                         |
  | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
  | `GlobalWorkerOptions.workerSrc`, `cMapUrl`, `standardFontDataUrl`, `wasmUrl`                         | Nothing to configure                                                                                                                         | The install is the package and `PdfProviders`.                                                                |
  | `getDocument({ url })`                                                                               | `fetch` the bytes, then `loadPdf(data)` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                   | `loadPdf` takes a `Uint8Array`.                                                                               |
  | `getDocument({ data })`                                                                              | `loadPdf(data)`                                                                                                                              | Both take over the buffer; pass a copy to reuse it.                                                           |
  | `getDocument({ password })`, `loadingTask.onPassword`, `PasswordResponses`                           | `loadPdf(data, { password })`, catch `InvalidPasswordError`                                                                                  | A rejection replaces the callback.                                                                            |
  | `loadingTask.onProgress`                                                                             | **No equivalent**                                                                                                                            | The load state is `'initial'`, `'loading'`, or `'ready'`.                                                     |
  | `loadingTask.promise`                                                                                | The `loadPdf` promise and `loadState.status === 'ready'`                                                                                     |                                                                                                               |
  | `loadingTask.destroy()` to close a document                                                          | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf)                                                                            | Returns the load state to `'initial'`.                                                                        |
  | `pdfDocument.numPages`                                                                               | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                            | `0` while no document is loaded.                                                                              |
  | `pdfDocument.getPage(pageNumber)`                                                                    | `usePages()[pageNumber - 1].id` with [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages)                                                               | Each page has `id`, `width`, `height`, and `rotation`.                                                        |
  | `page.getViewport()`, `page.render()`, `TextLayer`, `AnnotationLayer`                                | [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport)                                                                                                    | One component, no layers to assemble.                                                                         |
  | `new PDFViewer({ container, eventBus, linkService })`, `setDocument()`                               | `PdfProviders` plus `Viewport`, rendered when the load state is `'ready'`                                                                    |                                                                                                               |
  | `pdf_viewer.css`                                                                                     | The `theme` configuration, see [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization)                                        | The SDK renders unstyled.                                                                                     |
  | `pdfViewer.currentPageNumber = n`, `nextPage()`, `previousPage()`, `PDFLinkService.goToPage()`       | `scrollToPage(pageId)` from [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage)                                                          |                                                                                                               |
  | `eventBus.on('pagechanging')`                                                                        | [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId)                                                                                    | Re-renders with the id of the page in the middle of the viewport.                                             |
  | `pdfViewer.currentScale`, `increaseScale()`, `decreaseScale()`                                       | [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale)                                                                                    | Scale `1` is one user-space unit per CSS pixel.                                                               |
  | `pdfViewer.currentScaleValue = 'page-width'` or `'page-height'`                                      | [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale), [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale)     | A one-time fit, not a persistent mode.                                                                        |
  | `currentScaleValue = 'page-fit'`                                                                     | `setScale(Math.min(fitWidthScale, fitHeightScale))`                                                                                          | See [Zoom and fit modes](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#zoom-and-fit-modes).                                                                |
  | `currentScaleValue = 'auto'`                                                                         | **No equivalent**                                                                                                                            |                                                                                                               |
  | `eventBus.on('scalechanging')`                                                                       | `scale` from `useViewportScale` in a `useEffect`                                                                                             | `zoomGestureEnd` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports pinch and wheel zoom. |
  | `pdfViewer.pagesRotation` (view only)                                                                | **No equivalent**                                                                                                                            | [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation) edits the document.                             |
  | Thumbnails rendered with a small `getViewport({ scale })`                                            | [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas)                                                                                                | Lazy; shows the page as loaded. Showing session edits is coming soon.                                         |
  | `AnnotationMode.ENABLE_FORMS`                                                                        | Content policy `widgets: 'interactive'` (`CONTENT_POLICY_PRESET.PDF_VIEWER`)                                                                 |                                                                                                               |
  | `pdfDocument.getFieldObjects()`                                                                      | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets), [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter)                                     | Widgets in reading order, not a map of fields.                                                                |
  | `annotationStorage.getValue()`, `setValue()`                                                         | [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                                                                                        | One widget per hook call.                                                                                     |
  | `PDFScriptingManager`, `enableXfa`                                                                   | **No equivalent**                                                                                                                            | Acrobat date formats are recognized without running scripts.                                                  |
  | `pdfViewer.annotationEditorMode = { mode }`                                                          | [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode)                                                                          | See [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#editing-and-interaction-tools).                                          |
  | `AnnotationEditorType.FREETEXT`                                                                      | `{ type: 'addText' }`                                                                                                                        | The nearest tool; it creates a text paragraph.                                                                |
  | `AnnotationEditorType.STAMP`                                                                         | `{ type: 'addImage', image }` with [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage)                                                             | Places a stamp annotation.                                                                                    |
  | `AnnotationEditorType.INK`                                                                           | [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate)                                                                        | Creates ink from strokes your code collects; there is no drawing tool.                                        |
  | `AnnotationEditorType.HIGHLIGHT`, `SIGNATURE`, `COMMENT`                                             | **No equivalent**                                                                                                                            |                                                                                                               |
  | `eventBus.dispatch('editingaction', { name: 'undo' })`, `'editingstateschanged'`                     | [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo)                                                                                              |                                                                                                               |
  | `annotationStorage.onSetModified`, `onResetModified`                                                 | [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision), [`useDocumentRevisionGetter`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevisionGetter) | Compare revisions to tell whether there are unsaved changes.                                                  |
  | `pdfDocument.saveDocument()`                                                                         | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                         | Resolves with a `Uint8Array`.                                                                                 |
  | `pdfDocument.getData()`                                                                              | `prepareDocument()` with the default `returnOriginalIfUnchanged: true`                                                                       | Returns the original buffer only while there are no changes.                                                  |
  | `DownloadManager`                                                                                    | `downloadDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                        |                                                                                                               |
  | `InvalidPDFException`                                                                                | `DocumentFormatError`                                                                                                                        | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#errors-and-logging).                                                                |
  | `ResponseException`                                                                                  | Errors from your own `fetch`                                                                                                                 | `loadPdf` takes bytes; fetching is yours.                                                                     |
  | `verbosity`, `VerbosityLevel`                                                                        | [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages), `LogLevel`                                                                                  | The SDK never prints to the console.                                                                          |
  | `RenderTask.cancel()`, `PDFPageProxy.cleanup()`, `PDFDocumentProxy.cleanup()`, `PDFWorker.destroy()` | Nothing to call                                                                                                                              | See [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#cleanup-and-lifecycle).                                                          |
  | `page.getTextContent()`                                                                              | `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)                                       | Paragraph text and bounds, not text items.                                                                    |
  | `PDFFindController`                                                                                  | [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch)                                                                                  | Highlights the matches and scrolls to the active one.                                                         |
  | `pdfDocument.extractPages()`                                                                         | `prepareDocument({ pageIds })`                                                                                                               | Saves only the listed pages, in the listed order.                                                             |
</details>

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

### Before: pdf.js [#before-pdfjs]

pdf.js ships as `pdfjs-dist`. A page loads `pdfjs-dist/build/pdf.mjs` (plus `pdfjs-dist/web/pdf_viewer.mjs` and `pdf_viewer.css` for the viewer components), works through the `pdfjsLib` and `pdfjsViewer` namespaces, and sets up the worker and the cMaps before loading anything:

```js
pdfjsLib.GlobalWorkerOptions.workerSrc =
  '../../node_modules/pdfjs-dist/build/pdf.worker.mjs'

// Some PDFs need external cMaps.
const CMAP_URL = '../../node_modules/pdfjs-dist/cmaps/'
const CMAP_PACKED = true
```

### 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; the PDF application re-renders whenever a prop changes identity, so define static configuration outside the component and memoize the rest:

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

// Static configuration lives outside the component, so its identity never changes.
const initialInteractionTool: InteractionTool = { type: 'textSelect' }

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

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

* **Pick the content policy deliberately.** A pdf.js viewer reads and fills forms by default: `PDFViewer`'s `annotationMode` defaults to `AnnotationMode.ENABLE_FORMS`, and `annotationEditorMode` to `AnnotationEditorType.NONE`. `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. To keep a reader's behavior, start with `PDF_VIEWER` (text and annotations read-only, links and widgets interactive) and the `textSelect` tool, as above.
* **No asset URLs.** There is no worker file, cMap folder, font folder, or WebAssembly URL to point at.
* **React only.** Code that drives pdf.js from outside React moves into components and hooks.

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

### Before: pdf.js [#before-pdfjs-1]

`getDocument()` takes a URL or binary data. The loading task asks for passwords and reports progress through callbacks; `onPassword` receives a function to call with the password and a `PasswordResponses` reason:

```js
const loadingTask = pdfjsLib.getDocument({ url })

loadingTask.onPassword = (updatePassword, reason) => {
  const isRetry = reason === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD
  askForPassword({ isRetry }).then(updatePassword)
}

loadingTask.onProgress = ({ loaded, total }) => {
  showProgress(loaded, total)
}

const pdfDocument = await loadingTask.promise
```

To open another document, destroy the old loading task first; `destroy()` aborts its network requests and destroys the worker:

```js
await loadingTask.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 bytes can come from a file input, a `fetch`, or drag-and-drop, so a URL becomes a `fetch`:

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

function DocumentLoader({ url }: { url: string }) {
  const [loadState, loadPdf] = useLoadPdf()
  const bytesRef = useRef<Uint8Array | null>(null)
  const [needsPassword, setNeedsPassword] = useState(false)

  async function open(password?: string) {
    if (!bytesRef.current) {
      const response = await fetch(url)
      bytesRef.current = new Uint8Array(await response.arrayBuffer())
    }

    try {
      // 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 })
      setNeedsPassword(false)
    } catch (error) {
      if (error instanceof InvalidPasswordError) {
        // The password is missing or wrong: ask for it, then call open(password) again.
        setNeedsPassword(true)
        return
      }
      throw error
    }
  }

  if (needsPassword) return <PasswordPrompt 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 OpenAnotherButton() {
  const resetPdf = useResetPdf()

  // The load state returns to 'initial'.
  return <button onClick={() => resetPdf()}>Open another document</button>
}
```

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

* **Bytes, not URLs.** `loadPdf` takes a `Uint8Array`. Network options of `getDocument` (`httpHeaders`, `withCredentials`, `range`, `disableRange`, `disableStream`, `disableAutoFetch`) move to your own `fetch`; range and streaming loading have no equivalent.
* **Buffer reuse needs the same care.** pdf.js transfers a TypedArray to its worker and takes ownership of it. `loadPdf` reads from the given buffer for the document's whole lifetime, and anything else touching that buffer can detach it. To reuse the bytes, pass a copy: `loadPdf(data.slice())`.
* **Passwords are a rejection, not a callback.** Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`. There is no `PasswordResponses`: keep your own first-attempt-or-retry state.
* **No progress reporting.** `loadState.status` is `'initial'`, `'loading'`, or `'ready'`.
* **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: pdf.js display layer [#before-pdfjs-display-layer]

With the display layer alone, you render each page into a canvas you size yourself:

```js
const pdf = await pdfjsLib.getDocument({ url }).promise
const page = await pdf.getPage(1)
const viewport = page.getViewport({ scale: 1.5 })
const outputScale = window.devicePixelRatio || 1

const canvas = document.getElementById('the-canvas')
const context = canvas.getContext('2d')
canvas.width = Math.floor(viewport.width * outputScale)
canvas.height = Math.floor(viewport.height * outputScale)
canvas.style.width = Math.floor(viewport.width) + 'px'
canvas.style.height = Math.floor(viewport.height) + 'px'

const transform = outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null
page.render({ canvasContext: context, transform, viewport })
```

Text selection and interactive annotations need the `TextLayer` and `AnnotationLayer` classes on top of the canvas.

### Before: pdf.js viewer components [#before-pdfjs-viewer-components]

With the viewer components, `PDFViewer` manages the pages:

```js
const container = document.getElementById('viewerContainer')
const eventBus = new pdfjsViewer.EventBus()
const pdfLinkService = new pdfjsViewer.PDFLinkService({ eventBus })

const pdfViewer = new pdfjsViewer.PDFViewer({
  container,
  eventBus,
  linkService: pdfLinkService,
})
pdfLinkService.setViewer(pdfViewer)

eventBus.on('pagesinit', () => {
  pdfViewer.currentScaleValue = 'page-width'
})

const pdfDocument = await pdfjsLib.getDocument({ url }).promise
pdfViewer.setDocument(pdfDocument)
pdfLinkService.setDocument(pdfDocument, null)
```

### 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() {
  // Replaces `currentScaleValue = 'page-width'` on 'pagesinit'.
  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'`. The `pagesinit` hook point becomes the component rendered after `'ready'`.
* **No layers to stack.** 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). Where `PDFViewer` takes `textLayerMode` and `annotationMode`, 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 at all. A level can also be a function of the block id, such as `widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive')`.
* **Styling replaces pdf_viewer.css.** The SDK renders unstyled: attach class names and inline styles through `theme.classNames` and `theme.styles`, and override CSS variables such as `--selection-color`, as [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization) describes. The gap between the pages and the viewport edges is `theme.viewportPadding`.
* **One layout.** `Viewport` shows the pages in one scrollable area; `ScrollMode`, `SpreadMode`, and `PDFSinglePageViewer` have no equivalent.

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

### Before: pdf.js [#before-pdfjs-2]

With the display layer, navigation is your own page counter and render queue, because one canvas can't render two pages at once:

```js
function onNextPage() {
  if (pageNum >= pdfDoc.numPages) return
  pageNum++
  queueRenderPage(pageNum)
}
```

With `PDFViewer`, the current page is a property, and changes arrive on the event bus:

```js
// Go to page 3 (1-based).
pdfViewer.currentPageNumber = 3

// Go to the next page, taking scroll and spread modes into account.
pdfViewer.nextPage()

eventBus.on('pagechanging', ({ pageNumber }) => {
  pageNumberInput.value = pageNumber
})
```

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

[`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) returns the pages in document order, so a position is an index in that list. [`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 currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()

  // -1 while there is no current page yet.
  const index = pages.findIndex((page) => page.id === currentPageId)

  return (
    <div role="toolbar">
      <button disabled={index <= 0} onClick={() => scrollToPage(pages[index - 1].id)}>
        Previous
      </button>
      <span>
        {index === -1 ? '–' : index + 1} of {pages.length}
      </span>
      <button
        disabled={index === -1 || index >= pages.length - 1}
        onClick={() => scrollToPage(pages[index + 1].id)}
      >
        Next
      </button>
    </div>
  )
}
```

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

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

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

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

* **Ids are session-scoped.** Don't persist page ids or reuse them across reloads. Keep persisting page numbers (for example in a `#page=3` deep link) 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`.
* **Rotation edits the document.** `pdfViewer.pagesRotation` rotates the view only. `setPageRotation(pageIdOrPageIds, rotation)` from [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation) rotates the pages in the document as an undoable edit, with an absolute angle of `0`, `90`, `180`, or `270`.
* **Page operations edit the loaded document.** `@sdk-pdf/react` changes the pages of the loaded document: [`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). Deleting, moving, or rotating several pages in one call is a single undo step. The counterpart of `extractPages()` is `prepareDocument({ pageIds })`, which saves only the listed pages.
* **Links.** Internal links work inside `Viewport` when `links` is `'interactive'`. `PDFLinkService`'s `externalLinkTarget`, named destinations, and page labels have no equivalent.

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

### Before: pdf.js [#before-pdfjs-3]

The display layer zooms by re-rendering with a different `getViewport({ scale })`. `PDFViewer` has a scale, preset fit values, and an event:

```js
// or 'page-height', 'page-fit', 'page-actual', 'auto'
pdfViewer.currentScaleValue = 'page-width'
pdfViewer.currentScale = newScale
pdfViewer.increaseScale()

eventBus.on('scalechanging', ({ scale, presetValue }) => {
  updateZoomLabel(scale, presetValue)
})
```

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

[`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:

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

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

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

  return (
    <div role="toolbar">
      <button onClick={() => setScale(scale / 1.25)}>−</button>
      <span>{Math.round(scale * 100)}%</span>
      <button onClick={() => setScale(scale * 1.25)}>+</button>
      {/* Call the getter inside the handler: the result reflects the viewport size at
         that moment. */}
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={() => setScale(getFitScale().fitHeightScale)}>Fit height</button>
    </div>
  )
}
```

### Mapping the presets [#mapping-the-presets]

| `currentScaleValue` | @sdk-pdf/react                                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `'page-width'`      | `useViewportAutoFitScale('width')` on load, or `setScale(getFitScale().fitWidthScale)`, which fits the widest page into the viewport width.                                                                                                                                                                          |
| `'page-height'`     | `useViewportAutoFitScale('height')`, or `fitHeightScale`, which fits the tallest page into the viewport height.                                                                                                                                                                                                      |
| `'page-fit'`        | `setScale(Math.min(fitWidthScale, fitHeightScale))` fits the pages in both directions.                                                                                                                                                                                                                               |
| `'page-actual'`     | `setScale(96 / 72)` shows pages at the same size. `PDFViewer` multiplies its scale by 96 / 72, while `@sdk-pdf/react`'s scale `1` (its 100%) is one user-space unit per CSS pixel, the same unit as the display layer's `getViewport({ scale: 1 })`. A stored `PDFViewer` scale `s` becomes `setScale(s * 96 / 72)`. |
| `'auto'`            | No equivalent.                                                                                                                                                                                                                                                                                                       |

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

* **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. There is no fit mode that stays applied while the viewport resizes.
* **Gestures are built in.** Pinch and wheel/trackpad zoom go through the same scale and are tuned with `zoomGestures` on [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) (`minScale` `0.5` and `maxScale` `4` by default).
* **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: pdf.js [#before-pdfjs-4]

The pdf.js viewer app's `PDFThumbnailViewer` isn't one of the exported viewer components, so integrations render thumbnails themselves at a small scale:

```js
const page = await pdfDocument.getPage(pageNumber)
const desiredWidth = 100
const viewport = page.getViewport({ scale: 1 })
const scale = desiredWidth / viewport.width
const scaledViewport = page.getViewport({ scale })

canvas.width = scaledViewport.width
canvas.height = scaledViewport.height
await page.render({ canvas, viewport: scaledViewport }).promise
```

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

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

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

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

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

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

* **Size with CSS, not scale math.** The render resolution follows the canvas's displayed CSS size, or an explicit `scale` prop.
* **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), 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.
* **Render only when ready.** `PageCanvas` throws `DocumentNotLoadedError` when no document is loaded.

## Forms [#forms]

### Before: pdf.js [#before-pdfjs-5]

`PDFViewer` renders interactive form elements by default (`annotationMode: AnnotationMode.ENABLE_FORMS`). Field data comes from the document proxy, and values live in its `annotationStorage`, a key/value store typed as `string` keys and `Object` values:

```js
// A Map of AcroForm field data, or null when the PDF has no fields.
const fieldObjects = await pdfDocument.getFieldObjects()

const storage = pdfDocument.annotationStorage
const value = storage.getValue(key, defaultValue)
storage.setValue(key, newValue)
```

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

Users fill fields in `Viewport` with nothing to build when the content policy allows interacting with widgets. For form UI outside the main view, [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets) lists the widgets 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 }) {
  switch (widget.widgetType) {
    case 'text':
      return <TextField widget={widget} />
    case 'checkbox':
      return <CheckboxField widget={widget} />
    default:
      return null
  }
}

function TextField({ widget }: { widget: TextWidget }) {
  const [value, setValue] = useWidgetValue(widget)

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

function CheckboxField({ widget }: { widget: CheckboxWidget }) {
  const [isChecked, setIsChecked] = useWidgetValue(widget)

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

### From fields to widgets [#from-fields-to-widgets]

| Concept      | pdf.js                                      | @sdk-pdf/react                                                                                                                                                                            |
| ------------ | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unit of work | A map of AcroForm fields                    | Widgets, the visible boxes users interact with; each carries a `fieldName`                                                                                                                |
| Shared value | Fields                                      | Every widget with the same field name shows and synchronizes the same value                                                                                                               |
| Value shape  | Storage objects                             | `text` and `choice`: `string`; `checkbox` and `radio`: `boolean`; `multiChoice`: selected option indices; `pushButton` and `signature`: no value                                          |
| Completeness | —                                           | `useWidgets().isLoaded`, or `await getWidgets()` from [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter), which loads every page with widgets first                                |
| Date fields  | `PDFScriptingManager` runs document scripts | The SDK recognizes Acrobat date formats without running scripts and exposes them as the text widget's `format` (`FormatType.DateTime`); see [`FormatType`](https://sdk.pdf.net/docs/v1/reference/FormatType) |

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

* **Values follow the document.** Filling in the main view and undo/redo show up as they happen. 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 and no FDF or XFDF form-data export. `prepareDocument()` produces the filled PDF.
* **Field change events.** The `widgetInput` event of [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports a value the user changed in the main view, with the widget id; values set from code, undo, and redo emit nothing. [`useFocusedWidgetId`](https://sdk.pdf.net/docs/v1/reference/useFocusedWidgetId) returns the widget being filled, and [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) changes on any edit.
* **No scripting or XFA.** `PDFScriptingManager` and the `enableXfa` loading option have no equivalent: date formats are recognized without running scripts, and fields driven by hand-written scripts read as plain text fields.
* **Form building is new.** [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate), the `addWidget` tool, [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps), and [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) create, restyle, and delete widgets. To lock a submitted form, call `setWidgetProps(ids, { isReadOnly: true }, { excludeFromHistory: true })`.

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

### Before: pdf.js [#before-pdfjs-6]

`PDFViewer` takes an `annotationEditorMode` option that enables creating and editing annotations, `AnnotationEditorType.NONE` by default. The setter switches the active editor and dispatches `annotationeditormodechanged`. The editor types are `DISABLE`, `NONE`, `FREETEXT`, `HIGHLIGHT`, `STAMP`, `INK`, `POPUP`, `SIGNATURE`, and `COMMENT`; passing `DISABLE` as the option turns the editor off, and the setter then throws `The AnnotationEditor is not enabled.`:

```js
const pdfViewer = new pdfjsViewer.PDFViewer({
  container,
  eventBus,
  annotationEditorMode: pdfjsLib.AnnotationEditorType.NONE,
})

// Later, from a toolbar button:
pdfViewer.annotationEditorMode = { mode: pdfjsLib.AnnotationEditorType.FREETEXT }
```

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

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. [Building a PDF application](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application#interaction-tool-and-content-policy) lists the tools:

```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>
      <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>
  )
}
```

### Mapping the editors [#mapping-the-editors]

| pdf.js `AnnotationEditorType` | @sdk-pdf/react                                                                                                                                                                                                                                                                                                                                      |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NONE`                        | `{ type: 'textSelect' }` for native text selection, or `{ type: 'select' }` to select, move, and resize blocks.                                                                                                                                                                                                                                     |
| `FREETEXT`                    | The nearest tool is `{ type: 'addText' }`: it creates a text paragraph at the click position and focuses it for typing. It requires editable `text`, not editable `annotations`.                                                                                                                                                                    |
| `STAMP`                       | `{ 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. |
| `INK`                         | No drawing tool. [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from strokes your code collects.                                                                                                                                                                                                   |
| `HIGHLIGHT`                   | No equivalent.                                                                                                                                                                                                                                                                                                                                      |
| `SIGNATURE`                   | No equivalent. Signature widgets exist as a form kind, and the SDK never signs.                                                                                                                                                                                                                                                                     |
| `COMMENT`, `POPUP`            | No equivalent.                                                                                                                                                                                                                                                                                                                                      |

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

* **Editing existing text is new.** Under `PDF_EDITOR`, text is editable 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.
* **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).
* **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; [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps) edits `borderColor` and `borderWidth` of selected shapes, lines, ink, and widgets.
* **Keyboard shortcuts are your code.** Wire undo, redo, delete, and arrow keys with [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys).
* **Programmatic edits.** [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) reads and rewrites paragraph text without UI, as [Programmatic editing](https://sdk.pdf.net/docs/v1/guides/programmatic-editing) shows.

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

### Before: pdf.js [#before-pdfjs-7]

pdf.js keeps undo/redo inside its annotation editor. The editor listens for `editingaction` events on the event bus and reports `hasSomethingToUndo` and `hasSomethingToRedo` in `editingstateschanged`. `AnnotationStorage` calls `onSetModified` and `onResetModified` when its modified state changes, which the viewer uses to warn before leaving a page with edited forms:

```js
undoButton.onclick = () => eventBus.dispatch('editingaction', { name: 'undo' })
redoButton.onclick = () => eventBus.dispatch('editingaction', { name: 'redo' })

eventBus.on('editingstateschanged', ({ details }) => {
  undoButton.disabled = !details.hasSomethingToUndo
  redoButton.disabled = !details.hasSomethingToRedo
})

pdfDocument.annotationStorage.onSetModified = () => setUnsavedChanges(true)
pdfDocument.annotationStorage.onResetModified = () => setUnsavedChanges(false)
```

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

[`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 listening for `editingstateschanged`:

```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>
  )
}
```

`onSetModified` and `onResetModified` become 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-pdfjs#saving-and-downloading).

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

* **One history for the whole document,** not only for annotations: 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, and edits made within 500 ms of each other merge into a single undo step, even across hook calls. To keep an operation a separate step, call `markUndoBoundary()` from [`useUndoBoundary`](https://sdk.pdf.net/docs/v1/reference/useUndoBoundary) before it starts and after its edits finish.
* **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: pdf.js [#before-pdfjs-8]

`saveDocument()` resolves with a `Uint8Array` holding the full saved document, and `getData()` resolves with the original file data. `saveDocument()` also resets the storage's modified state, which fires `onResetModified`, and warns you to use `getData()` when the annotation storage is empty. The viewer components export a `DownloadManager` for downloads:

```js
const data = await pdfDocument.saveDocument()
await fetch('/api/documents', { method: 'PUT', body: data })
```

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

[`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: record the saved revision yourself:

```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 onSetModified: 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 buffer = await prepareDocument({ returnOriginalIfUnchanged: false })
      const response = await fetch('/api/documents', {
        method: 'PUT',
        body: buffer as BodyInit,
      })
      // A failed upload keeps the unsaved-changes indicator.
      if (!response.ok) return

      // Replaces onResetModified.
      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-9]

* **returnOriginalIfUnchanged** defaults to `true`: with no changes, the original buffer passed to `loadPdf` comes back with no copy, the closest match to `getData()`. Set it to `false` when you transfer or mutate the result, or the loaded document's buffer is detached and later reads throw.
* **Both functions throw DocumentNotLoadedError** when no document is loaded.
* **Save options.** Besides `returnOriginalIfUnchanged`, `pageIds` saves only the listed pages, in the listed order, which replaces `extractPages()`. There is no flattening or download file name option.

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

### Before: pdf.js [#before-pdfjs-9]

Errors arrive as rejected promises. The `verbosity` loading option sets the console logging level with `VerbosityLevel` constants, and the exported exception classes tell invalid files from missing ones:

```js
const loadingTask = pdfjsLib.getDocument({
  url,
  verbosity: pdfjsLib.VerbosityLevel.ERRORS,
})

try {
  const pdfDocument = await loadingTask.promise
} catch (reason) {
  if (reason instanceof pdfjsLib.InvalidPDFException) {
    showError('Invalid PDF file')
  } else if (reason instanceof pdfjsLib.ResponseException) {
    showError(reason.missing ? 'Missing PDF file' : 'Unexpected server response')
  }
}
```

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

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 { InvalidPasswordError, logMessages, LogLevel, useLoadPdf } from '@sdk-pdf/react'

// Subscribing returns an unsubscribe function.
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)
  }
})

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

  return async (data: Uint8Array) => {
    try {
      // Pass a copy and keep `data` for a password retry.
      await loadPdf(data.slice())
    } catch (error) {
      if (error instanceof InvalidPasswordError) {
        askForPassword()
      } else {
        // Narrow the other SDK error classes with instanceof, as for
        // InvalidPasswordError.
        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 pdf.js situation                            |
| ------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------- |
| `InvalidPasswordError`   | The document is encrypted, and the password is missing or wrong.        | `onPassword` with `PasswordResponses`               |
| `DocumentFormatError`    | The PDF content is malformed beyond what the SDK can work around.       | `InvalidPDFException`                               |
| `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.                                 | —                                                   |
| `UninitializedError`     | An object is used before its asynchronous initialization has completed. | —                                                   |
| —                        | —                                                                       | `ResponseException`: your own `fetch` fails instead |

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

* **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.
* **Logs are opt-in.** The SDK never prints to the browser console. Each message 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: pdf.js [#before-pdfjs-10]

pdf.js leaves resource management to you:

```js
renderTask.cancel() // The render promise rejects when cancelled.
page.cleanup() // Frees the page's resources.
await pdfDocument.cleanup() // Never while rendering is in progress.
await loadingTask.destroy() // Aborts network requests and destroys the worker.
```

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

Most of this moves into the SDK and React's component lifecycle:

| Concern            | @sdk-pdf/react                                                                                                                            |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Cancelling renders | Nothing to call: [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders lazily and does no work off-screen.                                |
| Event listeners    | [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) subscriptions follow the component lifecycle.                                   |
| Log subscriptions  | `logMessages.on()` returns an unsubscribe function.                                                                                       |
| 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. |
| Closing a document | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) unloads the document and invalidates its ids, snapshots, and revisions. |
| Buffers            | Don't reuse a buffer passed to `loadPdf`; copy it first.                                                                                  |

In a component, a log subscription follows the same pattern as any other subscription:

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

function useReportSdkWarnings() {
  useEffect(() => {
    const unsubscribe = logMessages.on('message', (logMessage) => {
      if (logMessage.level >= LogLevel.Warning)
        reportToMonitoring(logMessage.message, logMessage.data)
    })
    return () => {
      unsubscribe()
    }
  }, [])
}
```

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

pdf.js reads text items with `getTextContent()` and searches with `PDFFindController`. In `@sdk-pdf/react`, `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) resolves with paragraph entries in content order: `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 useReadPageText() {
  const { getParagraphsByPage } = useProgrammaticEditor()

  return async (pageId: string) => {
    const paragraphs = await getParagraphsByPage(pageId)
    return paragraphs.map((paragraph) => paragraph.content).join('\n\n')
  }
}
```

[`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch) replaces `PDFFindController`: set the query and the options, and the SDK finds the matches, 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>
  )
}
```

* **Options.** `isCaseSensitive` and `isWholeWord` replace `caseSensitive` and `entireWord`; there is no diacritics option.
* **Matches.** Each match carries `id`, `pageId`, `paragraphId`, and the matched `text`, in document order; `goToMatch(match.id)` activates one from a results list. A changed query is searched once typing pauses, and `status` is `'searching'` until the whole document is searched.
* **No selection API.** There is no API to read the user's text selection. To jump to a paragraph you located yourself, call `scrollToBlock(blockId)` from [`useScrollToBlock`](https://sdk.pdf.net/docs/v1/reference/useScrollToBlock).

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

Plan around these before you migrate:

| pdf.js feature                                                    | In @sdk-pdf/react                                                                                                     |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Printing                                                          | No print API; `PageCanvas` can render pages for your own print layout, as loaded from the file, without session edits |
| Outline, named destinations, page labels, `PDFHistory`            | No equivalent                                                                                                         |
| View-only rotation (`pagesRotation`)                              | Rotation is a document edit (`useSetPageRotation`)                                                                    |
| Scroll, spread, and single-page modes                             | One scrollable layout                                                                                                 |
| The `'auto'` zoom preset, and fits that stay applied on resize    | Fits are one-time calculations; see [Zoom and fit modes](https://sdk.pdf.net/docs/v1/migration/from-pdfjs#zoom-and-fit-modes)                                         |
| Load progress, range and streaming loading                        | No equivalent                                                                                                         |
| JavaScript execution (`PDFScriptingManager`), XFA                 | No equivalent                                                                                                         |
| Ink drawing, highlight, signature, and comment annotation editors | No equivalent; ink annotations can be created from code with `useInkAnnotationCreate`                                 |
| FDF or XFDF form data export, field-value events                  | No equivalent                                                                                                         |
| Localization (`GenericL10n`), page colors, document permissions   | No equivalent                                                                                                         |

