# Migrating from Foxit PDF SDK for Web

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



This guide moves a web application built on Foxit PDF SDK for Web to `@sdk-pdf/react`. It covers both ways Foxit apps are built: the complete viewer of the UIExtension layer (`PDFUI`, with its ribbon, panels, and add-ons), and the PDFViewCtrl layer alone (`PDFViewer`) under a UI of your own. Both work on the same `PDFDoc` and `PDFPage` objects, so most `@sdk-pdf/react` replacements are the same for either setup. 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-foxit#overview): how the two integrations differ, and the API map for every `PDFUI`, `PDFViewer`, and `PDFDoc` call you use.
* [ ] List the built-in UI your users rely on (ribbon tabs, the thumbnail and comment panels, context menus, dialogs, add-ons): each becomes a React component.
* [ ] [Install the package and replace the `new PDFUI(...)` mount with `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-foxit#install-and-set-up).
* [ ] [Load and close documents](https://sdk.pdf.net/docs/v1/migration/from-foxit#load-and-reset-a-document) with `loadPdf` and `resetPdf`, including the password flow.
* [ ] [Replace the appearance, layout template, and fragments with your own layout around `Viewport`](https://sdk.pdf.net/docs/v1/migration/from-foxit#display-the-document).
* [ ] [Move page navigation from zero-based indexes to page ids](https://sdk.pdf.net/docs/v1/migration/from-foxit#pages-and-navigation).
* [ ] [Move page operations from page ranges to the page hooks](https://sdk.pdf.net/docs/v1/migration/from-foxit#page-manipulation).
* [ ] [Move zoom and fit modes](https://sdk.pdf.net/docs/v1/migration/from-foxit#zoom-and-fit-modes).
* [ ] [Rebuild the thumbnail panel with `PageCanvas`](https://sdk.pdf.net/docs/v1/migration/from-foxit#thumbnails).
* [ ] [Move form fields from `PDFForm` and `PDFFormField` to widgets](https://sdk.pdf.net/docs/v1/migration/from-foxit#forms).
* [ ] [Check which annotation workflows carry over](https://sdk.pdf.net/docs/v1/migration/from-foxit#annotations).
* [ ] [Map state handlers, read-only permissions, and text editing to the interaction mode](https://sdk.pdf.net/docs/v1/migration/from-foxit#editing-and-interaction-tools).
* [ ] [Replace the undo-redo add-on and `isDocModified()`](https://sdk.pdf.net/docs/v1/migration/from-foxit#undo-redo-and-change-tracking).
* [ ] [Replace `getFile()` with `prepareDocument`](https://sdk.pdf.net/docs/v1/migration/from-foxit#saving-and-downloading).
* [ ] [Move error handling](https://sdk.pdf.net/docs/v1/migration/from-foxit#errors-and-logging), [cleanup](https://sdk.pdf.net/docs/v1/migration/from-foxit#cleanup-and-lifecycle), and [text extraction and search](https://sdk.pdf.net/docs/v1/migration/from-foxit#text-extraction-and-search).
* [ ] [Plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-foxit#features-without-an-equivalent).
* [ ] Remove `@foxitsoftware/foxit-pdf-sdk-for-web-library`, the copied `lib` and `external` folders, `license-key.js`, and the `Service-Worker-Allowed` header you added for Foxit. 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>

  **Foxit PDF SDK for Web** is built in three layers on a WebAssembly engine:

  * **Core:** `PDFDoc` and `PDFPage` read and change the document. Pages are zero-based indexes, page ranges are two-dimensional arrays such as `[[0], [2, 4]]`, and rectangles are PDF coordinates (`left`, `bottom`, `right`, `top`).
  * **PDFViewCtrl:** `PDFViewer` renders the document into a DOM element you pass to `init()`. It owns the scrolling, zoom, view modes, and the active tool (a state handler), and it reports what happens through viewer events. `getPDFDocRender()` and `getPDFPageRender(index)` reach the rendered document and pages.
  * **UIExtension:** `PDFUI` wraps a `PDFViewer` and mounts a complete viewer into `renderTo`: a ribbon or mobile appearance, a layout template you adjust with fragments, and features delivered as add-ons from `lib/uix-addons` (thumbnails, undo and redo, print, search, form design, and more), often loaded together as `allInOne.js`.
  * You serve the package's `lib` folder (engine modules, workers, add-ons, locales, stamps) as static files and point `libPath` at it, optionally with fonts from `external`. A `licenseSN` and `licenseKey` go into the `jr` options, and the license decides which feature modules load; advanced editing is licensed separately.
  * Edits change the `PDFDoc` in the engine, and `getFile()` exports it as a PDF `File`. Annotations and form data can also travel separately as JSON, FDF, or XFDF.

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

  * There is no prebuilt UI to configure. [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) is the main view, [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders single pages, and every toolbar, panel, and dialog is your React code driven by hooks.
  * [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the document state and the 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      | Foxit PDF SDK for Web                                                          | @sdk-pdf/react                                                            |
  | ------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
  | UI           | `PDFUI` with appearances, layout templates, fragments, and add-ons             | `Viewport` and `PageCanvas` only; you build toolbars, panels, and dialogs |
  | Mounting     | `new PDFUI({ renderTo })` or `new PDFViewer()` plus `init()` in an effect      | `PdfProviders` and components in your React tree                          |
  | Deployment   | The `lib` folder under `libPath`, fonts, license key, `Service-Worker-Allowed` | The package from a private registry                                       |
  | State access | `PDFViewer`, `PDFDocRender`, and `PDFDoc` objects, viewer events               | State hooks that re-render, command hooks, and event hooks                |
  | Pages        | Zero-based indexes and two-dimensional page ranges                             | Session-scoped page ids                                                   |
  | Tools        | State handlers switched with `StateHandlerManager.switchTo()`                  | An interaction tool paired with a content policy                          |
  | Forms        | `PDFForm`, `PDFFormField`, and `Widget` objects                                | Widgets with a `fieldName`, values bound with `useWidgetValue`            |
  | Editing text | Std Edit, or the separately licensed Adv Edit                                  | Built in under the `PDF_EDITOR` content policy                            |
  | Undo         | The undo-redo add-on, for annotation, bookmark, and Adv Edit changes           | `useUndoRedo` for the whole document                                      |
  | Persistence  | `getFile()` for the PDF; JSON, FDF, or XFDF for annotations and form data      | `prepareDocument()` for the PDF                                           |

  What this means for your code:

  1. **Build the UI.** Every part of the Foxit UI your users rely on (ribbon groups, the thumbnail and comment panels, context menus, the print and password dialogs) 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 `PDFUI` or `PDFViewer` instance, the `getPDFViewer()` promise, and any context that shares them are replaced by `PdfProviders` and hooks.
  3. **Replace objects with hooks.** `PDFDocRender`, `PDFDoc`, `PDFForm`, `StateHandlerManager`, and add-on calls become hook calls in the components that need them.
  4. **Replace indexes with page ids.** Every page index and page range in your state becomes a page id; convert at the edges (deep links, stored data) with `usePages` and `usePageIndexGetter`.
  5. **Rethink annotations.** `@sdk-pdf/react` edits page text, form widgets, image stamps, and pages, creates ink from code, and restyles the borders of existing shapes, lines, and ink; creating markup annotations, comments, and their JSON, FDF, or XFDF exchange have no equivalent.
  6. **Map permissions** such as `customs.getDocPermissions` to content policies, and decide whether to expose editing of existing text, which is built in.
  7. **Remove deployment pieces:** the copied `lib` folder, fonts, the license key, and the service worker header.
</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-foxit#features-without-an-equivalent). `pdfui` is a `PDFUI`, `pdfViewer` the `PDFViewer` from `await pdfui.getPDFViewer()`, `docRender` its `getPDFDocRender()`, and `pdfDoc` its `getCurrentPDFDoc()`.

  | Foxit PDF SDK for Web                                                                              | @sdk-pdf/react                                                                                                                               | Notes                                                                |
  | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
  | `new PDFUI({ viewerOptions: { libPath, jr }, renderTo, addons })`                                  | `<PdfProviders>`                                                                                                                             | No `libPath`, license key, or add-ons.                               |
  | `new PDFViewer({ libPath, jr })`, `pdfViewer.init(element)`                                        | `<PdfProviders>` and `<Viewport />`                                                                                                          |                                                                      |
  | `openPDFByFile(file, { password, fileName })`                                                      | `loadPdf(Uint8Array, { password })` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                       | Read a `File` or `Blob` into bytes first.                            |
  | `openPDFByHttpRangeRequest({ range: { url, headers } })`                                           | **No equivalent**                                                                                                                            | `fetch` the whole file, with your headers, then `loadPdf`.           |
  | Open options `annotsJson`, `fdf`, `beforeRenderPDFDoc`                                             | **No equivalent**                                                                                                                            |                                                                      |
  | `ViewerEvents.openFileSuccess`                                                                     | `loadState.status === 'ready'`                                                                                                               |                                                                      |
  | `pdfui.close()`, `pdfViewer.close()`                                                               | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf)                                                                            |                                                                      |
  | `customs.closeDocBefore`, `customs.closeDocAfter`                                                  | Your own code around `resetPdf()`                                                                                                            | See [Saving and downloading](https://sdk.pdf.net/docs/v1/migration/from-foxit#saving-and-downloading).               |
  | `pdfDoc.getPageCount()`                                                                            | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                            |                                                                      |
  | `docRender.getCurrentPageIndex()`, `ViewerEvents.pageNumberChange`                                 | [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId)                                                                                    | Re-renders with the id of the page in the middle of the viewport.    |
  | `docRender.goToPage(index)`                                                                        | `scrollToPage(pageId)` from [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage)                                                          | No offset within the page.                                           |
  | `pdfDoc.insertPage(index, width, height)`, `insertBlankPages(range, width, height)`                | `createPage(width, height, insertAfterPageId?)` from [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate)                                     | Inserts after a page, not at an index.                               |
  | `pdfDoc.removePage(index)`, `removePages(range)`                                                   | `deletePages(pageIdOrIds)` from [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete)                                                          |                                                                      |
  | `pdfDoc.movePageTo(index, destIndex)`, `movePagesTo(range, destIndex)`                             | `moveBefore` or `moveAfter` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove)                                                             |                                                                      |
  | `pdfDoc.rotatePages(range, rotation)`, `pdfPage.setRotation(rotation)`                             | `setPageRotation(pageIdOrIds, degrees)` from [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation)                                   | Degrees, not the `Rotation` enum.                                    |
  | `extractPages()` then `insertPages()` to copy a page                                               | `duplicatePage(sourcePageId, insertAfterPageId?)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate)                             |                                                                      |
  | `extractPages()` to a new PDF                                                                      | `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                              | Saves only the listed pages, in the listed order.                    |
  | `insertPages()`, `mergePDFDoc()`, `setPagesBox()`                                                  | **No equivalent**                                                                                                                            |                                                                      |
  | `pdfViewer.rotateTo(degree)` (view only)                                                           | **No equivalent**                                                                                                                            | `useSetPageRotation` edits the document.                             |
  | `pdfViewer.zoomTo(scale)`, `docRender.getScale()`                                                  | [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale)                                                                                    |                                                                      |
  | `pdfViewer.zoomTo('fitWidth')`, `zoomTo('fitHeight')`                                              | [`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.                                    |
  | `ViewerEvents.zoomToSuccess`                                                                       | `scale` from `useViewportScale` in a `useEffect`                                                                                             | Updates once a zoom gesture or animation ends.                       |
  | `defaultViewMode`, view mode add-ons                                                               | **No equivalent**                                                                                                                            | One scrollable layout.                                               |
  | Thumbnail add-on, `pdfDoc.loadThumbnail()`                                                         | [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas)                                                                                                |                                                                      |
  | `pdfDoc.getPDFForm()`, `await form.getField(name)`                                                 | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets) filtered by `fieldName`                                                                        | Widgets, not fields.                                                 |
  | `await field.getValue()`, `field.setValue(value)`                                                  | [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                                                                                        | Inside a component.                                                  |
  | `FormFillerService.onDataChanged()`                                                                | `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent)                                                                 | Reports values the user changes in the main view.                    |
  | The other `FormFillerService` listeners and interceptors                                           | **No equivalent**                                                                                                                            |                                                                      |
  | `form.createTextField({ pdfRect, pageIndex })` and the other `create*` methods                     | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate)                                                        |                                                                      |
  | Form designer add-on, `STATE_HANDLER_CREATE_FIELD_*` state handlers                                | The `{ type: 'addWidget', widgetProps }` interaction tool                                                                                    |                                                                      |
  | `page.removeAnnotByObjectNumber()` on a widget                                                     | [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) with widget ids                                                                        |                                                                      |
  | `form.exportToFile()`, `importFromFile()`, `resetForm()`                                           | **No equivalent**                                                                                                                            |                                                                      |
  | `page.addAnnot(json)` for ink                                                                      | [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate)                                                                        | See [Annotations](https://sdk.pdf.net/docs/v1/migration/from-foxit#annotations).                                     |
  | `page.addAnnot(json)` for other types, annotation state handlers                                   | **No equivalent**                                                                                                                            | See [Annotations](https://sdk.pdf.net/docs/v1/migration/from-foxit#annotations).                                     |
  | `pdfDoc.exportAnnotsToJSON()`, `exportAnnotsToFDF()`, `importAnnotsFromJSON()`                     | **No equivalent**                                                                                                                            |                                                                      |
  | `getStateHandlerManager().switchTo(name)`                                                          | [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode)                                                                          | See [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-foxit#editing-and-interaction-tools). |
  | `customs.getDocPermissions: () => 0`                                                               | `PDF_VIEWER` or `PDF_PRINTER` from [`CONTENT_POLICY_PRESET`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET)                                       |                                                                      |
  | Appearances, layout templates, fragments, `getComponentByName()`                                   | Your own components and the `theme` configuration, see [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization)                |                                                                      |
  | `UndoRedoAddon` `undo()`, `redo()`                                                                 | [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo)                                                                                              |                                                                      |
  | `UndoRedoAddon` `undoAll()`, `invoke()`                                                            | **No equivalent**                                                                                                                            |                                                                      |
  | `pdfDoc.isDocModified()`                                                                           | [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision), [`useDocumentRevisionGetter`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevisionGetter) | Compares against the revision you saved.                             |
  | `pdfDoc.getFile({ fileName })`                                                                     | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                         | Resolves with a `Uint8Array`, not a `File`.                          |
  | Download button of the built-in UI                                                                 | `downloadDocument()`                                                                                                                         | No file name option.                                                 |
  | `getFile()` option `flags`, `getStream()`                                                          | **No equivalent**                                                                                                                            |                                                                      |
  | Rejected `openPDFByFile`, `ViewerEvents.openFileFailed`                                            | The rejected `loadPdf` promise                                                                                                               | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-foxit#errors-and-logging).                       |
  | `Log.LoggerFactory.toggleLogger()`, `setLogLevel()`                                                | [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages)                                                                                              | The SDK never prints to the console.                                 |
  | `pdfui.destroy()`                                                                                  | Nothing to call                                                                                                                              | See [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-foxit#cleanup-and-lifecycle).                 |
  | `pdfPage.getText()`, `pdfDoc.getText(range)`                                                       | `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)                                       | Paragraph text and bounds.                                           |
  | `pdfDoc.getTextSearch()`, `searchText()`                                                           | [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch)                                                                                  | Highlights the matches and scrolls to the active one.                |
  | `pdfui.getSelectedTextInfo()`, `pdfPage.getCharInfoByIndex()`                                      | **No equivalent**                                                                                                                            | `useSelectionTextStyle` reads the selection's style.                 |
  | `pdfDoc.applyRedaction()`, `pdfDoc.sign()`, `pdfui.registerSignHandler()`, the stamp state handler | **No equivalent**                                                                                                                            |                                                                      |
  | Form JavaScript, XFA forms, `pdfDoc.autoRecognitionForm()`                                         | **No equivalent**                                                                                                                            |                                                                      |
  | `pdfViewer.print()`, `printCurrentView()`, `takeSnapshot()`                                        | **No equivalent**                                                                                                                            |                                                                      |
  | `pdfDoc.addHeaderFooter()`, `addWatermark()`, `getPageLabels()`, `getLayerNodesJson()`             | **No equivalent**                                                                                                                            |                                                                      |
  | `pdfViewer.getBookmarkDataService()`, `compareDocuments()`, `getReadAloudService()`, 3D content    | **No equivalent**                                                                                                                            |                                                                      |
  | `pdfViewer.zoomTo(scale, position)`                                                                | `setScale(scale, pivot)` from `useViewportScale`                                                                                             | The pivot is a viewport point, not a point on the page.              |
