# Migrating from Apryse WebViewer

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



This guide moves a web application built on Apryse WebViewer to `@sdk-pdf/react`. WebViewer ships a complete viewer UI on top of its `Core` APIs; `@sdk-pdf/react` ships the document engine and the page view and leaves the rest of the UI to you. Most of the migration is rebuilding the WebViewer UI you rely on as React components, then moving your `instance.Core` calls to hooks. 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-apryse#overview): how the two integrations differ, and the API map for every `instance.UI` and `instance.Core` call you use.
* [ ] List the WebViewer UI your users rely on (toolbar groups, panels, popups, dialogs): each becomes a React component.
* [ ] [Install the package and replace the `WebViewer(...)` mount with `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-apryse#install-and-set-up).
* [ ] [Load and close documents](https://sdk.pdf.net/docs/v1/migration/from-apryse#load-and-reset-a-document) with `loadPdf` and `resetPdf`, including the password flow.
* [ ] [Lay out `Viewport` with your own toolbar and sidebars](https://sdk.pdf.net/docs/v1/migration/from-apryse#display-the-document).
* [ ] [Move page navigation from numbers to page ids](https://sdk.pdf.net/docs/v1/migration/from-apryse#pages-and-navigation).
* [ ] [Move page manipulation to the page hooks](https://sdk.pdf.net/docs/v1/migration/from-apryse#page-manipulation).
* [ ] [Move zoom and fit modes](https://sdk.pdf.net/docs/v1/migration/from-apryse#zoom-and-fit-modes).
* [ ] [Rebuild the thumbnails panel with `PageCanvas`](https://sdk.pdf.net/docs/v1/migration/from-apryse#thumbnails).
* [ ] [Move form fields from `FieldManager` to widgets](https://sdk.pdf.net/docs/v1/migration/from-apryse#forms).
* [ ] [Map tool modes, content editing, and read-only mode](https://sdk.pdf.net/docs/v1/migration/from-apryse#editing-and-interaction-tools).
* [ ] [Replace `AnnotationHistoryManager` and `annotationChanged` tracking](https://sdk.pdf.net/docs/v1/migration/from-apryse#undo-redo-and-change-tracking).
* [ ] [Replace XFDF export and `getFileData` with `prepareDocument`](https://sdk.pdf.net/docs/v1/migration/from-apryse#saving-and-downloading).
* [ ] Check [collaboration](https://sdk.pdf.net/docs/v1/migration/from-apryse#collaboration) and convert stored positions using [coordinates](https://sdk.pdf.net/docs/v1/migration/from-apryse#coordinates).
* [ ] [Rebuild the search panel with `useDocumentSearch`](https://sdk.pdf.net/docs/v1/migration/from-apryse#text-search).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-apryse#errors-and-logging) and [cleanup](https://sdk.pdf.net/docs/v1/migration/from-apryse#cleanup-and-lifecycle).
* [ ] [Plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-apryse#features-without-an-equivalent).
* [ ] Remove `@pdftron/webviewer`, the copied static assets, the license key, and WebViewer Server settings. Test encrypted, malformed, large, and form-heavy PDFs, page operations, and the save round-trip.

## Overview [#overview]

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

  **WebViewer** is a packaged application:

  * `WebViewer(options, viewerElement)` embeds a viewer in the page and resolves with an instance. `instance.UI` drives the prebuilt interface; `instance.Core` holds `documentViewer`, `annotationManager`, `Tools`, and `Annotations`.
  * Since version 11 the UI runs in a Web Component by default (an iframe is opt-in). You customize it from outside: hide elements with `UI.disableElements`, switch features with `UI.enableFeatures` and `UI.disableFeatures`, restructure it with the Modular UI, and theme it with `UI.setTheme`.
  * WebViewer serves static assets (WebAssembly modules, HTML, CSS) that you copy into your app, and runs in demo mode without a `licenseKey`. Some features are licensed separately, such as Page Manipulation and the content editing add-on.
  * Form widgets, stamps, and markup are annotation objects managed by `annotationManager`, exported as XFDF, and merged into the PDF on save with `doc.getFileData({ xfdfString })`. Collaboration is XFDF that your own server syncs between clients.
  * Pages are 1-based numbers, and annotation positions are viewer coordinates with the origin at the top left of the page.

  **@sdk-pdf/react** is a library: the SDK owns the document, and you own the UI.

  * There are no prebuilt toolbars, panels, or dialogs. [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) is the main view and [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders single pages; everything around them is your React code driven by hooks.
  * [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the state and configuration inside your React tree. The SDK renders unstyled, and your design system supplies the look through the `theme` configuration.
  * Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers the history; [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) builds the final PDF when you need it.
  * Pages and blocks are addressed by session-scoped ids, and positions are user-space units with the origin at the bottom-left corner of the page.

  | Concern      | WebViewer                                                           | @sdk-pdf/react                                                            |
  | ------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------- |
  | UI           | Prebuilt, customized from outside (`disableElements`, Modular UI)   | `Viewport` and `PageCanvas` only; you build toolbars, panels, and dialogs |
  | Mounting     | `WebViewer(options, element)` in a `useEffect`, Web Component       | `PdfProviders` and components in your React tree                          |
  | Deployment   | Static assets under `path`, `licenseKey`, optional WebViewer Server | The package from a private registry                                       |
  | State access | `instance.Core` managers and their events                           | State hooks that re-render, command hooks, and event hooks                |
  | Pages        | 1-based numbers                                                     | Session-scoped page ids                                                   |
  | Forms        | Fields by full name (`FieldManager`), widgets as annotations        | Widgets with a `fieldName`, values bound with `useWidgetValue`            |
  | Editing text | Content editing add-on                                              | Built in under the `PDF_EDITOR` content policy                            |
  | Persistence  | XFDF for annotations, `getFileData` for the PDF                     | `prepareDocument()` for the PDF                                           |
  | File types   | PDF, Office, and more                                               | PDF                                                                       |

  What this means for your code:

  1. **Build the UI.** Every WebViewer UI element your users rely on becomes a React component built from the hooks in the [API reference](https://sdk.pdf.net/docs/v1/reference).
  2. **Drop the mount effect.** The `WebViewer(...)` promise, the instance reference, and any context that shares it are replaced by `PdfProviders` and hooks.
  3. **Replace managers with hooks.** `documentViewer`, `annotationManager`, `FieldManager`, and `AnnotationHistoryManager` calls become hook calls in the components that need them.
  4. **Replace XFDF persistence.** Storing annotations or field data as XFDF becomes storing the PDF produced by `prepareDocument()`.
  5. **Replace numbers and viewer coordinates** with page ids and user-space coordinates; check stored positions after converting them.
  6. **Map the read-only switches** to content policies, and decide whether to expose editing of existing text, which is built in.
  7. **Remove deployment pieces:** static assets under `path`, the license key, and WebViewer Server settings.
</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-apryse#features-without-an-equivalent).

  | WebViewer                                                                                 | @sdk-pdf/react                                                                                                                                             | Notes                                                                                                         |
  | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
  | `WebViewer({ path, licenseKey }, element)`                                                | `<PdfProviders>`                                                                                                                                           | No asset path or license key.                                                                                 |
  | `initialDoc`, `UI.loadDocument(url \| File \| Blob, options)`                             | `loadPdf(Uint8Array, { password })` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                                     | Read the file or `fetch` the URL into bytes first.                                                            |
  | `loadDocument` option `password`                                                          | `loadPdf(data, { password })`, catch `InvalidPasswordError`                                                                                                |                                                                                                               |
  | `loadDocument` options `onLoadingProgress`, `customHeaders`, `withCredentials`            | **No equivalent**                                                                                                                                          | Headers and credentials go on your own `fetch`.                                                               |
  | `documentLoaded` event                                                                    | `loadState.status === 'ready'`                                                                                                                             |                                                                                                               |
  | `UI.closeDocument()`, `documentViewer.closeDocument()`                                    | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf)                                                                                          |                                                                                                               |
  | `documentViewer.getPageCount()`                                                           | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                                          |                                                                                                               |
  | `documentViewer.getCurrentPage()`, `pageNumberUpdated`                                    | [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId)                                                                                                  | Re-renders with the id of the page in the middle of the viewport.                                             |
  | `documentViewer.setCurrentPage(pageNumber, isSmoothScroll)`                               | `scrollToPage(pageId, { behavior: 'smooth' })` from [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage)                                                |                                                                                                               |
  | `documentViewer.displayPageLocation(...)`                                                 | **No equivalent**                                                                                                                                          | `scrollToBlock` scrolls to a block, not to coordinates.                                                       |
  | `doc.insertBlankPages(insertBeforeThesePages, width, height)`                             | `createPage(width, height, insertAfterPageId?)` from [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate)                                                   | Inserts after, not before.                                                                                    |
  | `doc.removePages(pageArray)`                                                              | `deletePages(pageIdOrIds)` from [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete)                                                                        |                                                                                                               |
  | `doc.movePages(pageArray, insertBeforeThisPage)`                                          | `moveBefore` or `moveAfter` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove)                                                                           |                                                                                                               |
  | `doc.rotatePages(pageArray, rotation)`                                                    | `setPageRotation(pageIdOrIds, rotation)` from [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation)                                                | Absolute degrees, not an added rotation.                                                                      |
  | —                                                                                         | `duplicatePage(sourcePageId, insertAfterPageId?)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate)                                           |                                                                                                               |
  | `pagesUpdated` event                                                                      | [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages)                                                                                                                  | Re-renders with the pages in their current order; page commands emit no events.                               |
  | `documentViewer.zoomTo()`, `UI.setZoomLevel()`, `getZoomLevel()`                          | [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale)                                                                                                  |                                                                                                               |
  | `UI.setFitMode(UI.FitMode.FitWidth)`                                                      | [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale), [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale)                   | A one-time fit, not a fixed mode.                                                                             |
  | `UI.FitMode.FitPage`                                                                      | `Math.min(fitWidthScale, fitHeightScale)`                                                                                                                  | See [Zoom and fit modes](https://sdk.pdf.net/docs/v1/migration/from-apryse#zoom-and-fit-modes).                                                                |
  | `zoomUpdated` event                                                                       | `scale` from `useViewportScale` in a `useEffect`                                                                                                           | `zoomGestureEnd` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports pinch and wheel zoom. |
  | `UI.setLayoutMode(UI.LayoutMode.Single)` and other layout modes                           | **No equivalent**                                                                                                                                          |                                                                                                               |
  | Thumbnails panel, `doc.loadThumbnail()`, `doc.loadCanvas()`                               | [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas)                                                                                                              |                                                                                                               |
  | `annotationManager.getFieldManager().getField(name)`                                      | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets) filtered by `fieldName`                                                                                      | Widgets, not fields.                                                                                          |
  | `field.setValue(value)`, `field.getValue()`                                               | [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                                                                                                      | Inside a component.                                                                                           |
  | `fieldChanged` event                                                                      | `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent)                                                                               | Reports values the user changes in the main view, not values set from code.                                   |
  | `new Annotations.Forms.Field()` with `TextWidgetAnnotation`, `addField`, `addAnnotation`  | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate)                                                                      |                                                                                                               |
  | `FormFieldCreationManager.startFormFieldCreationMode()`                                   | The `{ type: 'addWidget', widgetProps }` tool                                                                                                              |                                                                                                               |
  | `documentViewer.setToolMode()`, `UI.setToolMode()`                                        | [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode)                                                                                        | See [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-apryse#editing-and-interaction-tools).                                          |
  | `ContentEditManager.startContentEditMode()`                                               | The `PDF_EDITOR` content policy, where text is editable                                                                                                    |                                                                                                               |
  | `isReadOnly`, `annotationManager.enableReadOnlyMode()`, `UI.enableViewOnlyMode()`         | `PDF_VIEWER` or `PDF_PRINTER` from [`CONTENT_POLICY_PRESET`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET)                                                     |                                                                                                               |
  | `UI.disableElements()`, Modular UI, `UI.setTheme()`                                       | Your own components and the `theme` configuration, see [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization)                              |                                                                                                               |
  | `AnnotationHistoryManager` `undo()`, `redo()`, `canUndo()`, `canRedo()`, `historyChanged` | [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo)                                                                                                            |                                                                                                               |
  | `annotationChanged` event                                                                 | [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) for user actions, [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) for any change | Ids, not annotation objects.                                                                                  |
  | Unsaved-change tracking through `annotationChanged`                                       | [`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.                                                  |
  | `annotationManager.exportAnnotations()`, `importAnnotations()`                            | **No equivalent**                                                                                                                                          | No XFDF.                                                                                                      |
  | `doc.getFileData({ xfdfString })`                                                         | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                                       | Resolves with a `Uint8Array`, not an `ArrayBuffer`.                                                           |
  | `UI.downloadPdf({ filename })`                                                            | `downloadDocument()`                                                                                                                                       | No file name option.                                                                                          |
  | `flatten` save option                                                                     | **No equivalent**                                                                                                                                          |                                                                                                               |
  | `UI.Events.LOAD_ERROR`, `loadDocument` option `onError`                                   | The rejected `loadPdf` promise                                                                                                                             | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-apryse#errors-and-logging).                                                                |
  | `disableLogs`, `Core.disableLogs()`, `Core.enableWorkerLogs()`                            | [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages)                                                                                                            | The SDK never prints to the console.                                                                          |
  | `UI.dispose()`                                                                            | No dispose call; unsubscribe log listeners yourself                                                                                                        | See [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-apryse#cleanup-and-lifecycle).                                                          |
  | `UI.searchText()`, `documentViewer.textSearchInit()`                                      | [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch)                                                                                                | See [Text search](https://sdk.pdf.net/docs/v1/migration/from-apryse#text-search).                                                                              |
  | `doc.getPDFCoordinates()`, `doc.getViewerCoordinates()`                                   | **No equivalent**                                                                                                                                          | See [Coordinates](https://sdk.pdf.net/docs/v1/migration/from-apryse#coordinates).                                                                              |
</details>

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

### Before: WebViewer [#before-webviewer]

WebViewer installs as `@pdftron/webviewer`, copies its static assets into `public`, and mounts into a `ref` from an effect:

```sh
npm i @pdftron/webviewer
npx --yes cpy-cli "node_modules/@pdftron/webviewer/public/**/*" public/lib/webviewer
```

```jsx
import { useEffect, useRef } from 'react'
import WebViewer from '@pdftron/webviewer'

function App() {
  const viewerRef = useRef(null)

  useEffect(() => {
    WebViewer(
      {
        path: '/lib/webviewer',
        licenseKey: 'YOUR_LICENSE_KEY',
        initialDoc: 'https://example.com/files/contract.pdf',
      },
      viewerRef.current
    ).then((instance) => {
      // const { UI, Core } = instance
    })
  }, [])

  return <div ref={viewerRef} style={{ height: '100vh', width: '100%' }} />
}
```

Under React Strict Mode the effect runs twice in development, so WebViewer integrations add a `hasMounted` ref guard.

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

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.

Mount [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) in your tree. Its props must keep their identity between renders, so memoize the configuration:

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

export function App({ isReadOnly }: { isReadOnly: boolean }) {
  // The PDF application re-renders whenever a PdfProviders prop changes identity.
  const config = useMemo(
    () => ({
      theme: { viewportPadding: 16 },
      // Applied when the viewport first mounts; switch later with useSetInteractionMode.
      initialContentPolicy: isReadOnly
        ? CONTENT_POLICY_PRESET.PDF_VIEWER
        : CONTENT_POLICY_PRESET.PDF_EDITOR,
    }),
    [isReadOnly]
  )

  return (
    <PdfProviders {...config}>
      <DocumentLoader />
    </PdfProviders>
  )
}
```

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

* **No assets, no license key in code.** The install is the registry scope and the package; there is no asset folder to copy and no `path` or `licenseKey` to pass.
* **No mount effect.** The SDK renders as components, so the `useRef` and `useEffect` mount, the Strict Mode guard, and sharing the instance through React context go away: any component under `PdfProviders` reaches the document through hooks.
* **Both default to editable.** WebViewer's `isReadOnly` defaults to `false`, and `PdfProviders` defaults to `PDF_EDITOR`, where text, annotations, links, and widgets are all editable. Map your read-only flag to `initialContentPolicy`, as above. The initial policy applies when the viewport first mounts; switch at runtime with `useSetInteractionMode`, as in [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-apryse#editing-and-interaction-tools).

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

### Before: WebViewer [#before-webviewer-1]

`UI.loadDocument(documentPath, options, documentViewerKey)` takes a URL, `File`, `Blob`, `Core.Document`, or `Core.PDFNet.PDFDoc`. Its options include `filename`, `extension`, `password` (a string, or a function that provides it through a callback), `onLoadingProgress` (for network loads only), and `onError`. The document is usable after `documentLoaded`:

```js
const { documentViewer } = instance.Core

documentViewer.addEventListener('documentLoaded', () => {
  showDocumentUi()
})

instance.UI.loadDocument('https://example.com/files/contract.pdf', {
  filename: 'contract.pdf',
  password: enteredPassword,
  // Only for files loaded over network.
  onLoadingProgress: (percent) => showProgress(percent),
  onError: (error) => showError(error),
})
```

`loadDocument` closes the current document itself, so there is no need to close it before loading another one; `UI.closeDocument()` closes it explicitly.

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

`loadPdf` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) takes the file bytes. A `File` from an input becomes a `Uint8Array` first, and the load state decides what to render:

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

function DocumentLoader() {
  const [loadState, loadPdf] = useLoadPdf()

  async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.currentTarget.files?.[0]
    if (!file) return

    const data = new Uint8Array(await file.arrayBuffer())

    try {
      // loadPdf reads from the buffer it receives, so keep `data` for a retry and pass a
      // copy.
      await loadPdf(data.slice())
    } catch (error) {
      if (error instanceof InvalidPasswordError) {
        // The password is missing or wrong: ask for it, then call
        // loadPdf(data.slice(), { password }) again.
      }
    }
  }

  if (loadState.status === 'initial') {
    return <input type="file" accept=".pdf" onChange={handleFileChange} />
  }

  if (loadState.status === 'loading') {
    return <p>Loading the document…</p>
  }

  return <PdfApplication />
}

function CloseDocumentButton() {
  const resetPdf = useResetPdf()

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

For a URL, `fetch` it and pass `new Uint8Array(await response.arrayBuffer())`.

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

* **PDF only.** `useLoadPdf` loads PDF documents; there is no Office, image, or XOD input, and no `extension` or `filename` to set.
* **Keep a copy for retries.** The document reads from the given buffer for its whole lifetime, and another `loadPdf` call can detach it. To reuse the bytes for a password retry, pass a copy: `loadPdf(data.slice(), { password })`.
* **Without a password, the empty password is tried,** as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`. Instead of a password function, catch the rejection and call `loadPdf` again.
* **No progress or multi-tab loading.** There is no `onLoadingProgress`, and no equivalent of passing several documents to `initialDoc` for multi-tab mode.
* **Closing invalidates ids.** After `resetPdf()`, page and block ids, widget snapshots, and document revisions from the old document are no longer valid. For an open-another-document action, call `resetPdf()` and then `loadPdf`.

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

### Before: WebViewer [#before-webviewer-2]

The prebuilt UI renders the document, toolbars, and panels; integrations trim and theme it from outside:

```js
instance.UI.disableElements(['leftPanel', 'leftPanelButton'])
instance.UI.setTheme(instance.UI.Theme.DARK)
```

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

[`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages, and your components render everything else. A typical shell puts a toolbar above the viewport and a sidebar beside it, as [Building a PDF application](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application) walks through:

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

// A fuller theme than the one in App: pass it as PdfProviders' `theme` prop, defined
// outside components so its identity is stable.
export const theme: Config['theme'] = {
  viewportPadding: [24, 16],
  classNames: { scrollViewport: 'my-viewport', page: 'my-page' },
  styles: {
    scrollViewport: { '--selection-color': 'rebeccapurple' } as React.CSSProperties,
  },
}

function PdfApplication() {
  // Fit the width once per loaded document, like opening in FitWidth.
  useViewportAutoFitScale('width')

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', height: '100vh' }}>
      <ThumbnailsSidebar />
      <div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', minHeight: 0 }}>
        {/* Your toolbar: the navigation, zoom, and history controls from this guide. */}
        <Toolbar />
        {/* The viewport fills its parent, so the parent needs an explicit size. */}
        <div style={{ minHeight: 0 }}>
          <Viewport />
        </div>
      </div>
    </div>
  )
}
```

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

* **There is nothing to hide.** `disableElements`, `enableFeatures`, and the Modular UI configuration have no counterpart. Selection, text editing, form filling, links, and zoom gestures work inside `Viewport` and are limited by the content policy; every control around it exists only if you build it.
* **Theming is CSS you own.** `theme.classNames` and `theme.styles` target the SDK's elements (`scrollViewport`, `page`, `pageCanvas`, `resizable`, `widgetOverlay`, and more), and CSS variables such as `--selection-color` and `--widget-overlay-background` tune the selection and widget visuals; [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization) lists them. There are no built-in light and dark themes.
* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded.
* **One layout.** `UI.setLayoutMode` with `Single`, `Continuous`, `Facing`, and the facing and cover variants, and `DisplayModeManager.setDisplayMode` on `DocumentViewer`, have no equivalent: `Viewport` shows the pages in one scrollable area.

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

### Before: WebViewer [#before-webviewer-3]

`DocumentViewer` navigates by 1-based page number and reports changes with `pageNumberUpdated`:

```js
const { documentViewer } = instance.Core

pageCountLabel.textContent = documentViewer.getPageCount()

documentViewer.addEventListener('pageNumberUpdated', (pageNumber) => {
  pageSelect.value = pageNumber
})

pageSelect.onchange = () => documentViewer.setCurrentPage(Number(pageSelect.value), true)
```

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

[`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) bridges positions and ids: a position is an index in the list, and navigation with [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage) takes an id. [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId) replaces `pageNumberUpdated`:

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

function PagePicker() {
  const pages = usePages()
  // The page in the middle of the viewport; undefined until the viewport has one.
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()

  return (
    <select
      value={currentPageId ?? pages[0]?.id}
      onChange={(event) =>
        scrollToPage(event.currentTarget.value, { behavior: 'smooth' })
      }
    >
      {pages.map((page, index) => (
        <option key={page.id} value={page.id}>
          Page {index + 1} of {pages.length}
        </option>
      ))}
    </select>
  )
}
```

When you need a page's number inside a handler, such as for analytics, `getPageIndex(pageId)` from [`usePageIndexGetter`](https://sdk.pdf.net/docs/v1/reference/usePageIndexGetter) resolves an id to its zero-based index without re-rendering on order changes.

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

* **Ids survive reordering; numbers don't.** An id keeps pointing at the same page while pages are added, removed, and rearranged by the user, where a number would drift to another page. Ids are session-scoped: persist page numbers, not ids.
* **Scroll options.** `scrollToPage` aligns the page with `vertical` (`'top'` by default) and `horizontal` (`'nearest'` by default); `isSmoothScroll` becomes `behavior: 'smooth'`.
* **Scrolling to a spot.** `displayPageLocation(pageNumber, horizontalPosition, verticalPosition)` has no equivalent. `scrollToBlock(blockId, options?)` from [`useScrollToBlock`](https://sdk.pdf.net/docs/v1/reference/useScrollToBlock) scrolls to a block, such as a form widget.

## Page manipulation [#page-manipulation]

### Before: WebViewer [#before-webviewer-4]

`Core.Document` changes pages by number. These methods work on PDF documents only, resolve with a `PageUpdateResult`, and are licensed as part of Page Manipulation:

```js
const doc = instance.Core.documentViewer.getDocument()

// Inserts blank pages before the given pages; the size is in PDF points.
await doc.insertBlankPages([2], 612, 792)
await doc.removePages([4])
await doc.movePages([5], 1)
// Adds the rotation to the pages' current rotation.
await doc.rotatePages([1], instance.Core.PageRotation.E_90)
```

### 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), [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation), and [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate):

```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 { moveBefore } = usePageMove()
  const setPageRotation = useSetPageRotation()

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

  return (
    <div role="toolbar">
      {/* A4 is roughly 595 × 842 user-space units; the new page goes after this one. */}
      <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, moveBefore inserts at the start of the document. */}
      <button onClick={() => moveBefore(pageId)}>Move to start</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 | WebViewer                                                | @sdk-pdf/react                                                                                                  |
| --------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Insert    | Before given page numbers; defaults 612 × 792 PDF points | After `insertAfterPageId`, or appended; `width` and `height` in user-space units (1/72 of an inch), no defaults |
| Move      | Before `insertBeforeThisPage`                            | `moveBefore(ids, beforePageId?)` (start when omitted) or `moveAfter(ids, afterPageId?)` (end when omitted)      |
| Rotate    | Adds a `Core.PageRotation` to the current rotation       | Sets an absolute angle; `-90` and `450` are normalized; angles off a quarter turn throw `InvalidInputError`     |
| Duplicate | —                                                        | `duplicatePage` is asynchronous and inserts after the source by default                                         |

* **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 page events.** Page commands emit no events: track them where you call them. [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) re-renders with the current pages and their order, which covers what `pagesUpdated` reported.
* **Extracting pages.** The counterpart of `extractPages` is `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving), which saves only the listed pages, in the listed order.
* **No page import, merging, or cropping.** `insertPages`, `mergeDocument`, and `cropPages` have no equivalent.

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

### Before: WebViewer [#before-webviewer-5]

Zoom is a number or a percentage, and a fit mode fixes the zoom: `FitWidth` fits the width of the page, and `FitPage` fits the width or the height, whichever is smaller. Both calls need a loaded document:

```js
const { documentViewer } = instance.Core

documentViewer.addEventListener('documentLoaded', () => {
  instance.UI.setFitMode(instance.UI.FitMode.FitWidth)
  // or a fixed zoom: instance.UI.setZoomLevel('150%')
})

documentViewer.addEventListener('zoomUpdated', (zoom) => {
  persistZoom(zoom)
})
```

### 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` (in `PdfApplication` above) 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, like zoomUpdated.
  useEffect(() => persistZoom(scale), [scale])

  function fitPage() {
    const { fitWidthScale, fitHeightScale } = getFitScale()
    // The smaller scale fits the pages in both directions, like FitPage.
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

  return (
    <div role="toolbar">
      <button onClick={() => setScale(scale / 1.25)}>−</button>
      <span>{Math.round(scale * 100)}%</span>
      <button onClick={() => setScale(scale * 1.25)}>+</button>
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={fitPage}>Fit page</button>
    </div>
  )
}
```

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

* **Fits are one-time calculations.** [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/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 as the viewport resizes.
* **What the fits measure.** `fitWidthScale` fits the widest page into the viewport width and `fitHeightScale` the tallest page into its height, both accounting for `viewportPadding`.
* **Scale 1 is 100%:** one user-space unit per CSS pixel. `setScale` takes a positive finite number, not a percentage string.
* **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.
* **Gestures.** Pinch and wheel zoom are built in and tuned with `zoomGestures` (`minScale`, `maxScale`, wheel speed) on `PdfProviders`. During a gesture, `scale` keeps its value until the gesture ends; `zoomGestureStart` and `zoomGestureEnd` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) mark it.

## Thumbnails [#thumbnails]

### Before: WebViewer [#before-webviewer-6]

The prebuilt UI has a thumbnails tab in the left panel with page operations; `UI.Feature.ThumbnailReordering` and `ThumbnailMerging` toggle reordering and merging. Custom thumbnails come from `loadThumbnail`:

```js
const { documentViewer } = instance.Core

documentViewer.addEventListener('documentLoaded', () => {
  const doc = documentViewer.getDocument()
  doc.loadThumbnail(1, (thumbnail) => {
    // thumbnail is a HTMLCanvasElement or HTMLImageElement
    thumbnailContainer.appendChild(thumbnail)
  })
})
```

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

[`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders a page lazily at the canvas's CSS size; the sidebar, its selection, and its page operations are yours. A sidebar with reordering buttons:

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

function ThumbnailsSidebar() {
  const pages = usePages()
  const scrollToPage = useScrollToPage()
  const { moveBefore, moveAfter } = usePageMove()

  return (
    <nav>
      {pages.map((page, index) => (
        <div key={page.id}>
          <button onClick={() => scrollToPage(page.id)}>
            <PageCanvas
              pageId={page.id}
              className="thumbnail"
              role="img"
              aria-label={`Thumbnail ${index + 1}`}
            />
          </button>
          <button
            disabled={index === 0}
            onClick={() => moveBefore(page.id, pages[index - 1]?.id)}
          >
            Move up
          </button>
          <button
            disabled={index === pages.length - 1}
            onClick={() => moveAfter(page.id, pages[index + 1]?.id)}
          >
            Move down
          </button>
        </div>
      ))}
    </nav>
  )
}
```

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

* **Lazy and sized with CSS.** An off-screen canvas does no work until it becomes visible, and the render resolution follows the displayed CSS size or an explicit `scale`. In a virtualized list, pass `isVisible`.
* **Annotations included.** `PageCanvas` draws annotations, form widgets included, unless `hideAnnotations` is set; there is no separate draw step as with `loadCanvas` and `drawAnnotations`.
* **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 followed live.
* **No merging.** Dropping another file into the thumbnails (`ThumbnailMerging`) has no equivalent.

## Forms [#forms]

### Before: WebViewer [#before-webviewer-7]

Fields are looked up by full name through the field manager, and widgets are annotations:

```js
const { documentViewer, annotationManager } = instance.Core

documentViewer.addEventListener('annotationsLoaded', () => {
  // getField returns null when no field has that full name.
  const field = annotationManager.getFieldManager().getField('customer.name')
  if (field) field.setValue('Jane Doe')
})

annotationManager.addEventListener('fieldChanged', (field, value) => {
  autosave(field.name, value)
})
```

Creating a field takes a `Field`, a widget annotation positioned in viewer coordinates, and both managers:

```js
const { documentViewer, annotationManager, Annotations } = instance.Core

documentViewer.addEventListener('documentLoaded', () => {
  const field = new Annotations.Forms.Field('customer.name', { type: 'Tx' })
  const widgetAnnot = new Annotations.TextWidgetAnnotation(field)
  widgetAnnot.PageNumber = 1
  widgetAnnot.X = 100
  widgetAnnot.Y = 100
  widgetAnnot.Width = 200
  widgetAnnot.Height = 50

  annotationManager.getFieldManager().addField(field)
  annotationManager.addAnnotation(widgetAnnot)
  annotationManager.drawAnnotationsFromList([widgetAnnot])
})
```

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

Forms are widget-centric: you work with the widgets on the pages, and each value is shaped like the value of the matching HTML form control, as [Working with forms](https://sdk.pdf.net/docs/v1/guides/working-with-forms) explains. The counterpart of `getField(name)` is a widget lookup by `fieldName`, bound with [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue):

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

function isTextWidget(widget: Widget): widget is TextWidget {
  return widget.widgetType === 'text'
}

// The counterpart of getFieldManager().getField(fieldName) for a text field, as a bound
// input.
function TextFieldByName({ fieldName }: { fieldName: string }) {
  const { widgets } = useWidgets()
  const widget = widgets
    .filter(isTextWidget)
    .find((widget) => widget.fieldName === fieldName)

  return widget ? <TextField widget={widget} /> : null
}

function TextField({ widget }: { widget: TextWidget }) {
  // Like a useState backed by the document: filling in the main view and undo update it.
  const [value, setValue] = useWidgetValue(widget)

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

function useAddNameField() {
  const createWidgets = useWidgetsCreate()

  return async (pageId: string) => {
    const [widget] = await createWidgets({
      pageId,
      widgetType: 'text',
      // User-space units: the origin is at the bottom-left corner of the page.
      x: 72,
      y: 700,
      width: 180,
      height: 24,
      fieldName: 'customer.name',
    })
    return widget
  }
}
```

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

| Concept              | WebViewer                                               | @sdk-pdf/react                                                                                                                                                                                                                   |
| -------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lookup               | `getField(name)`, `getFields()`                         | `useWidgets()` (re-renders), or `await getWidgets()` from [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter) (one-time, loads every page with widgets first)                                                              |
| Shared values        | A field with several widgets                            | Every widget with the same field name shows and synchronizes the same value                                                                                                                                                      |
| Values               | `field.setValue(value)`, `getValue()`                   | `useWidgetValue(widget)`: `string` for `text` and `choice`, `boolean` for `checkbox` and `radio`, option indices for `multiChoice`                                                                                               |
| Change events        | `fieldChanged(field, value)`                            | `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports a value the user changed in the main view; values set from code, undo, and redo emit nothing, and `useWidgetValue` re-renders on any change |
| Creation             | `Field`, widget annotation, `addField`, `addAnnotation` | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate); required: `pageId`, `widgetType`, `x`, `y`, `width`, `height`; a unique `fieldName` is generated when omitted                             |
| Interactive creation | `FormFieldCreationManager.startFormFieldCreationMode()` | The `{ type: 'addWidget', widgetProps }` interaction tool, under an editable `widgets` policy                                                                                                                                    |
| Deletion             | `annotationManager.deleteAnnotations()`                 | [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) with widget ids; same-named widgets act as one field                                                                                                                       |
| Properties           | Widget annotation properties and flags                  | `setWidgetProps(ids, props, options?)` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps): `isReadOnly`, `borderColor`, `fillColor`, `fontSize`, `isMultiline`, `options`, and more                                |
| Form data            | XFDF export and import                                  | No equivalent                                                                                                                                                                                                                    |

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

* **Coordinates differ.** WebViewer widget `X` and `Y` are viewer coordinates with the origin at the top left of the page; `@sdk-pdf/react` widget positions are user-space units with the origin at the bottom left. Check placement on a test document before converting stored positions.
* **Values are read and written per widget.** `useWidgetValue` binds one widget per hook call, and there is no setter by field name: to prefill a form, render a bound component per widget, like `TextFieldByName` above.
* **Read-only fields.** `isReadOnly` restricts filling the field, not authoring the widget; with `{ excludeFromHistory: true }`, an undo can't unlock a submitted form.
* **Date fields.** A text widget's `format` exposes Acrobat date formats as `FormatType.DateTime`, recognized without running scripts; see [`FormatType`](https://sdk.pdf.net/docs/v1/reference/FormatType). Fields driven by other scripts read as plain text fields.
* **Signatures.** `signature` widgets carry a read-only `isCryptoSigned`; the SDK never signs.

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

### Before: WebViewer [#before-webviewer-8]

Tools are switched by name, content editing is an add-on mode, and read-only is a switch on the annotation manager or the UI:

```js
const { documentViewer, annotationManager, Tools } = instance.Core

documentViewer.setToolMode(documentViewer.getTool(Tools.ToolNames.EDIT))
instance.UI.setToolMode(Tools.ToolNames.TEXT_SELECT)

// Content editing is a separately licensed add-on.
instance.UI.enableFeatures([instance.UI.Feature.ContentEdit])
documentViewer.getContentEditManager().startContentEditMode()

// No annotations can be added or commented on.
annotationManager.enableReadOnlyMode()
// Disables all editing features in the UI.
instance.UI.enableViewOnlyMode()
```

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

The **interaction mode** combines a **content policy** (what may change) and an **interaction tool** (what a gesture does); [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) switches both at once:

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

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

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

  return (
    <div role="toolbar">
      {/* Like UI.enableViewOnlyMode(): read-only text, interactive links and widgets. */}
      <button
        aria-pressed={interactionTool.type === 'textSelect'}
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_VIEWER)
        }
      >
        View
      </button>
      {/* Edit mode: select, move, and resize blocks, with editable text. */}
      <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 paragraph
      </button>
    </div>
  )
}
```

### Mapping the tools [#mapping-the-tools]

| `Core.Tools.ToolNames` and modes                                                | @sdk-pdf/react                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EDIT`                                                                          | `{ type: 'select' }`: select, move, and resize blocks.                                                                                                                                                                                                                                                                                                                             |
| `TEXT_SELECT`                                                                   | `{ type: 'textSelect' }`: native text selection for reading and copying; needs read-only text.                                                                                                                                                                                                                                                                                     |
| `CONTENT_EDIT` and `startContentEditMode()`                                     | Text is editable in place when the policy's `text` level is `'editable'` (`PDF_EDITOR`); there is no separate mode or add-on.                                                                                                                                                                                                                                                      |
| `ADD_PARAGRAPH`                                                                 | `{ type: 'addText' }`: creates a paragraph at the click position and focuses it for typing; needs editable text.                                                                                                                                                                                                                                                                   |
| `ADD_IMAGE_CONTENT`, `STAMP` with an image                                      | `{ type: 'addImage', image, width?, height? }` with `await stageImage(blob)` places the image as a stamp annotation, not page content; the optional size is in user-space units, and a single dimension keeps the aspect ratio. See [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage); [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate) places one from code. |
| `FormFieldCreationManager`                                                      | `{ type: 'addWidget', widgetProps }`, under an editable `widgets` policy.                                                                                                                                                                                                                                                                                                          |
| `PAN`                                                                           | No tool: `Viewport` scrolls and zooms on its own.                                                                                                                                                                                                                                                                                                                                  |
| `FREETEXT`                                                                      | The nearest tool is `addText` (see `ADD_PARAGRAPH`), which creates a text paragraph and needs editable `text`.                                                                                                                                                                                                                                                                     |
| `FREEHAND`                                                                      | No drawing tool. [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from strokes your code collects.                                                                                                                                                                                                                                  |
| `RECTANGLE`, `HIGHLIGHT`, `STICKY`, `SIGNATURE`, `REDACTION`, measurement tools | No equivalent.                                                                                                                                                                                                                                                                                                                                                                     |
| `isReadOnly`, `enableReadOnlyMode()`, `UI.enableViewOnlyMode()`                 | `CONTENT_POLICY_PRESET.PDF_VIEWER` (widgets stay fillable) or `PDF_PRINTER` (widgets read-only too).                                                                                                                                                                                                                                                                               |

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

* **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).
* **Per-kind permissions.** A custom [content policy](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) is a plain object with `text`, `annotations`, `links`, and `widgets` levels, finer than a single read-only switch. `'hidden'` doesn't render that kind of content at all, and a level can be a function of the block id, such as `widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive')`. There is no hook to read the policy back: keep the mode you set in your own state to show or hide editing UI.
* **Local mode.** Switching the mode affects this client only.
* **Selection toolbars.** [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle) (font, size, color, bold, italic, alignment, spacing) and [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps) (border color and width) replace the style popups of the prebuilt UI. For widget settings such as multiline, read the selected widgets from [`useSelectedBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectedBlocks) and change them with `setWidgetProps` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps). [`useSelection`](https://sdk.pdf.net/docs/v1/reference/useSelection) tells a paragraph in text editing from selected blocks.
* **Programmatic text edits.** [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) reads paragraphs and rewrites them without UI.

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

### Before: WebViewer [#before-webviewer-9]

`AnnotationHistoryManager` handles undo and redo of annotation changes and emits `historyChanged`. Change tracking listens to `annotationChanged` and skips imported changes:

```js
const { documentViewer, annotationManager } = instance.Core
const historyManager = documentViewer.getAnnotationHistoryManager()

undoButton.onclick = () => historyManager.undo()
redoButton.onclick = () => historyManager.redo()

historyManager.addEventListener('historyChanged', () => {
  undoButton.disabled = !historyManager.canUndo()
  redoButton.disabled = !historyManager.canRedo()
})

annotationManager.addEventListener(
  'annotationChanged',
  (annotations, action, { imported }) => {
    if (imported) return
    setUnsavedChanges(true)
  }
)
```

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

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

function HistoryToolbar() {
  // canUndo and canRedo re-render the component as the history changes, like
  // historyChanged.
  const { undo, redo, canUndo, canRedo } = useUndoRedo()

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

Unsaved-change tracking moves to 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 indicator lives with the save button in [Saving and downloading](https://sdk.pdf.net/docs/v1/migration/from-apryse#saving-and-downloading).

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

* **Beyond annotations.** [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo) covers the 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 and merged.** Undo reverts the local user's changes, and edits within 500 ms merge into one 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.
* **Events for user actions only.** Where `annotationChanged` fed analytics or side effects, [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports what users do in the main view, such as `textEditorInput`, `addToolApplied`, `blocksDragEnd`, and `widgetInput`, with block ids rather than annotation objects. Commands your code calls, undo, and redo emit no events, so there is no `imported` flag to check: track your own calls where you make them, and watch `useDocumentRevision` for any change.
* **No history reset.** `AnnotationHistoryManager.clear()` has no equivalent; a save only moves the revision you compare against.

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

### Before: WebViewer [#before-webviewer-10]

Annotations are exported as XFDF and merged into the file data; `getFileData` resolves with an `ArrayBuffer`:

```js
const { documentViewer, annotationManager } = instance.Core

const doc = documentViewer.getDocument()
const xfdfString = await annotationManager.exportAnnotations()
const data = await doc.getFileData({ xfdfString })
const blob = new Blob([new Uint8Array(data)], { type: 'application/pdf' })

const body = new FormData()
body.append('mydoc.pdf', blob, 'mydoc.pdf')
await fetch('/api/documents', { method: 'POST', body })

// Or let the UI download it.
await instance.UI.downloadPdf({ filename: 'contract.pdf' })
```

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

[`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) applies the recorded changes to the original document; there is no separate annotation layer to export:

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

function SaveButtons() {
  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 annotationChanged-based tracking: undoing back to the saved state clears it
  // too.
  const hasUnsavedChanges = revision !== savedRevision

  async function upload() {
    if (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 goes to fetch, so don't hand out the loaded document's original
      // buffer.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })

      const body = new FormData()
      body.append(
        'mydoc.pdf',
        new Blob([bytes as BlobPart], { type: 'application/pdf' }),
        'mydoc.pdf'
      )
      const response = await fetch('/api/documents', { method: 'POST', body })
      // 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 upload()}>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-10]

* **One artifact.** WebViewer lets you store XFDF apart from the PDF, or post each change as an XFDF command. `@sdk-pdf/react` saves the complete PDF: `prepareDocument()` resolves with its bytes as a `Uint8Array`, and `downloadDocument()` saves the same bytes on the user's device.
* **returnOriginalIfUnchanged** defaults to `true`, returning the buffer passed to `loadPdf` when nothing changed; set it to `false` when the bytes will be transferred or mutated.
* **Save options.** Besides `returnOriginalIfUnchanged`, `pageIds` saves only the listed pages, in the listed order. There is no download file name, flattening, linearization (`Core.SaveOptions.LINEARIZED`), or encryption on save.

## Collaboration [#collaboration]

WebViewer leaves collaboration to you: your server syncs XFDF between clients, optionally as XFDF commands from `exportAnnotationCommand`.

Real-time collaboration in `@sdk-pdf/react` is coming soon. Your XFDF sync code doesn't carry over: edits are CRDT operations, not annotation exports.

## Coordinates [#coordinates]

| Topic          | WebViewer                                                                               | @sdk-pdf/react                                                          |
| -------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Page reference | 1-based page numbers                                                                    | Session-scoped page ids; the position is the index in `usePages()`      |
| Origin         | Viewer coordinates: top left of the page, y pointing down; PDF coordinates: bottom left | User space with the origin at the bottom-left corner of the page        |
| Units          | PDF points for `insertBlankPages`                                                       | User-space units, one unit is 1/72 of an inch                           |
| Page size      | `documentViewer.getPageWidth()`, `getPageHeight()`, with rotation applied               | `width` and `height` of the `usePages()` entries, with rotation applied |
| Conversion     | `getPDFCoordinates`, `getViewerCoordinates`                                             | No conversion helpers                                                   |

## Text search [#text-search]

### Before: WebViewer [#before-webviewer-11]

The prebuilt UI has a search panel, and `UI.searchText` runs a search from code with options such as `caseSensitive`, `wholeWord`, and `regex`:

```js
instance.UI.searchText('invoice', { caseSensitive: true, wholeWord: true })
```

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

[`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch) holds the query, the options, and the matches; the SDK highlights the matches on the pages and scrolls to the active one, and the search panel is yours:

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

function SearchPanel() {
  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.isWholeWord}
          onChange={(event) =>
            setSearchCriteria({ ...criteria, isWholeWord: event.currentTarget.checked })
          }
        />
        Whole word
      </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-11]

* **Options.** `isCaseSensitive` and `isWholeWord` replace `caseSensitive` and `wholeWord`; regular expressions and wildcards have no equivalent.
* **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.
* **Styling.** The highlights take their look from the `searchMatch` key of `theme.classNames` and `theme.styles`, as [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization) describes.
* **No selection API.** `getSelectedText` has no equivalent.

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

### Before: WebViewer [#before-webviewer-12]

Load failures arrive through the `loaderror` UI event or the `onError` option, and logging is switched off with `disableLogs` or `Core.disableLogs`:

```js
instance.UI.addEventListener(instance.UI.Events.LOAD_ERROR, (error) => {
  showError(error)
})

instance.Core.disableLogs(true)
```

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

Failures are typed errors from the call that failed, and recoverable problems are log messages you subscribe to with [`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() {
  // The SDK never prints to the console: subscribe, and unsubscribe on unmount.
  useEffect(() => {
    const unsubscribe = logMessages.on('message', (logMessage) => {
      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) {
      // Replaces the LOAD_ERROR listener for this load.
      if (error instanceof InvalidPasswordError) askForPassword()
      else showError(error)
    }
  }
}
```

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

* **Error classes.** `DocumentNotLoadedError`, `InvalidInputError`, `InvalidPasswordError`, `DocumentFormatError` (malformed beyond what the SDK can work around), `NotSupportedError` (a PDF feature not supported yet), and `UninitializedError`, all extending `GenericError` with a static `message` and an optional `data` object.
* **Nothing half-applied.** Commands validate first and throw before touching anything; readers return empty values instead of throwing; hooks never throw during render.
* **Logs replace console output.** Where WebViewer logs to the console unless disabled, `@sdk-pdf/react` delivers `level`, `reason`, a static `message`, `data`, and `stack` to your subscriber. There is no `UI.displayErrorMessage`: error UI is yours.

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

### Before: WebViewer [#before-webviewer-13]

`UI.dispose()` cleans up the instance's listeners and data and belongs in the code that removes WebViewer from the DOM. In a React effect, the cleanup also has to handle an unmount that happens before `WebViewer()` resolves:

```jsx
useEffect(() => {
  let instance
  let isUnmounted = false

  WebViewer({ path: '/lib/webviewer', licenseKey }, viewerRef.current).then((created) => {
    instance = created
    // The component may unmount before WebViewer resolves, such as under Strict Mode.
    if (isUnmounted) instance.UI.dispose()
  })

  return () => {
    isUnmounted = true
    instance?.UI.dispose()
  }
}, [])
```

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

* **Subscriptions follow components.** [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) subscribes for the component's lifetime, with callbacks that don't need memoizing. [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) installs window-level shortcuts, and passing a falsy value instead of the mapping disables them, such as while a modal is open.
* **Log subscriptions** return an unsubscribe function.
* **Closing a document** is `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf), which returns the load state to `'initial'`.

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

Plan around these before you migrate:

| WebViewer feature                                                                             | In @sdk-pdf/react                                                                                                                                                   |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Reading the selected text (`getSelectedText`)                                                 | No equivalent                                                                                                                                                       |
| Creating markup and shape annotations, sticky notes, free text, and measurements; drawing ink | No equivalent; existing shapes, lines, and ink display, and their border color and width can be changed. Ink can be created from code with `useInkAnnotationCreate` |
| Redaction, digital signatures                                                                 | No equivalent; the SDK never signs                                                                                                                                  |
| XFDF import and export, annotation commands, flattening                                       | No equivalent                                                                                                                                                       |
| Coordinate conversion (`getPDFCoordinates`, `getViewerCoordinates`)                           | No equivalent                                                                                                                                                       |
| Office, image, and XOD files; multi-tab mode                                                  | PDF only                                                                                                                                                            |
| Layout modes, fixed fit modes, `displayPageLocation`                                          | No equivalent                                                                                                                                                       |
| Inserting pages from another document, merging, cropping                                      | No equivalent; `prepareDocument({ pageIds })` extracts pages                                                                                                        |
| Load progress, custom headers, credentials                                                    | Use your own `fetch`; no progress                                                                                                                                   |
| Built-in UI, themes, localization                                                             | The UI is yours                                                                                                                                                     |
| WebViewer Server                                                                              | No equivalent                                                                                                                                                       |