</details>

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

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web]

Foxit PDF SDK for Web installs as `@foxitsoftware/foxit-pdf-sdk-for-web-library`. The build copies the package's `lib` folder into the app's static files, and the server sends a `Service-Worker-Allowed` header, which the SDK needs since version 10.0:

```sh
npm install @foxitsoftware/foxit-pdf-sdk-for-web-library
```

```js
// webpack.config.js
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  plugins: [
    new CopyWebpackPlugin({
      patterns: [
        {
          // Engine modules, workers, add-ons, locales, and stamps, served at /foxit-lib.
          from: path.resolve(
            'node_modules/@foxitsoftware/foxit-pdf-sdk-for-web-library/lib'
          ),
          to: 'foxit-lib',
          force: true,
        },
      ],
    }),
  ],
  devServer: {
    headers: { 'Service-Worker-Allowed': '/' },
  },
}
```

A React component creates the `PDFUI` in an effect, with the license from `license-key.js`, and destroys it on unmount:

```jsx
import { useEffect, useRef } from 'react'
import * as UIExtension from '@foxitsoftware/foxit-pdf-sdk-for-web-library/lib/UIExtension.full.js'
import '@foxitsoftware/foxit-pdf-sdk-for-web-library/lib/UIExtension.css'
import { licenseKey, licenseSN } from './license-key'

function PDFViewer() {
  const elementRef = useRef(null)

  useEffect(() => {
    const pdfui = new UIExtension.PDFUI({
      viewerOptions: {
        libPath: '/foxit-lib',
        jr: { licenseSN, licenseKey },
      },
      renderTo: elementRef.current,
      appearance: UIExtension.appearances.adaptive,
      addons: UIExtension.PDFViewCtrl.DeviceInfo.isMobile
        ? '/foxit-lib/uix-addons/allInOne.mobile.js'
        : '/foxit-lib/uix-addons/allInOne.js',
    })

    return () => {
      // Asynchronous: closes the document and removes the UI's DOM.
      pdfui.destroy()
    }
  }, [])

  return <div ref={elementRef} style={{ height: '100vh' }} />
}
```

Integrations that bring their own UI create the viewer layer alone: `new PDFViewCtrl.PDFViewer({ libPath, jr })`, `pdfViewer.init(element)`, and `PDFViewCtrl.css`. Under React Strict Mode the effect runs twice in development, and because `destroy()` returns a promise, the second `PDFUI` can mount before the first is gone; Foxit integrations keep the instance in a ref or turn Strict Mode off.

### 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) where the viewer element was. Its props must keep their identity between renders, so define static values outside the component and memoize the rest. `CanEditContext` carries your app's own permission to the editing controls in the rest of this guide:

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

// Your app's permission, in place of customs.getDocPermissions: editing controls render
// only when it's true.
export const CanEditContext = createContext(false)

const readerTool: InteractionTool = { type: 'textSelect' }
const editorTool: InteractionTool = { type: 'select' }

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

  return (
    <CanEditContext.Provider value={canEdit}>
      <PdfProviders {...config}>
        <DocumentLoader url="/api/files/contract.pdf" />
      </PdfProviders>
    </CanEditContext.Provider>
  )
}
```

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

* **Nothing to serve or license in code.** The install is the registry scope and the package: there is no `lib` folder to copy, no `libPath`, `jr` engine paths, or `fontPath`, no `licenseSN` or `licenseKey`, and no add-on list.
* **No mount effect.** The SDK renders as components, so the element ref, the effect that creates `PDFUI` or `PDFViewer`, the Strict Mode guard, and the `destroy()` call go away. Any component under `PdfProviders` reaches the document through hooks instead of `getPDFViewer()`.
* **Choose the starting permissions.** `customs.getDocPermissions` defaults to `-1`, all permissions, and `PdfProviders` defaults to the `select` tool and `PDF_EDITOR`, where text, annotations, links, and widgets are all editable. For users who may not edit, where your app returned `0`, start with `PDF_VIEWER` (forms stay fillable) or `PDF_PRINTER` (forms read-only too) and the `textSelect` tool, as above, and leave the editing controls out: the content policy is a mode your UI can switch with `useSetInteractionMode`, as in [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-foxit#editing-and-interaction-tools), not a permission.

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

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-1]

`openPDFByFile(file, options)` opens a `File`, `Blob`, `ArrayBuffer`, `TypedArray`, or `DataView`; `openPDFByHttpRangeRequest` loads a remote file on demand through HTTP range requests. Both exist on `PDFUI` and `PDFViewer`, and both close the current document first:

```js
await pdfui.openPDFByFile(file, {
  fileName: file.name,
  password: enteredPassword,
})

// Large remote files, read in ranges; the server must support Range requests and CORS.
await pdfui.openPDFByHttpRangeRequest(
  {
    range: {
      url: '/api/files/contract.pdf',
      headers: { Authorization: `Bearer ${token}` },
    },
  },
  { fileName: 'contract.pdf' }
)

// A closeDocBefore callback that returns false cancels both an explicit close and opening
// another file.
await pdfui.close()
```

The open options also take `annotsJson` to import annotations while opening, `fdf` to import an FDF file of form data (`fdf.type` `1`) or annotations (`2`), `beforeRenderPDFDoc` to prepare the document before the first render, and `isRenderOnDocLoaded`. With the complete viewer, an encrypted document brings up the built-in password dialog.

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

`loadPdf` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) takes the file bytes, and the load state decides what to render. A URL becomes your own `fetch`, which also takes the headers you passed in `range.headers`:

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

function DocumentLoader({ url }: { url: string }) {
  const [loadState, loadPdf] = useLoadPdf()
  const resetPdf = useResetPdf()
  const bytesRef = useRef<Uint8Array | null>(null)
  const openedUrlRef = useRef(url)
  const [isAskingForPassword, setIsAskingForPassword] = useState(false)
  const [loadError, setLoadError] = useState<unknown>()

  // Like opening another file in Foxit: a new URL closes the current document. Skips the
  // first mount.
  useEffect(() => {
    if (openedUrlRef.current === url) return
    openedUrlRef.current = url
    bytesRef.current = null
    setIsAskingForPassword(false)
    setLoadError(undefined)
    resetPdf()
    // resetPdf stays out of the dependencies: only a URL change closes the document.
  }, [url])

  async function open(password?: string) {
    setLoadError(undefined)
    try {
      if (!bytesRef.current) {
        // The whole file: there is no range loading. Add your request headers here.
        const response = await fetch(url, { credentials: 'include' })
        if (!response.ok)
          throw new Error(`Download failed with status ${response.status}`)
        const bytes = new Uint8Array(await response.arrayBuffer())
        // The URL changed during the download: drop this response.
        if (openedUrlRef.current !== url) return
        bytesRef.current = bytes
      }

      // loadPdf reads from the buffer it receives, so keep the original for a retry and
      // pass a copy.
      await loadPdf(bytesRef.current.slice(), { password })
      setIsAskingForPassword(false)
    } catch (error) {
      // Replaces the built-in password dialog.
      if (error instanceof InvalidPasswordError) setIsAskingForPassword(true)
      else setLoadError(error)
    }
  }

  if (loadError !== undefined)
    return <LoadErrorMessage error={loadError} onRetry={() => void open()} />
  if (isAskingForPassword)
    return <PasswordDialog onSubmit={(password: string) => void open(password)} />
  if (loadState.status === 'initial')
    return <button onClick={() => void open()}>Open</button>
  if (loadState.status === 'loading') return <p>Loading the document…</p>

  return <PdfApplication />
}
```

For a file input, pass `new Uint8Array(await file.arrayBuffer())`. `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) closes the document, and the load state returns to `'initial'`.

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

* **Bytes, not files or URLs.** `loadPdf` takes a `Uint8Array`. `openPDFByHttpRangeRequest` and its on-demand loading have no equivalent: fetch the whole file, with the headers and credentials your server needs, and pass the bytes.
* **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 })`.
* **Passwords are a rejection, not a dialog.** Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`. The prompt is your component.
* **No import on open.** `annotsJson`, `fdf`, and `beforeRenderPDFDoc` have no equivalent, and neither does importing annotation or form data later.
* **No close hooks.** `customs.closeDocBefore` and `closeDocAfter` have no counterpart: `resetPdf()` closes at once, so check for unsaved changes before calling it, as the [Saving and downloading](https://sdk.pdf.net/docs/v1/migration/from-foxit#saving-and-downloading) example does. To open another document, check the same way, then call `resetPdf()` and `loadPdf`.
* **Closing 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: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-2]

The complete viewer renders the document inside its appearance: `RibbonAppearance` on desktop, `MobileAppearance` on phones, or `AdaptiveAppearance`, which picks one by device. Integrations reshape it with fragments and hide components at runtime:

```js
const FRAGMENT_ACTION = UIExtension.UIConsts.FRAGMENT_ACTION

const pdfui = new UIExtension.PDFUI({
  viewerOptions: { libPath: '/foxit-lib', jr: { licenseSN, licenseKey } },
  renderTo: elementRef.current,
  appearance: UIExtension.appearances.adaptive.extend({
    getDefaultFragments() {
      // Remove the Delete item from the thumbnail context menu.
      return [
        { target: 'contextmenu-item-thumbnail-delete', action: FRAGMENT_ACTION.REMOVE },
      ]
    },
  }),
  addons: '/foxit-lib/uix-addons/allInOne.js',
})

// Hide a component and keep the SDK from showing it again.
const applyRedactionItem = await pdfui.getComponentByName('fv--contextmenu-item-apply')
applyRedactionItem.hide()
applyRedactionItem.keepState()
```

With `PDFViewCtrl` alone, `pdfViewer.init(element)` renders the pages into your element, styled by `PDFViewCtrl.css`.

### 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 thumbnail 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: 'pdf-viewport', page: 'pdf-page' },
  styles: {
    scrollViewport: {
      '--widget-overlay-background': 'rgba(37, 99, 235, 0.08)',
    } as React.CSSProperties,
  },
}

function PdfApplication() {
  // Foxit opens documents at fit width on desktop; this fits once per loaded document.
  useViewportAutoFitScale('width')

  return (
    <div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: '100vh' }}>
      {/* Your ribbon: the navigation, zoom, tool, and history controls from this
         guide. */}
      <Toolbar />
      <div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', minHeight: 0 }}>
        <ThumbnailsSidebar />
        {/* 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.** Appearances, layout templates, fragments, `getComponentByName`, and `keepState` have no counterpart. Selection, text editing, form filling, and links work inside `Viewport` within the content policy, and zoom gestures are built in; 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. `UIExtension.dark-variable.css` and the theme colors of the complete viewer have no counterpart: dark mode is your stylesheet.
* **Dialogs are yours.** The pieces a custom `viewerUI` (extending `XViewerUI` or `TinyViewerUI`) overrides in Foxit, such as alerts, confirmations, the password prompt, context menus, and the loading overlay, are ordinary React UI in your app.
* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded.
* **One layout.** `defaultViewMode` (`'continuous-view-mode'` or `'single-page-view-mode'`), the facing and horizontal modes of the `h-continuous` add-on, custom `Viewmodes`, and `customs.ScrollWrap` have no equivalent: `Viewport` shows the pages in one scrollable area.

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

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-3]

`PDFDocRender` navigates by zero-based page index, and the `pageNumberChange` viewer event reports the new current page as a one-based number:

```js
const pdfViewer = await pdfui.getPDFViewer()
const { ViewerEvents } = UIExtension.PDFViewCtrl.constants

pdfui.addViewerEventListener(ViewerEvents.openFileSuccess, (pdfDoc) => {
  pageCountLabel.textContent = pdfDoc.getPageCount()
})

pdfui.addViewerEventListener(ViewerEvents.pageNumberChange, (pageNumber) => {
  pageInput.value = pageNumber
})

pageInput.onchange = () => {
  // Page indexes start at 0.
  pdfViewer.getPDFDocRender().goToPage(Number(pageInput.value) - 1)
}
```

### 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) re-renders with the page in the middle of the viewport:

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

function PageNumberInput() {
  const pages = usePages()
  // Replaces getCurrentPageIndex() and the pageNumberChange listener.
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()
  // What the user is typing; undefined shows the current page.
  const [draft, setDraft] = useState<string>()

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

  function goToDraft() {
    const page = pages[Number(draft) - 1]
    if (page) scrollToPage(page.id, { behavior: 'smooth' })
    setDraft(undefined)
  }

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        goToDraft()
      }}
    >
      <label>
        Page
        <input
          inputMode="numeric"
          value={draft ?? String(currentIndex + 1)}
          onChange={(event) => setDraft(event.currentTarget.value)}
          onBlur={goToDraft}
        />
        of {pages.length}
      </label>
    </form>
  )
}
```

When a handler needs a page's position, 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; indexes don't.** An id keeps pointing at the same page while pages are added, removed, and rearranged by the user, where an index would drift to another page. Ids are session-scoped: persist page numbers, not ids, and resolve them with `usePages` once the document is ready.
* **Scroll options.** `scrollToPage` aligns the page with `vertical` (`'top'` by default) and `horizontal` (`'nearest'` by default), and `behavior` sets the animation. The `offset` and `isPDFPoint` arguments of `goToPage`, which jump to a point on the page, have no equivalent; `scrollToBlock(blockId, options?)` from [`useScrollToBlock`](https://sdk.pdf.net/docs/v1/reference/useScrollToBlock) scrolls to a block, such as a form widget.
* **The page count.** Where you read `getPageCount()` once after `openFileSuccess`, [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount) re-renders when pages are added or removed, and returns `0` while no document is loaded.

## Page manipulation [#page-manipulation]

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-4]

`PDFDoc` changes pages by index. Most methods take two-dimensional page ranges, where `[[0], [2, 4]]` means page 1 and pages 3 to 5, and sizes are PDF points:

```js
const pdfDoc = pdfViewer.getCurrentPDFDoc()

// An A4 page before page 1.
await pdfDoc.insertPage(0, 595, 842)
await pdfDoc.removePages([[1, 3]])
await pdfDoc.movePagesTo([[0]], 2)
// Sets the rotation; the Rotation enum runs from 0 to 3 for 0° to 270°.
await pdfDoc.rotatePages([[0, 2]], 1)

// Copying a page means extracting it and inserting the extracted PDF.
const extracted = await pdfDoc.extractPages([[0]])
await pdfDoc.insertPages({
  destIndex: 1,
  file: new Blob(extracted, { type: 'application/pdf' }),
  password: '',
  startIndex: 0,
  endIndex: 0,
})
```

### 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). These buttons replace the thumbnail context menu:

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

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

  const page = pages.find((page) => page.id === pageId)
  // Page operations are editing: users who may not edit don't get the menu.
  if (!page || !canEdit) return null

  return (
    <div role="menu">
      {/* The rotation is absolute: add to the current angle. -90 is normalized to
         270. */}
      <button role="menuitem" onClick={() => setPageRotation(pageId, page.rotation - 90)}>
        Rotate left
      </button>
      <button role="menuitem" onClick={() => setPageRotation(pageId, page.rotation + 90)}>
        Rotate right
      </button>
      {/* A4 is roughly 595 × 842 user-space units; the new page goes after this one. */}
      <button role="menuitem" onClick={() => createPage(595, 842, pageId)}>
        Insert blank page
      </button>
      <button role="menuitem" onClick={() => void duplicatePage(pageId)}>
        Duplicate
      </button>
      {/* Without a target page, moveAfter moves the page to the end. */}
      <button role="menuitem" onClick={() => moveAfter(pageId)}>
        Move to end
      </button>
      <button
        role="menuitem"
        disabled={pages.length <= 1}
        onClick={() => deletePages(pageId)}
      >
        Delete page
      </button>
    </div>
  )
}
```

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

| Operation | Foxit PDF SDK for Web                                             | @sdk-pdf/react                                                                                                         |
| --------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Insert    | At a zero-based index (`insertPage`) or at page ranges, in points | After `insertAfterPageId`, or appended; `width` and `height` in user-space units (1/72 of an inch), no defaults        |
| Move      | To a destination index (`movePageTo`, `movePagesTo`)              | `moveBefore(ids, beforePageId?)` (start when omitted) or `moveAfter(ids, afterPageId?)` (end when omitted)             |
| Rotate    | Sets a `Rotation` enum value, `0` to `3`                          | Sets an absolute angle in degrees; `-90` and `450` are normalized; angles off a quarter turn throw `InvalidInputError` |
| Copy      | `extractPages` and `insertPages`                                  | `duplicatePage` is asynchronous and inserts after the source by default                                                |
| Delete    | A page index (`removePage`) or page ranges                        | One id or an array of ids                                                                                              |

* **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 operations your code calls emit no events: track them where you call the commands. `usePages` re-renders with the new page list, and [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) changes on any edit.
* **Extracting pages.** `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) saves only the listed pages, in the listed order, in place of `extractPages` to a new PDF.
* **Pages from other files, page boxes.** `insertPages` from another PDF, `mergePDFDoc`, `setPagesBox` cropping and resizing, and dropping a PDF on the thumbnails to merge it have no equivalent.

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

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-5]

`zoomTo` takes a number or a fit mode, `'fitWidth'` or `'fitHeight'` (which fits the whole page), within the `minScale` and `maxScale` constructor options (`0.01` and `10` by default). Documents open at `defaultScale`, `'fitWidth'` by default:

```js
const pdfViewer = await pdfui.getPDFViewer()

await pdfViewer.zoomTo('fitWidth')
await pdfViewer.zoomTo(1.25)

// A number, or the active fit mode.
const scale = await pdfViewer.getPDFDocRender().getScale()

pdfui.addViewerEventListener(
  UIExtension.PDFViewCtrl.constants.ViewerEvents.zoomToSuccess,
  (newScale) => {
    saveZoomPreference(newScale)
  }
)
```

### 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'

const zoomSteps = [0.5, 0.75, 1, 1.25, 1.5, 2, 4]

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])

  function fitPage() {
    const { fitWidthScale, fitHeightScale } = getFitScale()
    // The smaller scale fits whole pages, like zoomTo('fitHeight').
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

  return (
    <div role="toolbar">
      <select
        value={scale}
        onChange={(event) => setScale(Number(event.currentTarget.value))}
      >
        {!zoomSteps.includes(scale) && (
          <option value={scale}>{Math.round(scale * 100)}%</option>
        )}
        {zoomSteps.map((step) => (
          <option key={step} value={step}>
            {step * 100}%
          </option>
        ))}
      </select>
      {/* 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={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, and no `'fitWidth'` value to read back.
* **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` and `animateScale`, the third item of `useViewportScale()`, take a positive finite number and throw `InvalidInputError` otherwise.
* **Zoom around a point.** In place of the `position` argument of `zoomTo`, `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. `animateScale(scale, pivot?)` animates the change and resolves when it ends.
* **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.
* **Gestures and limits.** Pinch and wheel zoom are built in and tuned with `zoomGestures` on `PdfProviders`: its `minScale` and `maxScale` (`0.5` and `4` by default) bound the gestures.
* **No redraw call.** Foxit integrations call `redraw()` after the window resizes; `Viewport` fills its parent element and has no redraw API, so give the parent an explicit size.

## Thumbnails [#thumbnails]

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-6]

The thumbnail panel comes from the `thumbnail` add-on: thumbnails reorder by dragging, a PDF dropped on the list is merged in, and a context menu rotates, inserts, and deletes pages. Thumbnails outside the panel come from `loadThumbnail`:

```js
const pdfViewer = await pdfui.getPDFViewer()
const pdfDoc = pdfViewer.getCurrentPDFDoc()

// Fits the page into a 160 × 200 box and resolves with a canvas.
const canvas = await pdfDoc.loadThumbnail({
  pageIndex: 0,
  width: 160,
  height: 200,
  type: 'canvas',
})
thumbnailContainer.appendChild(canvas)
```

### 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. Drag-to-reorder becomes `moveBefore` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove):

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

function ThumbnailsSidebar() {
  const canEdit = useContext(CanEditContext)
  const pages = usePages()
  const scrollToPage = useScrollToPage()
  const { moveBefore } = usePageMove()
  const [draggedPageId, setDraggedPageId] = useState<string>()

  return (
    <nav aria-label="Pages">
      {pages.map((page, index) => (
        <button
          key={page.id}
          // Reordering is editing: only users who may edit can drag.
          draggable={canEdit}
          onDragStart={(event) => {
            event.dataTransfer.setData('text/plain', page.id)
            setDraggedPageId(page.id)
          }}
          onDragOver={(event) => {
            // Accept only thumbnails from this list, not files or text.
            if (draggedPageId) event.preventDefault()
          }}
          onDrop={() => {
            // Drop a page before this one, like rearranging in the thumbnail panel.
            if (draggedPageId && draggedPageId !== page.id) {
              try {
                moveBefore(draggedPageId, page.id)
              } catch {
                // One of the pages no longer exists: InvalidInputError, and nothing
                // moved.
              }
            }
            setDraggedPageId(undefined)
          }}
          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={`Page ${index + 1}`}
          />
        </button>
      ))}
    </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 place of `loadThumbnail`'s `scale`, `width`, and `height`. In a virtualized list, pass `isVisible`.
* **Annotations included.** `PageCanvas` draws annotations, form widgets included, unless `hideAnnotations` is set.
* **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.
* **Stale ids.** If the dragged page no longer exists, `moveBefore` throws `InvalidInputError` and changes nothing, so the drop handler catches it.
* **No merging.** Dropping another PDF on the thumbnails has no equivalent.

## Forms [#forms]

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-7]

Since version 11.0, forms are `PDFForm`, `PDFFormField`, and `Widget` objects with asynchronous accessors. Fields are looked up by name, values are strings, and widgets are created in PDF coordinates on a zero-based page:

```js
const pdfViewer = await pdfui.getPDFViewer()
const form = pdfViewer.getCurrentPDFDoc().getPDFForm()

const field = await form.getField('customer.name')
if (field) {
  const currentValue = await field.getValue()
  await field.setValue('Jane Doe')
}

// A date field: the rectangle is left, bottom, right, and top in PDF coordinates.
await form.createDatetime({
  pdfRect: { left: 72, bottom: 700, right: 192, top: 724 },
  pageIndex: 0,
  rotate: 0,
  timeformat: 'yyyy-mm-dd',
})

// Form data as a file: xfdf, fdf, csv, json, text, or xml.
const formData = await form.exportToFile(
  UIExtension.PDFViewCtrl.commons.FileFormat.json,
  {}
)
```

### 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 {
  FormatType,
  TextWidget,
  useWidgets,
  useWidgetsCreate,
  useWidgetValue,
  Widget,
} from '@sdk-pdf/react'

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

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

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

function BoundTextInput({ widget }: { widget: TextWidget }) {
  // Replaces getValue and setValue: 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 useAddDateField() {
  const createWidgets = useWidgetsCreate()

  // Replaces form.createDatetime.
  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: 120,
      height: 24,
      fieldName: 'contract.date',
      format: { type: FormatType.DateTime, format: 'yyyy-mm-dd' },
    })
    return widget
  }
}
```

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

| Concept              | Foxit PDF SDK for Web                                                                  | @sdk-pdf/react                                                                                                                                                                                                       |
| -------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lookup               | `getField(name)`, `getFields()`, `getFieldsByNamePrefix()`                             | `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.getValue()`, `setValue()`, strings                                              | `useWidgetValue(widget)`: `string` for `text` and `choice`, `boolean` for `checkbox` and `radio`, option indices for `multiChoice`                                                                                   |
| Creation             | `createTextField`, `createCheckbox`, `createDatetime`, and the other `create*` methods | `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 | Design mode of the form designer add-on, `STATE_HANDLER_CREATE_FIELD_*`                | The `{ type: 'addWidget', widgetProps }` interaction tool, with editable widgets in the content policy                                                                                                               |
| Deletion             | `page.removeAnnotByObjectNumber()`, which removes the field with its last widget       | [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) with widget ids; same-named widgets act as one field, and deleting a widget doesn't erase the stored field value                                               |
| Properties           | `PDFFormField` setters and the form designer's properties dialog                       | `setWidgetProps(ids, props, options?)` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps): `isReadOnly`, `borderColor`, `fillColor`, `fontSize`, `isMultiline`, `options`, and more                    |
| Interaction events   | `FormFillerService` listeners and interceptors                                         | `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) for values the user changes, [`useFocusedWidgetId`](https://sdk.pdf.net/docs/v1/reference/useFocusedWidgetId) for the widget being filled; no interceptors |
| Form data            | `exportToFile`, `importFromFile`, `resetForm`                                          | No equivalent                                                                                                                                                                                                        |

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

* **Coordinates differ in shape.** A Foxit `pdfRect` gives `left`, `bottom`, `right`, and `top` in PDF coordinates; a widget here takes `x`, `y`, `width`, and `height` in user space, with the origin at the bottom-left corner of the page. 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, call `setValue` from a component bound to one widget of each field, like `NamedTextField` above. Widgets with the same field name share the value, so one per field is enough.
* **Read-only fields.** `isReadOnly` restricts filling the field, not authoring the widget; pass `{ excludeFromHistory: true }` to `setWidgetProps` so an undo can't unlock a submitted form. `setWidgetProps` applies the change at once and returns a promise that resolves when the widgets show it.
* **Field events.** `widgetInput` reports only values the user changes in the main view: values set from code, undo, and redo emit nothing, and `useWidgetValue` re-renders on all of them.
* **Date fields and scripts.** Foxit runs form JavaScript for calculation, validation, and formatting. `@sdk-pdf/react` 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). Date fields driven by hand-written scripts aren't recognized and read as plain text fields. Calculation and validation scripts have no equivalent.
* **Signatures.** `signature` widgets carry a read-only `isCryptoSigned`; signing and verification (`registerSignHandler`, `PDFDoc.sign`, `SignatureWorkflowService`) have no equivalent, and the SDK never signs.

## Annotations [#annotations]

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-8]

Annotations are first-class in Foxit: the complete viewer creates them with its tools, and `PDFPage` reads, adds, and removes them in PDF coordinates. Annotation data travels as JSON, FDF, or XFDF:

```js
const pdfDoc = pdfViewer.getCurrentPDFDoc()
const page = await pdfDoc.getPageByIndex(0)

const annots = await page.getAnnots()

// addAnnot may also add an associated popup, so it resolves with an array.
const [square] = await page.addAnnot({
  type: 'square',
  rect: { left: 100, bottom: 500, right: 200, top: 550 },
  color: 0xff0000,
})
await page.removeAnnotById(square.getUniqueID())

// null exports every annotation.
const annotsJson = await pdfDoc.exportAnnotsToJSON(null)
```

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

Existing annotations display in `Viewport` and `PageCanvas`, and the border color, width, and opacity (the color's alpha channel) of selected shapes, lines, ink, and widgets can be changed with [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps). Images are placed as stamp annotations with the `addImage` tool, as in [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-foxit#editing-and-interaction-tools), or from code with [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate), and ink annotations are created from code with [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate):

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

function AnnotationBorderControls() {
  const { isApplicable, borderColor, borderWidth } = useSelectionAnnotationProps()

  // The selection holds no annotation with a border.
  if (!isApplicable) return null

  return (
    <div role="toolbar">
      <input
        type="color"
        value={borderColor.value ?? '#000000'}
        onChange={(event) => borderColor.set(event.currentTarget.value)}
      />
      <input
        type="number"
        min={0}
        // An empty value means the selected annotations have different widths.
        value={borderWidth.value ?? ''}
        onChange={(event) => borderWidth.set(Number(event.currentTarget.value))}
      />
    </div>
  )
}
```

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

* **Ink from code only.** `createInkAnnotation({ pageId, x, y, width, height, inkList, borderColor, borderWidth, lineCap, blendMode })` from `useInkAnnotationCreate` adds an ink annotation from strokes your code collects; there is no drawing tool like `STATE_HANDLER_CREATE_PENCIL`. Each stroke in `inkList` is a flat `[x1, y1, x2, y2, …]` array of fractions of the box given by `x`, `y`, `width`, and `height` in user space.
* **No other annotation authoring.** Creating notes, free text, text markup, shapes, links, file attachments, measurements, and redaction marks has no equivalent, and neither does `addAnnot` for them, setting an annotation's content, or the comment panel with replies and review states.
* **No annotation data exchange.** `exportAnnotsToJSON`, `exportAnnotsToFDF`, the import counterparts, and flattening have no equivalent.
* **Per-kind permissions.** `AnnotationAuthorityManager` and `customs.getAnnotPermissions` set permissions per annotation; a content policy sets an `annotations` level, `'hidden'`, `'readonly'`, or `'editable'`, for all of them, or per annotation with a function of the block id, such as `annotations: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'editable')`.

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

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-9]

The active tool is a state handler, switched by name through the `StateHandlerManager`. Read-only viewing comes from the permission callback, and content editing is Std Edit or the separately licensed Adv Edit, both driven from the complete viewer's Edit tab:

```js
const { STATE_HANDLER_NAMES } = UIExtension.PDFViewCtrl.constants
const stateHandlerManager = await pdfui.getStateHandlerManager()

stateHandlerManager.switchTo(STATE_HANDLER_NAMES.STATE_HANDLER_HAND)
stateHandlerManager.switchTo(STATE_HANDLER_NAMES.STATE_HANDLER_SELECT_ANNOTATION)
stateHandlerManager.switchTo(STATE_HANDLER_NAMES.STATE_HANDLER_CREATE_STAMP, {
  category: 'Static',
  name: 'Void',
})

// Read-only viewing: 0 means read-only, and the default -1 grants all permissions.
const readOnlyViewer = new UIExtension.PDFUI({
  viewerOptions: {
    libPath: '/foxit-lib',
    jr: { licenseSN, licenseKey },
    customs: { getDocPermissions: () => 0 },
  },
  renderTo: elementRef.current,
})
```

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

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, and [`useInteractionTool`](https://sdk.pdf.net/docs/v1/reference/useInteractionTool) reads the tool back:

```tsx
import { useContext } from 'react'
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 ToolGroup() {
  const canEdit = useContext(CanEditContext)
  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">
      {/* Like getDocPermissions returning 0, with fillable forms and links. */}
      <button
        aria-pressed={interactionTool.type === 'textSelect'}
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_VIEWER)
        }
      >
        Select text
      </button>
      {/* The editing tools switch to PDF_EDITOR, so only users who may edit get them. */}
      {canEdit && (
        <>
          <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/*"
            aria-label="Add image"
            onChange={(event) => {
              const file = event.currentTarget.files?.[0]
              if (file) placeImage(file).catch(showError)
            }}
          />
        </>
      )}
    </div>
  )
}
```

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

| `STATE_HANDLER_NAMES` and modes                                                                        | @sdk-pdf/react                                                                                                                                                                                                                                                                                          |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `STATE_HANDLER_HAND`                                                                                   | No tool: `Viewport` scrolls and zooms on its own.                                                                                                                                                                                                                                                       |
| `STATE_HANDLER_SELECT_TEXT_IMAGE`                                                                      | `{ type: 'textSelect' }`: native text selection for reading and copying, with read-only text.                                                                                                                                                                                                           |
| `STATE_HANDLER_SELECT_ANNOTATION`                                                                      | `{ type: 'select' }`: select, move, and resize blocks.                                                                                                                                                                                                                                                  |
| Std Edit and Adv Edit text editing                                                                     | Text is editable in place when the policy's `text` level is `'editable'` (`PDF_EDITOR`); there is no separate mode or add-on.                                                                                                                                                                           |
| Add text in the Edit tab                                                                               | `{ type: 'addText' }`: creates a paragraph at the click position and focuses it for typing, with `PDF_EDITOR`.                                                                                                                                                                                          |
| `STATE_HANDLER_CREATE_IMAGE`, `STATE_HANDLER_CREATE_STAMP` with an image                               | `{ type: 'addImage', image, width?, height? }` with `await stageImage(blob)` places a stamp annotation; see [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage). 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. |
| `STATE_HANDLER_CREATE_FIELD_TEXT` and the other field state handlers                                   | `{ type: 'addWidget', widgetProps }`, with `PDF_EDITOR`.                                                                                                                                                                                                                                                |
| `STATE_HANDLER_CREATE_FREETEXT_TYPEWRITER`, `STATE_HANDLER_CREATE_FREETEXT_BOX`                        | The nearest tool is `addText`, which creates a text paragraph.                                                                                                                                                                                                                                          |
| `STATE_HANDLER_CREATE_PENCIL`                                                                          | No drawing tool. [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from strokes your code collects.                                                                                                                                                       |
| Text markup, shapes, notes, links, measurements, `STATE_HANDLER_ERASER`, `STATE_HANDLER_SNAPSHOT_TOOL` | No equivalent.                                                                                                                                                                                                                                                                                          |
| `customs.getDocPermissions` returning `0`                                                              | `CONTENT_POLICY_PRESET.PDF_VIEWER` (forms stay fillable) or `PDF_PRINTER` (forms read-only too).                                                                                                                                                                                                        |

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

* **Switch the policy with the tool.** Pass `PDF_EDITOR` with the editing tools, as above. `textSelect` needs read-only text: with editable text, the combination is unsupported and reports a message through [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages).
* **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, in place of `User_Permissions` bits. `'hidden'` doesn't render that kind of content at all, and a level can be a function of the block id to set it per block, such as `widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive')`. There is no hook that reads the policy back: keep the policy you set in your own state to show or hide editing UI.
* **Gate the mode switcher.** The content policy is the current mode, not what the user may do: render controls that switch to `PDF_EDITOR` only when your app's own permission allows it, as `canEdit` does in these examples.
* **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) replaces the font and paragraph controls of the Edit tab, and for the form properties dialog, read the selected widgets with [`useSelectedBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectedBlocks) and change them with `setWidgetProps(ids, { isMultiline, format })` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps). Return the focus to the text with [`useFocusSelection`](https://sdk.pdf.net/docs/v1/reference/useFocusSelection) after a toolbar click.
* **Keyboard shortcuts are your code.** Foxit's built-in shortcuts and `onShortcutKey` move to your app: undo, redo, and Delete become a [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) mapping, memoized so the listener doesn't re-install on every render, as in the next section. Paste and print have no equivalent, and search is [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch) behind your own search bar.
* **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: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-10]

The undo-redo add-on undoes annotation, bookmark, and Adv Edit changes, binds Ctrl+Z and Ctrl+Y, and records changes made through the wrapped document of `invoke`. `isDocModified()` reports unsaved changes:

```js
const undoRedoAddon = await pdfui.getAddonInstance('UndoRedoAddon')

undoButton.onclick = () => undoRedoAddon.undo()
redoButton.onclick = () => undoRedoAddon.redo()
revertButton.onclick = () => undoRedoAddon.undoAll()

// Changes made through the wrapped pdfDoc enter the undo stack.
await undoRedoAddon.invoke(async (pdfDoc) => {
  const page = await pdfDoc.getPageByIndex(0)
  await page.addAnnot({
    type: 'square',
    rect: { left: 0, bottom: 500, right: 100, top: 550 },
  })
})

const hasUnsavedChanges = pdfViewer.getCurrentPDFDoc().isDocModified()
```

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

[`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo) returns the commands and their availability, and [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) binds the shortcuts the add-on bound for you:

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

function HistoryControls() {
  const canEdit = useContext(CanEditContext)
  // canUndo and canRedo re-render the component as the history changes.
  const { undo, redo, canUndo, canRedo } = useUndoRedo()
  const deleteSelection = useDeleteSelection()

  // Foxit's Ctrl+Z, Ctrl+Y, and Delete; Mod is Cmd on macOS. The mapping must be
  // memoized.
  useHotkeys(
    useMemo(
      () => ({
        'Mod-z': undo,
        'Mod-y': redo,
        'Shift-Mod-z': redo,
        // Deleting is editing: bind Delete only for users who may edit.
        // createUnfocusedHandler keeps Delete for text while the user types in a
        // paragraph or a field.
        ...(canEdit ? { Delete: createUnfocusedHandler(deleteSelection) } : {}),
      }),
      [undo, redo, deleteSelection, canEdit]
    )
  )

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

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

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

* **Beyond annotations.** The history covers the document: page operations 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.
* **No wrapper.** There is no `invoke`: the history tracks the local user's edits without a wrapped document. Changes applied with the `excludeFromHistory` option of [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps) never enter it.
* **No undo all.** `undoAll()` has no equivalent; to return to the file as loaded, reload the original bytes: `resetPdf()`, then `loadPdf(original.slice())`.
* **Events for user actions only.** In place of Foxit's data events, [`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: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-11]

`getFile()` exports the document, with its annotations, form data, and page edits, as a `File`. Uploading and downloading are your code:

```js
const pdfViewer = await pdfui.getPDFViewer()
const pdfDoc = pdfViewer.getCurrentPDFDoc()

const file = await pdfDoc.getFile({ fileName: 'contract.pdf' })

const body = new FormData()
body.append('file', file, file.name)
await fetch('/api/pdf/save', { method: 'POST', body })

// A download goes through an object URL.
const url = URL.createObjectURL(file)
const link = document.createElement('a')
link.href = url
link.download = file.name
link.click()
URL.revokeObjectURL(url)
```

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

[`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) applies the recorded changes to the original document. One component owns the saved revision, so the save, the unsaved-changes indicator, and the close confirmation that replaces `customs.closeDocBefore` compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's:

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

function DocumentActions() {
  const canEdit = useContext(CanEditContext)
  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()
  const resetPdf = useResetPdf()

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

  async function save() {
    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()
      const bytes = await prepareDocument()

      const body = new FormData()
      body.append(
        'file',
        new Blob([bytes as BlobPart], { type: 'application/pdf' }),
        'contract.pdf'
      )
      const response = await fetch('/api/pdf/save', { method: 'POST', body })
      if (!response.ok) throw new Error(`Save failed with status ${response.status}`)

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

  function close() {
    // Replaces customs.closeDocBefore.
    if (hasUnsavedChanges && !window.confirm('Close without saving?')) return
    resetPdf()
  }

  return (
    <div role="toolbar">
      {canEdit && <button onClick={() => save().catch(showError)}>Save</button>}
      <button onClick={() => void downloadDocument()}>Download</button>
      <button onClick={close}>Close</button>
      {hasUnsavedChanges && <span>Unsaved changes</span>}
    </div>
  )
}
```

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

* **Bytes, not a File.** `prepareDocument()` resolves with the complete PDF as a `Uint8Array`, and `downloadDocument()` saves the same bytes on the user's device; there is no file name option.
* **returnOriginalIfUnchanged** defaults to `true`, returning the buffer passed to `loadPdf` when nothing changed. Wrapping it in a `Blob` copies it, as above; set the option to `false` when the bytes will be transferred or mutated, such as posted to a worker.
* **Save options.** Besides `returnOriginalIfUnchanged`, `pageIds` saves only the listed pages, in the listed order. The `getFile()` `flags` (incremental, linearized, and the others), `progressHandler`, and the chunked `getStream()` have no equivalent, and neither does saving with a password through `setPasswordAndPermission`.
* **Record the saved revision.** Saving doesn't reset any modified state: store the revision each successful save started from, as above.

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

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-12]

A failed open rejects with an object whose `error` code tells failures apart (`3` is a wrong password), and the viewer also emits the `openFileFailed` event. For a password failure, the complete viewer asks for the password itself:

```js
const { ViewerEvents } = UIExtension.PDFViewCtrl.constants
const passwordErrorCode = UIExtension.PDFViewCtrl.PDF.constant.Error_Code.password

pdfui.addViewerEventListener(ViewerEvents.openFileFailed, (failure) => {
  // The password dialog takes over.
  if (failure && failure.error === passwordErrorCode) return
  hideLoadingOverlay()
})

try {
  await pdfui.openPDFByFile(file, { fileName: file.name })
} catch (failure) {
  // Reported once: a password failure goes to the password dialog.
  if (!failure || failure.error !== passwordErrorCode) showError(failure)
}

// Logging: true hides the SDK's log output; setLogLevel takes a Log_Levels value.
UIExtension.PDFViewCtrl.Log.LoggerFactory.toggleLogger(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 useOpenBytes() {
  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 checking failure.error against Error_Code.password.
      if (error instanceof InvalidPasswordError) askForPassword()
      else showError(error)
    }
  }
}
```

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

* **Error classes, not codes.** `DocumentNotLoadedError`, `InvalidInputError`, `InvalidPasswordError`, `DocumentFormatError` (malformed beyond what the SDK can work around), `NotSupportedError` (a PDF feature the SDK doesn't support), and `UninitializedError`, all extending `GenericError` with a static `message` and an optional `data` object. Narrow them with `instanceof`.
* **Nothing half-applied.** Commands validate first and throw before touching anything; readers return empty values instead of throwing; hooks never throw during render.
* **Network errors are yours.** Without range loading, a failed download is an error from your own `fetch`, before `loadPdf` runs.
* **Logs are opt-in.** In place of `toggleLogger` and `setLogLevel`, filter by `level` in your `logMessages` subscriber; the SDK never prints to the console. Each message carries `level`, `reason`, a static `message`, `data`, and `stack`. Error UI, such as the alerts of the complete viewer, is yours.

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

### Before: Foxit PDF SDK for Web [#before-foxit-pdf-sdk-for-web-13]

`destroy()` closes the open document and removes the viewer's DOM, and returns a promise; viewer event listeners return a function that removes them. A React effect undoes both:

```jsx
useEffect(() => {
  const pdfui = new UIExtension.PDFUI({
    viewerOptions: { libPath: '/foxit-lib', jr: { licenseSN, licenseKey } },
    renderTo: elementRef.current,
  })
  const removeOpenListener = pdfui.addViewerEventListener(
    UIExtension.PDFViewCtrl.constants.ViewerEvents.openFileSuccess,
    () => setIsDocumentOpen(true)
  )

  return () => {
    removeOpenListener()
    pdfui.destroy()
  }
}, [])
```

Search objects from `getTextSearch()` are released with their own `destroy()`.

### 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; call it in the effect cleanup, as in [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-foxit#errors-and-logging).
* **Closing a document** is `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf), which returns the load state to `'initial'`. There is no instance to destroy.

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

Foxit reads text with `PDFPage.getText()`, `PDFDoc.getText(range)`, and `getTextInRect`, and searches with `getTextSearch()` and `searchText()`. In `@sdk-pdf/react`, [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) reads the text as paragraphs: `getParagraphsByPage(pageId)` for one page, or every paragraph in page and content order:

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

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

  return async () => {
    // Loads every page in turn: prefer getParagraphsByPage(pageId) when you know the
    // page.
    const paragraphIds = await getAllParagraphIds()
    const paragraphs = await getPlainTextParagraphs(paragraphIds)
    return paragraphs.map((paragraph) => ({
      pageNumber: paragraph.pageNumber,
      text: paragraph.content,
    }))
  }
}
```

Each entry also carries the paragraph box in user space as `bounds` (`{ x, y, width, height }`), and the paragraph layout, such as `textAlign` and `lineHeight`.

[`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch) replaces `getTextSearch()` and `searchText()`: set the query and the options, and the SDK finds the matches, highlights them on the pages, and scrolls to the active one. There is no search object to destroy:

```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.isWholeWord}
          onChange={(event) =>
            setSearchCriteria({ ...criteria, isWholeWord: event.currentTarget.checked })
          }
        />
        Whole words
      </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>
  )
}
```

The options are `isCaseSensitive` and `isWholeWord`. Each match carries `id`, `pageId`, `paragraphId`, and the matched `text`, in document order, with no rectangles; `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. Replacing text has no equivalent.

There is no character-level geometry, and no API returns the selected text or its rectangles (`getSelectedTextInfo`, `TextSelectionTool.getSelectionInfo()`); [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle) reads the selection's style. 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:

| Foxit PDF SDK for Web feature                                                                        | In @sdk-pdf/react                                                                                                                                                                               |
| ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The complete viewer: appearances, layout templates, fragments, add-ons, localization                 | The UI is yours                                                                                                                                                                                 |
| Range loading (`openPDFByHttpRangeRequest`), open options `annotsJson`, `fdf`, `beforeRenderPDFDoc`  | Fetch the whole file; no import                                                                                                                                                                 |
| Find and replace                                                                                     | No equivalent; `useDocumentSearch` finds the text                                                                                                                                               |
| Reading the selected text, character geometry (`getCharInfoByIndex`)                                 | No equivalent; `useProgrammaticEditor` reads paragraph text                                                                                                                                     |
| Creating markup, shape, note, free text, link, and measurement annotations; drawing ink; comments    | No equivalent; existing annotations display, ink is created from code with `useInkAnnotationCreate`, and the border color, width, and opacity of shapes, lines, ink, and widgets can be changed |
| Annotation and form data exchange (JSON, FDF, XFDF, CSV), `resetForm`, flattening                    | No equivalent                                                                                                                                                                                   |
| Redaction, digital signatures, stamps from the stamp library                                         | No equivalent; images place as stamp annotations with `addImage`, and the SDK never signs                                                                                                       |
| Form JavaScript, XFA forms, form recognition                                                         | No equivalent; Acrobat date formats are recognized without scripts                                                                                                                              |
| `FormFillerService` interceptors                                                                     | No equivalent; `widgetInput` from `useViewportEvent` reports value changes after the fact                                                                                                       |
| Inserting pages from another PDF, merging, cropping (`setPagesBox`)                                  | No equivalent; `prepareDocument({ pageIds })` saves a subset of pages                                                                                                                           |
| View-only rotation (`rotateTo`), view modes                                                          | Rotation is a document edit; one scrollable layout                                                                                                                                              |
| Printing (`print`, `printCurrentView`), snapshots                                                    | No print API; `PageCanvas` can render pages for your own print layout, drawn as loaded from the file, without this session's edits                                                              |
| Save flags, `getStream`, passwords and permissions on save (`setPasswordAndPermission`)              | `prepareDocument()` takes `returnOriginalIfUnchanged` and `pageIds` only                                                                                                                        |
| Headers and footers, watermarks, page labels, layers, bookmarks, document comparison, 3D, read aloud | No equivalent                                                                                                                                                                                   |
| `undoAll()` and the `invoke` wrapper of the undo-redo add-on                                         | `useUndoRedo` covers the local user's edits                                                                                                                                                     |

