# Migrating from PDFium

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



This guide moves the viewer and editor of a PDFium integration to `@sdk-pdf/react`. PDFium is the open-source PDF library, with a C API, that Chromium uses. It has no UI, so an integration links it into native code, such as a desktop app or a server, and builds the viewer on top: it renders pages into bitmaps, forwards input to a form-fill environment, extracts text, edits page objects, and saves through a write callback. `@sdk-pdf/react` runs in the browser and ships the viewer and editor as React components and hooks. The guide covers moving the part users see and edit into a React app; your server or native code keeps fetching and storing the bytes, and batch jobs that never show a page can stay on PDFium. 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-pdfium#overview): how the two integrations differ, and the API map for the `FPDF_*`, `FORM_*`, and `FPDFText_*` calls you use.
* [ ] Decide what moves to the browser (viewing, form filling, editing) and what stays with PDFium on your server, such as batch rendering, merging, and flattening.
* [ ] [Install the package and replace library setup with `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-pdfium#install-and-set-up), with a viewer content policy if you ship a reader.
* [ ] [Load and close documents](https://sdk.pdf.net/docs/v1/migration/from-pdfium#load-and-reset-a-document) with `loadPdf` and `resetPdf`, including the password flow.
* [ ] [Replace the render loop and zoom with `Viewport`](https://sdk.pdf.net/docs/v1/migration/from-pdfium#display-the-document).
* [ ] [Move page navigation from indices to page ids](https://sdk.pdf.net/docs/v1/migration/from-pdfium#pages-and-navigation).
* [ ] [Move page manipulation to the page hooks](https://sdk.pdf.net/docs/v1/migration/from-pdfium#page-manipulation).
* [ ] [Rebuild thumbnails with `PageCanvas`](https://sdk.pdf.net/docs/v1/migration/from-pdfium#thumbnails).
* [ ] [Replace the form-fill environment with widgets](https://sdk.pdf.net/docs/v1/migration/from-pdfium#forms).
* [ ] [Move text extraction and search](https://sdk.pdf.net/docs/v1/migration/from-pdfium#text-extraction-and-search).
* [ ] [Replace page object editing with interaction tools and hooks](https://sdk.pdf.net/docs/v1/migration/from-pdfium#editing-and-interaction-tools).
* [ ] [Replace `FORM_Undo` and `FFI_OnChange` tracking](https://sdk.pdf.net/docs/v1/migration/from-pdfium#undo-redo-and-change-tracking).
* [ ] [Replace `FPDF_SaveAsCopy` with `prepareDocument`](https://sdk.pdf.net/docs/v1/migration/from-pdfium#saving-and-downloading).
* [ ] Check stored positions against [coordinates](https://sdk.pdf.net/docs/v1/migration/from-pdfium#coordinates).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-pdfium#errors-and-logging) and [drop handle cleanup](https://sdk.pdf.net/docs/v1/migration/from-pdfium#cleanup-and-lifecycle).
* [ ] [Plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-pdfium#features-without-an-equivalent).
* [ ] Remove PDFium from the viewer: the build setup, the linked library, the bitmap and input plumbing, and the form-fill callbacks. Test encrypted, malformed, large, and form-heavy PDFs, page operations, and the save round-trip, including any server code that reads the saved bytes with PDFium.

## Overview [#overview]

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

  **PDFium** is a library with a C API and no UI:

  * You build it from source with Chromium's tooling (`gclient`, GN, and Ninja), choose features such as V8 JavaScript and XFA forms with GN arguments, and link it into your app. The API is the set of C headers in `public/`: `fpdfview.h`, `fpdf_formfill.h`, `fpdf_text.h`, `fpdf_edit.h`, `fpdf_save.h`, `fpdf_ppo.h`, and more.
  * `FPDF_InitLibraryWithConfig` sets up process-wide state before any other call, and `FPDF_DestroyLibrary` releases it.
  * Everything is an opaque handle you open and close yourself: `FPDF_DOCUMENT`, `FPDF_PAGE`, `FPDF_BITMAP`, `FPDF_TEXTPAGE`, and `FPDF_FORMHANDLE`.
  * There is no viewer. You render each page into a BGRx or BGRA bitmap and draw it, lay out and scroll the pages, convert pointer positions with `FPDF_DeviceToPage`, and forward input to the form-fill environment, which calls back into your app to repaint (`FFI_Invalidate`), run timers, and set the cursor.
  * Pages are 0-based indices, positions are page coordinates in points with the origin at the bottom-left corner, and text strings (`FPDF_WIDESTRING`) are UTF-16LE.
  * No call is thread-safe: an app makes one PDFium call at a time.
  * Edits change page objects in memory; `FPDFPage_GenerateContent` writes them into the page, and `FPDF_SaveAsCopy` streams the file through your write callback.

  **@sdk-pdf/react** runs in the browser; the SDK owns the document, and you own the UI:

  * [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages and handles scrolling, zooming, selection, text editing, form filling, and link navigation on its own. [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders single pages for thumbnails and previews.
  * [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the document and viewport state. Toolbars and panels are your React components, driven by hooks that re-render them on change and hooks that return commands.
  * Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers undo/redo; [`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       | PDFium                                                             | @sdk-pdf/react                                                           |
  | ------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
  | Where it runs | Native code: a desktop app or a server                             | The browser, in your React app                                           |
  | Setup         | Build from source, link, `FPDF_InitLibraryWithConfig`              | The package from a private registry and `PdfProviders`                   |
  | UI            | None: you draw bitmaps and forward input                           | `Viewport` and `PageCanvas`; you build toolbars and panels               |
  | Resources     | Handles you open and close in order                                | Owned by the SDK; `resetPdf()` unloads the document                      |
  | Pages         | 0-based indices                                                    | Session-scoped page ids                                                  |
  | Forms         | A form-fill environment, your callbacks, and your input forwarding | Built into `Viewport`; widgets and `useWidgetValue` for your own form UI |
  | Editing       | Page objects (`FPDFPageObj_*`) and `FPDFPage_GenerateContent`      | Interaction tools, paragraph and widget hooks, and page hooks            |
  | Undo          | The focused form field (`FORM_Undo`)                               | The whole document (`useUndoRedo`)                                       |
  | Saving        | `FPDF_SaveAsCopy` with a write callback                            | `prepareDocument()` resolves with the bytes                              |
  | Errors        | `NULL` or `FALSE` returns; `FPDF_GetLastError` after a failed load | Typed errors thrown before any change; `logMessages`                     |

  What this means for your code:

  1. **Split the work.** The viewer moves to the browser. Your server sends the PDF bytes and stores the bytes that `prepareDocument()` returns; batch jobs such as rendering previews, merging, flattening, or indexing text can keep running on PDFium there.
  2. **Delete the render loop and input plumbing.** Bitmap allocation, `FPDF_RenderPageBitmap` followed by `FPDF_FFLDraw`, page layout and scrolling, zooming by re-rendering, `FPDF_DeviceToPage`, and the `FPDF_FORMFILLINFO` callbacks move into `Viewport`.
  3. **Delete handle bookkeeping.** There is no library to initialize, and no page, text page, bitmap, or form handle to close.
  4. **Replace indices with ids.** Every page index in your state becomes a page id; convert at the edges with `usePages` and `usePageIndexGetter`.
  5. **Rethink editing.** Page objects become paragraphs, widgets, and images, edited through interaction tools and hooks; `FPDFPage_GenerateContent` and `FPDF_SaveAsCopy` become `prepareDocument()`.
  6. **Choose the policy.** A PDFium viewer allows exactly what you implemented; `PdfProviders` defaults to `PDF_EDITOR`, so a reader needs `PDF_VIEWER` or `PDF_PRINTER`.

  PDFium has no zoom state or fit modes: zoom is the size you render at, so [Display the document](https://sdk.pdf.net/docs/v1/migration/from-pdfium#display-the-document) covers it.
</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-pdfium#features-without-an-equivalent).

  | PDFium                                                                                         | @sdk-pdf/react                                                                                                                                       | Notes                                                                       |
  | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
  | `FPDF_InitLibraryWithConfig`, `FPDF_DestroyLibrary`                                            | `<PdfProviders>`                                                                                                                                     | Nothing to initialize or destroy.                                           |
  | `FPDF_LoadMemDocument(data, size, password)`                                                   | `loadPdf(data, { password })` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                                     | Both read from the buffer while the document is open.                       |
  | `FPDF_LoadDocument(path, password)`                                                            | `fetch` the bytes, then `loadPdf(data)`                                                                                                              | `loadPdf` takes a `Uint8Array`.                                             |
  | `FPDF_LoadCustomDocument`, `FPDFAvail_*` (linearized loading)                                  | **No equivalent**                                                                                                                                    | Fetch the whole file.                                                       |
  | `FPDF_GetLastError()` returning `FPDF_ERR_PASSWORD`                                            | `InvalidPasswordError`                                                                                                                               | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-pdfium#errors-and-logging).                              |
  | `FPDF_ERR_FORMAT`                                                                              | `DocumentFormatError`                                                                                                                                |                                                                             |
  | `FPDF_CloseDocument`                                                                           | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf)                                                                                    | Returns the load state to `'initial'`.                                      |
  | `FPDF_GetPageCount`                                                                            | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                                    | `0` while no document is loaded.                                            |
  | `FPDF_LoadPage(doc, index)`, `FPDF_ClosePage`                                                  | `usePages()[index].id`                                                                                                                               | No page handles.                                                            |
  | `FPDF_GetPageWidth`, `FPDF_GetPageHeight`                                                      | `width` and `height` of the [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) entries                                                                        | Both with rotation applied.                                                 |
  | `FPDFBitmap_Create`, `FPDF_RenderPageBitmap`, `FPDF_FFLDraw`                                   | [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport)                                                                                                            | No bitmaps to allocate or draw.                                             |
  | `FPDF_RenderPageBitmap_Start`, `FPDF_RenderPage_Continue`                                      | Nothing to call                                                                                                                                      | `Viewport` renders on its own.                                              |
  | The render size you compute to zoom                                                            | [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale)                                                                                            | Scale `1` is one user-space unit per CSS pixel.                             |
  | A fit scale you compute from the page and window sizes                                         | [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale), [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale)             | One-time fits.                                                              |
  | The `rotate` argument of `FPDF_RenderPageBitmap`, `FPDF_GRAYSCALE`, `FPDF_COLORSCHEME`         | **No equivalent**                                                                                                                                    | `useSetPageRotation` edits the document.                                    |
  | Thumbnails rendered as small bitmaps                                                           | [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas)                                                                                                        | Renders lazily, sized with CSS.                                             |
  | `FPDFPage_GetThumbnailAsBitmap`                                                                | **No equivalent**                                                                                                                                    | `PageCanvas` renders the page instead.                                      |
  | `FPDF_DeviceToPage`, `FPDF_PageToDevice`                                                       | **No equivalent**                                                                                                                                    | See [Coordinates](https://sdk.pdf.net/docs/v1/migration/from-pdfium#coordinates).                                            |
  | The current page index your viewer tracks                                                      | [`useCurrentPageId`](https://sdk.pdf.net/docs/v1/reference/useCurrentPageId)                                                                                            | The page in the middle of the viewport.                                     |
  | Scrolling your viewer to a page index                                                          | `scrollToPage(pageId)` from [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage)                                                                  |                                                                             |
  | `FPDFLink_GetLinkAtPoint`, `FPDFDest_GetDestPageIndex`, `FFI_DoGoToAction`                     | Nothing to call                                                                                                                                      | Internal links work in `Viewport` when `links` is `'interactive'`.          |
  | `FPDFBookmark_*`, `FPDF_GetPageLabel`                                                          | **No equivalent**                                                                                                                                    |                                                                             |
  | `FPDFPage_New(doc, index, width, height)`                                                      | `createPage(width, height, insertAfterPageId?)` from [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate)                                             | Inserts after a page id.                                                    |
  | `FPDFPage_Delete(doc, index)`                                                                  | `deletePages(pageIdOrIds)` from [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete)                                                                  |                                                                             |
  | `FPDF_MovePages`                                                                               | `moveBefore` or `moveAfter` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove)                                                                     |                                                                             |
  | `FPDFPage_SetRotation(page, quarterTurns)`                                                     | `setPageRotation(pageIdOrIds, degrees)` from [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation)                                           | Both set an absolute rotation.                                              |
  | —                                                                                              | `duplicatePage(sourcePageId, insertAfterPageId?)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate)                                     |                                                                             |
  | `FPDF_ImportPages` into a new document, to extract pages                                       | `prepareDocument({ pageIds })`                                                                                                                       | Saves only the listed pages, in the listed order.                           |
  | `FPDF_ImportPages` from another document, `FPDF_ImportNPagesToOne`                             | **No equivalent**                                                                                                                                    |                                                                             |
  | `FPDFDOC_InitFormFillEnvironment`, `FORM_OnAfterLoadPage`, `FORM_OnLButtonDown`, `FORM_OnChar` | `Viewport` under a content policy with interactive widgets                                                                                           | See [Forms](https://sdk.pdf.net/docs/v1/migration/from-pdfium#forms).                                                        |
  | `FFI_Invalidate`, `FFI_SetCursor`, `FFI_SetTimer`                                              | Nothing to implement                                                                                                                                 |                                                                             |
  | `FFI_OnChange`                                                                                 | `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent)                                                                         | Reports a value the user changed in the main view.                          |
  | `FPDFAnnot_GetFormFieldName`, `FPDFAnnot_GetFormFieldValue`                                    | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets), [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                                                 | Widgets, not annotations.                                                   |
  | `FPDF_SetFormFieldHighlightColor`                                                              | The `--widget-overlay-background` CSS variable                                                                                                       | See [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization). |
  | —                                                                                              | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate), the `addWidget` tool                                          | `FPDFPage_CreateAnnot` doesn't create widgets.                              |
  | `FORM_DoDocumentJSAction`, `FPDF_LoadXFA`                                                      | **No equivalent**                                                                                                                                    |                                                                             |
  | `FPDFText_LoadPage`, `FPDFText_GetText`                                                        | `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)                                               | Paragraphs, not characters.                                                 |
  | `FPDFText_GetRect`                                                                             | The paragraph's `bounds`                                                                                                                             | Bounds per paragraph, not per text segment.                                 |
  | `FPDFText_GetCharBox`, `FPDFText_GetCharIndexAtPos`                                            | **No equivalent**                                                                                                                                    |                                                                             |
  | `FPDFText_FindStart`, `FPDFText_FindNext`                                                      | [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch)                                                                                          | Highlights the matches and scrolls to the active one.                       |
  | `FPDFPageObj_NewTextObj`, `FPDFText_SetText`, `FPDFPage_InsertObject`                          | [`useParagraphCreate`](https://sdk.pdf.net/docs/v1/reference/useParagraphCreate) and `getParagraphEditor`, or the `addText` tool                                        | See [Editing and interaction tools](https://sdk.pdf.net/docs/v1/migration/from-pdfium#editing-and-interaction-tools).        |
  | `FPDFPageObj_NewImageObj`, `FPDFImageObj_LoadJpegFileInline`                                   | The `addImage` tool or [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate), with [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage) | Places a stamp annotation.                                                  |
  | `FPDFPageObj_Transform`                                                                        | The `select` tool, [`useMoveSelection`](https://sdk.pdf.net/docs/v1/reference/useMoveSelection), [`useSetParagraphProps`](https://sdk.pdf.net/docs/v1/reference/useSetParagraphProps)      |                                                                             |
  | `FPDFPage_RemoveObject`                                                                        | [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete), [`useDeleteSelection`](https://sdk.pdf.net/docs/v1/reference/useDeleteSelection)                                 | Blocks, not page objects.                                                   |
  | `FPDFPage_CreateAnnot` with `FPDF_ANNOT_INK`, `FPDFAnnot_AddInkStroke`                         | [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate)                                                                                | Ink from strokes your code collects.                                        |
  | `FPDFPageObj_CreateNewPath`, `FPDFPage_CreateAnnot` for other subtypes                         | **No equivalent**                                                                                                                                    |                                                                             |
  | `FPDFPage_GenerateContent`                                                                     | Nothing to call                                                                                                                                      | `prepareDocument()` applies the edits.                                      |
  | `FORM_Undo`, `FORM_Redo`, `FORM_CanUndo`, `FORM_CanRedo`                                       | [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo)                                                                                                      | The whole document, not the focused field.                                  |
  | An unsaved-changes flag set in `FFI_OnChange`                                                  | [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision), [`useDocumentRevisionGetter`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevisionGetter)         | Compare revisions to tell whether there are unsaved changes.                |
  | `FPDF_SaveAsCopy` with `FPDF_FILEWRITE`                                                        | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                                 | Resolves with a `Uint8Array`.                                               |
  | `FPDF_INCREMENTAL`, `FPDF_REMOVE_SECURITY`, `FPDF_SaveWithVersion`                             | **No equivalent**                                                                                                                                    |                                                                             |
  | `FPDFPage_Flatten`                                                                             | **No equivalent**                                                                                                                                    |                                                                             |
  | `FSDK_SetUnSpObjProcessHandler`                                                                | [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages)                                                                                                      | Problems the SDK works around arrive as log messages.                       |
  | `FPDFDOC_ExitFormFillEnvironment`, `FPDFText_ClosePage`, `FPDFBitmap_Destroy`                  | Nothing to call                                                                                                                                      | See [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-pdfium#cleanup-and-lifecycle).                        |
</details>

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

### Before: PDFium [#before-pdfium]

PDFium builds from source: `gclient` fetches the code, `gn args` configures a build directory, and Ninja builds it. The GN arguments choose the features:

```gn
# gn args out/Release
is_debug = false
is_component_build = false
pdf_enable_v8 = false # No JavaScript support.
pdf_enable_xfa = false # No XFA forms; XFA also requires JavaScript.
pdf_is_complete_lib = true # One static library to link.
```

The app initializes the library once per process, before any other call:

```c
#include <string.h>

#include "public/fpdfview.h"

int main(void) {
  FPDF_LIBRARY_CONFIG config;
  memset(&config, 0, sizeof(config));
  config.version = 2;
  // Zeroed fields: the default font paths, and no V8 isolate of your own.
  FPDF_InitLibraryWithConfig(&config);

  // ... load, render, edit, and save documents ...

  FPDF_DestroyLibrary();
  return 0;
}
```

### 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 your app shows documents. Its props must keep their identity between renders, so define the tools outside the component and memoize the configuration:

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

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

export function App({ documentUrl, canEdit }: { documentUrl: string; canEdit: boolean }) {
  // The PDF application re-renders whenever a PdfProviders prop changes identity.
  const config = useMemo(
    () =>
      canEdit
        ? {
            initialInteractionTool: selectTool,
            initialContentPolicy: CONTENT_POLICY_PRESET.PDF_EDITOR,
          }
        : {
            initialInteractionTool: textSelectTool,
            initialContentPolicy: CONTENT_POLICY_PRESET.PDF_VIEWER,
          },
    [canEdit]
  )

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

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

* **Nothing to build or link.** The GN arguments, the static library, the include paths, and the `FPDF_LIBRARY_CONFIG` fields go away: the install is the registry scope and the package.
* **No library lifetime.** `FPDF_InitLibraryWithConfig` and `FPDF_DestroyLibrary` have no counterpart. `PdfProviders` hosts the state, and every SDK hook and component renders inside it.
* **Pick the content policy deliberately.** A PDFium viewer can do only what you implemented. `PdfProviders` defaults to the `select` tool and [`CONTENT_POLICY_PRESET.PDF_EDITOR`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET), where text, annotations, links, and widgets are all editable. A reader starts with `PDF_VIEWER` (text and annotations read-only, links and widgets interactive) and the `textSelect` tool, as above; the initial policy applies when the viewport first mounts.
* **No JavaScript or XFA switch.** Where a PDFium build chooses `pdf_enable_v8` and `pdf_enable_xfa`, document JavaScript and XFA forms have no equivalent; see [Forms](https://sdk.pdf.net/docs/v1/migration/from-pdfium#forms).

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

### Before: PDFium [#before-pdfium-1]

`FPDF_LoadDocument` opens a file path (UTF-8), `FPDF_LoadMemDocument` takes a buffer that must stay valid while the document is open, and `FPDF_LoadCustomDocument` reads blocks through the `m_GetBlock` callback of an `FPDF_FILEACCESS`. Pass `NULL` or an empty string when there is no password. A `NULL` result means the load failed, and `FPDF_GetLastError` tells why:

```c
FPDF_DOCUMENT load_document(const void* data, int size, FPDF_BYTESTRING password) {
  // `data` must stay valid until FPDF_CloseDocument().
  FPDF_DOCUMENT doc = FPDF_LoadMemDocument(data, size, password);
  if (!doc) {
    switch (FPDF_GetLastError()) {
      case FPDF_ERR_PASSWORD:
        // Password required or incorrect: ask for it, then load again.
        break;
      case FPDF_ERR_FORMAT:
        // Not a PDF, or corrupted.
        break;
      case FPDF_ERR_SECURITY:
        // Unsupported security scheme.
        break;
    }
    return NULL;
  }
  return doc;
}

// When the document is no longer needed; after this, the buffer can be freed.
FPDF_CloseDocument(doc);
```

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

`loadPdf` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) takes the file bytes as a `Uint8Array`, so the bytes your server used to pass to `FPDF_LoadMemDocument` now come from a `fetch`:

```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 lastUrlRef = useRef(url)
  const [isPasswordNeeded, setIsPasswordNeeded] = useState(false)

  // A new url: unload the old document and drop its bytes, so the next open() fetches the
  // new file.
  useEffect(() => {
    if (lastUrlRef.current === url) return
    lastUrlRef.current = url
    bytesRef.current = null
    setIsPasswordNeeded(false)
    resetPdf()
  }, [url, resetPdf])

  async function open(password?: string) {
    try {
      if (!bytesRef.current) {
        const response = await fetch(url)
        // Where PDFium reports FPDF_ERR_FILE, check the response yourself.
        if (!response.ok) throw new Error(`Loading failed with status ${response.status}`)
        bytesRef.current = new Uint8Array(await response.arrayBuffer())
      }

      // The document reads from the buffer it receives: keep the original for a retry.
      await loadPdf(bytesRef.current.slice(), { password })
      setIsPasswordNeeded(false)
    } catch (error) {
      // The counterpart of FPDF_ERR_PASSWORD: ask for the password, then call
      // open(password).
      if (error instanceof InvalidPasswordError) setIsPasswordNeeded(true)
      else showError(error)
    }
  }

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

  return <PdfApplication />
}
```

[`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) replaces `FPDF_CloseDocument`:

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

function CloseButton() {
  const resetPdf = useResetPdf()

  // The load state returns to 'initial', and DocumentLoader shows the Open button again.
  return <button onClick={() => resetPdf()}>Close</button>
}
```

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

* **Bytes, not paths or readers.** `loadPdf` takes the whole file: block readers (`FPDF_LoadCustomDocument` with an `FPDF_FILEACCESS`) and linearized loading (`FPDFAvail_*`) have no equivalent, and a file path becomes a request to your server, which returns the bytes it used to open with PDFium.
* **The buffer rule is stricter.** `FPDF_LoadMemDocument` needs the buffer while the document is open. `loadPdf` reads from the buffer for the document's whole lifetime, and anything else touching the buffer can detach it, such as another `loadPdf` call. To reuse the bytes, pass a copy: `loadPdf(data.slice(), { password })`.
* **Passwords are a rejection.** Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`. Catch it, ask the user, and call `loadPdf` again.
* **A load state instead of a handle.** `loadState.status` is `'initial'`, `'loading'`, or `'ready'`; render the PDF UI only when it's `'ready'`. There is no load progress.
* **Closing invalidates ids.** After `resetPdf()`, page and block ids, widget snapshots, and document revisions from the old document are no longer valid. For an open-another-document action, call `resetPdf()` and then `loadPdf`.

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

### Before: PDFium [#before-pdfium-2]

The viewer renders each visible page into a bitmap at the current zoom, draws the form widgets on top, and hands the pixels to its UI toolkit:

```c
// One page at `scale`: at 1.0, one bitmap pixel per point.
FPDF_PAGE page = FPDF_LoadPage(doc, page_index);
FORM_OnAfterLoadPage(page, form);

int width = (int)(FPDF_GetPageWidth(page) * scale);
int height = (int)(FPDF_GetPageHeight(page) * scale);
FPDF_BITMAP bitmap = FPDFBitmap_Create(width, height, 0);
FPDFBitmap_FillRect(bitmap, 0, 0, width, height, 0xFFFFFFFF);

// The page content and non-interactive annotations, then the form widgets.
FPDF_RenderPageBitmap(bitmap, page, 0, 0, width, height, 0, FPDF_ANNOT);
FPDF_FFLDraw(form, bitmap, page, 0, 0, width, height, 0, FPDF_ANNOT);

// BGRx pixels with `stride` bytes per row, for your toolkit to draw.
blit_bgrx(FPDFBitmap_GetBuffer(bitmap), width, height, FPDFBitmap_GetStride(bitmap));
FPDFBitmap_Destroy(bitmap);

// The page stays loaded while it's visible; FORM_OnBeforeClosePage() and
// FPDF_ClosePage() release it.
```

Around this, the viewer lays out and scrolls the pages, re-renders an area when `FFI_Invalidate` reports a change, and zooms by rendering again at a new `scale`, such as `window_width / FPDF_GetPageWidth(page)` to fit the width. Heavy pages can render in steps with `FPDF_RenderPageBitmap_Start` and `FPDF_RenderPage_Continue`.

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

[`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages, scrolls, and zooms; your components render the rest:

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

function PdfApplication() {
  // Each document opens fitted to the viewport width.
  useViewportAutoFitScale('width')

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

The zoom you computed for each render becomes the viewport scale. [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale) reads and sets it, and [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale) calculates the fits on demand:

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

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

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

  function fitPage() {
    // The smaller fit shows whole pages in both directions.
    const { fitWidthScale, fitHeightScale } = getFitScale()
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

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

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

* **Nothing to render or repaint.** Bitmap sizing, `FPDF_RenderPageBitmap` and `FPDF_FFLDraw`, repainting on `FFI_Invalidate`, rendering in steps, and the page layout move into `Viewport`. Selection, text editing, form filling, link navigation, and the zoom and drag gestures work inside it, limited by the content policy.
* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded. It takes no props: [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) configures it, the theme styles it, and hooks control it.
* **Scale 1 is 100%:** one user-space unit, the same 1/72 inch as a PDFium point, per CSS pixel. `setScale` takes a positive finite number and throws `InvalidInputError` otherwise.
* **Zoom around a point.** `setScale(scale, pivot?)` keeps the document still at `pivot`, a point `{ x, y }` given as fractions of the viewport area (`0` the top or left edge, `1` the bottom or right edge), the center by default. The third item of `useViewportScale()`, `animateScale(scale, pivot?)`, animates the change and resolves when it ends.
* **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.
* **Fits are one-time calculations.** `fitWidthScale` fits the widest page into the viewport width and `fitHeightScale` the tallest page into its height, both accounting for `viewportPadding`, at the moment of the call. [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale) fits once per loaded document and doesn't re-apply on resizes or manual zooming.
* **Gestures are built in.** Pinch and wheel zoom go through the same scale and are tuned with `zoomGestures` on `PdfProviders` (`minScale` `0.5` and `maxScale` `4` by default).
* **Styling is CSS you own.** `theme.classNames` and `theme.styles` target the SDK's elements, `theme.viewportPadding` sets the gap around the pages, and CSS variables tune the selection and widget visuals, as [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization) describes.
* **No render options.** Grayscale, LCD text, print rendering, color schemes, and the view-only `rotate` argument have no equivalent; rotating a page is a document edit with [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation). `Viewport` shows the pages in one scrollable area, so custom layouts such as two-up spreads have no equivalent either.

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

### Before: PDFium [#before-pdfium-3]

Pages are 0-based indices from `0` to `FPDF_GetPageCount(doc) - 1`. The viewer keeps the current index, and internal links are a hit test on the page:

```c
// Your viewer keeps the current page index and scrolls to it.
static void go_to_page(Viewer* viewer, int page_index) {
  if (page_index < 0 || page_index >= FPDF_GetPageCount(viewer->doc)) {
    return;
  }
  viewer->current_page = page_index;
  scroll_to_page(viewer, page_index);
}

// A click at a point in page coordinates, from FPDF_DeviceToPage().
static void on_click(Viewer* viewer, FPDF_PAGE page, double page_x, double page_y) {
  FPDF_LINK link = FPDFLink_GetLinkAtPoint(page, page_x, page_y);
  if (!link) {
    return;
  }
  // NULL when the link carries an action instead: see FPDFLink_GetAction().
  FPDF_DEST dest = FPDFLink_GetDest(viewer->doc, link);
  if (dest) {
    go_to_page(viewer, FPDFDest_GetDestPageIndex(viewer->doc, dest));
  }
}
```

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

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

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

function GoToPage() {
  const pages = usePages()
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()
  const [pageNumber, setPageNumber] = useState('')

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

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        // People type 1-based numbers; the list is 0-based, like PDFium page indices.
        const page = pages[Number(pageNumber) - 1]
        if (page) scrollToPage(page.id, { behavior: 'smooth' })
      }}
    >
      <input
        aria-label="Page number"
        inputMode="numeric"
        placeholder={currentIndex === -1 ? '' : String(currentIndex + 1)}
        value={pageNumber}
        onChange={(event) => setPageNumber(event.currentTarget.value)}
      />
      <span>of {pages.length}</span>
    </form>
  )
}
```

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

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

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

* **Ids survive reordering; indices don't.** After `FPDFPage_Delete` or `FPDF_MovePages`, every later index points at another page. An id keeps pointing at the same page while pages are added, removed, and rearranged by the user. Ids are session-scoped: persist page numbers, not ids.
* **Ids can go stale.** `getPageIndex` returns `undefined` for an unknown id, and `scrollToPage` throws `InvalidInputError`.
* **No page handles.** `FPDF_LoadPage`, `FPDF_ClosePage`, and the per-page `FORM_OnAfterLoadPage` calls have no counterpart: the SDK loads page content when it needs it.
* **Scroll options.** `scrollToPage(pageId, options?)` takes `behavior` (`'auto'`, `'smooth'`, or `'instant'`), `vertical` (`'top'` by default), and `horizontal` (`'nearest'` by default).
* **Links.** Internal links work inside `Viewport` when the content policy's `links` level is `'interactive'`, so the `FPDFLink_*` hit test and the `FFI_DoGoToAction` callback go away. Bookmarks (`FPDFBookmark_*`), named-destination lookup (`FPDF_GetNamedDestByName`), and page labels (`FPDF_GetPageLabel`) have no equivalent.
* **Page sizes.** `FPDF_GetPageWidth` and `FPDF_GetPageHeight` become the `width` and `height` of the `usePages()` entries: user-space units with the rotation applied, along with `rotation` in degrees.

## Page manipulation [#page-manipulation]

### Before: PDFium [#before-pdfium-4]

The editing API changes pages by index:

```c
// Insert a blank US Letter page (612 × 792 points) at index 2.
FPDF_PAGE blank = FPDFPage_New(doc, 2, 612, 792);
FPDF_ClosePage(blank);

// Delete the page at index 4.
FPDFPage_Delete(doc, 4);

// Move the page at index 5 to the front (experimental API).
int pages_to_move[] = {5};
FPDF_MovePages(doc, pages_to_move, 1, 0);

// Set the first page's rotation to 90 degrees clockwise: values 0 to 3 are quarter turns.
FPDF_PAGE first = FPDF_LoadPage(doc, 0);
FPDFPage_SetRotation(first, 1);
FPDF_ClosePage(first);

// Append pages 1 to 3 of another document (the range is 1-based).
FPDF_ImportPages(doc, other_doc, "1-3", FPDF_GetPageCount(doc));
```

### 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). While you port index-based call sites, a small adapter resolves the indices against `usePages`:

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

// Index-based helpers for code ported from PDFium; new code passes page ids directly.
function usePageOperations() {
  const pages = usePages()
  const createPage = usePageCreate()
  const deletePages = usePageDelete()
  const { moveBefore } = usePageMove()
  const setPageRotation = useSetPageRotation()

  return {
    // FPDFPage_New(doc, index, width, height): new pages go after a page, or at the end.
    insertBlankPage(index: number, width: number, height: number) {
      const previousPage = pages[Math.min(index, pages.length) - 1]
      const pageId = createPage(width, height, previousPage?.id)
      // Without a previous page, the new page belongs at the start.
      if (!previousPage) moveBefore(pageId)
      return pageId
    },
    // FPDFPage_Delete(doc, index)
    deletePage(index: number) {
      deletePages(pages[index].id)
    },
    // FPDF_MovePages(doc, [index], 1, 0)
    moveToFront(index: number) {
      moveBefore(pages[index].id)
    },
    // FPDFPage_SetRotation(page, quarterTurns): both set an absolute rotation.
    setRotation(index: number, quarterTurns: number) {
      setPageRotation(pages[index].id, quarterTurns * 90)
    },
  }
}
```

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

| Operation | PDFium                                                                                      | @sdk-pdf/react                                                                                                                                                          |
| --------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Insert    | `FPDFPage_New` at an index, size in points, returns a page to close                         | `createPage(width, height, insertAfterPageId?)` after a page, or appended; size in user-space units; returns the new id                                                 |
| Delete    | `FPDFPage_Delete` by index                                                                  | `deletePages(pageIdOrIds)`, one or several pages                                                                                                                        |
| Move      | `FPDF_MovePages` (experimental); a failure may leave the document in an indeterminate state | `moveBefore(ids, beforePageId?)` (start when omitted) or `moveAfter(ids, afterPageId?)` (end when omitted); invalid ids throw `InvalidInputError` before anything moves |
| Rotate    | `FPDFPage_SetRotation` with quarter turns `0` to `3`                                        | `setPageRotation` with degrees `0`, `90`, `180`, or `270`; `-90` and `450` are normalized                                                                               |
| Duplicate | —                                                                                           | `duplicatePage(sourcePageId, insertAfterPageId?)` is asynchronous and inserts after the source by default                                                               |
| Extract   | `FPDF_ImportPages` into a new document                                                      | `prepareDocument({ pageIds })` saves only the listed pages, in the listed order                                                                                         |
| Import    | `FPDF_ImportPages` from another document                                                    | No equivalent                                                                                                                                                           |

* **Undo steps.** Deleting, moving, or rotating several pages in one call is a single undo step, and an undo can restore a deleted page with its blocks. Separate edits within 500 ms merge into one step, so the insert-then-move above undoes as one action; `markUndoBoundary()` from [`useUndoBoundary`](https://sdk.pdf.net/docs/v1/reference/useUndoBoundary) keeps an operation a separate step.
* **No page events.** Page operations happen where your code calls them, so track them there; [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) changes on any edit, undo, or redo.
* **Server-side page work stays.** Importing pages from another document, N-up layouts (`FPDF_ImportNPagesToOne`), and page boxes (`FPDFPage_SetCropBox`, `FPDFPage_SetMediaBox`) have no equivalent: keep merging and imposition on your server with PDFium, and load the result.

## Thumbnails [#thumbnails]

### Before: PDFium [#before-pdfium-5]

A thumbnail is the same render at a small size, cached by the viewer:

```c
// A 120-pixel-wide thumbnail of the page at `page_index`.
FPDF_PAGE page = FPDF_LoadPage(doc, page_index);
double scale = 120.0 / FPDF_GetPageWidth(page);
int width = 120;
int height = (int)(FPDF_GetPageHeight(page) * scale);

FPDF_BITMAP thumbnail = FPDFBitmap_Create(width, height, 0);
FPDFBitmap_FillRect(thumbnail, 0, 0, width, height, 0xFFFFFFFF);
// Without FPDF_FFLDraw, form widgets don't appear.
FPDF_RenderPageBitmap(thumbnail, page, 0, 0, width, height, 0, FPDF_ANNOT);
FPDF_ClosePage(page);

// Your cache owns the bitmap until FPDFBitmap_Destroy().
cache_thumbnail(page_index, thumbnail);
```

Files that store their own thumbnails expose them through the experimental `FPDFPage_GetThumbnailAsBitmap`.

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

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

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

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

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

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

* **Size with CSS, not bitmap math.** The render resolution follows the canvas's displayed CSS size, or an explicit `scale` prop, so the pixels stay sharp without a scale computation.
* **Lazy, with no cache to manage.** An off-screen canvas does no work until it becomes visible. In a virtualized list, pass `isVisible` to skip the per-canvas intersection observer; `renderOnResize` re-renders a canvas sized relative to the window.
* **Widgets included.** `PageCanvas` draws annotations, form widgets included, unless `hideAnnotations` is set; where `FPDF_RenderPageBitmap` with `FPDF_ANNOT` leaves widgets to `FPDF_FFLDraw`, there is no second step.
* **Thumbnails that show session edits are coming soon.** `PageCanvas` draws the page as loaded from the file; the main view shows the edits. Rotation is applied and followed live.
* **Embedded thumbnails.** `FPDFPage_GetThumbnailAsBitmap` has no equivalent: `PageCanvas` always renders the page.

## Forms [#forms]

### Before: PDFium [#before-pdfium-6]

Forms need a form-fill environment: a `FPDF_FORMFILLINFO` with your viewer's callbacks, a form handle for the document, a call for each loaded page, and every mouse and keyboard event forwarded in page coordinates:

```c
#include <string.h>

#include "public/fpdf_formfill.h"

static FPDF_FORMFILLINFO form_info;

FPDF_FORMHANDLE init_forms(FPDF_DOCUMENT doc) {
  memset(&form_info, 0, sizeof(form_info));
  form_info.version = 1;
  // Your viewer's callbacks: repaint an area, set the cursor, run timers, and look up
  // pages.
  form_info.FFI_Invalidate = on_invalidate;
  form_info.FFI_SetCursor = on_set_cursor;
  form_info.FFI_SetTimer = on_set_timer;
  form_info.FFI_KillTimer = on_kill_timer;
  form_info.FFI_GetPage = on_get_page;
  // Called when any field value changes.
  form_info.FFI_OnChange = on_change;

  // form_info must stay valid until FPDFDOC_ExitFormFillEnvironment().
  FPDF_FORMHANDLE form = FPDFDOC_InitFormFillEnvironment(doc, &form_info);
  FPDF_SetFormFieldHighlightColor(form, FPDF_FORMFIELD_UNKNOWN, 0xFFE4DD);
  return form;
}
```

```c
// After FPDF_LoadPage(), and before FPDF_ClosePage():
FORM_OnAfterLoadPage(page, form);
FORM_OnBeforeClosePage(page, form);

// Your window's input, with positions converted by FPDF_DeviceToPage():
FORM_OnMouseMove(form, page, modifiers, page_x, page_y);
FORM_OnLButtonDown(form, page, modifiers, page_x, page_y);
FORM_OnLButtonUp(form, page, modifiers, page_x, page_y);
FORM_OnKeyDown(form, page, key_code, modifiers);
FORM_OnChar(form, page, char_code, modifiers);
```

Reading field names and values goes through the experimental annotation API: `FPDFPage_GetAnnot`, then `FPDFAnnot_GetFormFieldName` and `FPDFAnnot_GetFormFieldValue` with the form handle. `FPDFPage_CreateAnnot` doesn't create widget annotations, so adding form fields isn't part of PDFium's API.

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

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

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

function FormFieldList() {
  const { widgets, isLoaded } = useWidgets()
  const scrollToBlock = useScrollToBlock()

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

  return (
    <ul>
      {widgets.map((widget) => (
        <li key={widget.id}>
          {/* Jump to the widget in the main view. */}
          <button
            onClick={() =>
              scrollToBlock(widget.id, { behavior: 'smooth', vertical: 'center' })
            }
          >
            {widget.fieldName}
          </button>
          <FieldInput widget={widget} />
        </li>
      ))}
    </ul>
  )
}

function FieldInput({ widget }: { widget: Widget }) {
  switch (widget.widgetType) {
    case 'text':
      return <TextInput widget={widget} />
    case 'checkbox':
      return <CheckboxInput widget={widget} />
    default:
      return null
  }
}

function TextInput({ widget }: { widget: TextWidget }) {
  // Typing in the main view and undo update the value here too.
  const [value, setValue] = useWidgetValue(widget)

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

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

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

### From the form-fill environment to widgets [#from-the-form-fill-environment-to-widgets]

| Concern       | PDFium                                                                            | @sdk-pdf/react                                                                                                                                             |
| ------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Setup         | `FPDF_FORMFILLINFO` callbacks, `FPDFDOC_InitFormFillEnvironment`, a call per page | Nothing: `Viewport` handles forms under a policy with interactive widgets                                                                                  |
| Drawing       | `FPDF_FFLDraw` after `FPDF_RenderPageBitmap`                                      | `Viewport` draws the widgets                                                                                                                               |
| Input         | `FORM_OnMouseMove`, `FORM_OnLButtonDown`, `FORM_OnChar`, and more                 | `Viewport` handles pointer and keyboard input                                                                                                              |
| Highlight     | `FPDF_SetFormFieldHighlightColor`, `FPDF_SetFormFieldHighlightAlpha`              | The `--widget-overlay-background` and `--widget-focused-overlay-background` CSS variables                                                                  |
| Field values  | `FPDFAnnot_GetFormFieldValue` (experimental), UTF-16LE strings                    | `useWidgetValue(widget)`: `string` for `text` and `choice`, `boolean` for `checkbox` and `radio`, option indices for `multiChoice`                         |
| Change events | `FFI_OnChange`                                                                    | `useWidgetValue` re-renders; `widgetInput` from [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) reports a value the user changed                 |
| Creating      | —                                                                                 | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate), or the `{ type: 'addWidget', widgetProps }` tool under `PDF_EDITOR` |
| Scripts       | `FORM_DoDocumentJSAction` and `IPDF_JSPLATFORM` in V8 builds                      | No equivalent                                                                                                                                              |

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

* **Nothing to wire.** The callbacks, the timers, the cursor changes, input forwarding, and `FORM_OnAfterLoadPage` for each page go away, and so does the form handle. [`useFocusedWidgetId`](https://sdk.pdf.net/docs/v1/reference/useFocusedWidgetId) returns the widget being filled, where PDFium reports the focused annotation.
* **Values are read and written per widget.** `useWidgetValue` binds one widget per hook call, and every widget with the same field name shows and synchronizes the same value. With nothing to read, the value is the kind's empty value: `''`, `false`, or `[]`.
* **The saved PDF carries the values.** `prepareDocument()` produces the filled PDF. Include server code that reads the fields from the saved bytes, such as with `FPDFAnnot_GetFormFieldValue`, in your round-trip tests.
* **Form building is new.** `useWidgetsCreate`, the `addWidget` tool, [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps), and [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) create, restyle, and delete widgets. To lock a submitted form, call `setWidgetProps(ids, { isReadOnly: true }, { excludeFromHistory: true })`.
* **No scripts or XFA.** `FORM_DoDocumentJSAction`, the page and document actions (`FORM_DoPageAAction`, `FORM_DoDocumentAAction`), and `FPDF_LoadXFA` have no equivalent. The SDK recognizes Acrobat date formats without running scripts and exposes them as the text widget's `format` (see [`FormatType`](https://sdk.pdf.net/docs/v1/reference/FormatType)); fields driven by other scripts read as plain text fields.
* **Signatures.** `signature` widgets carry a read-only `isCryptoSigned`; the SDK never signs.

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

### Before: PDFium [#before-pdfium-7]

A text page gives the page's characters, and a search handle walks the matches:

```c
#include <stdlib.h>

#include "public/fpdf_text.h"

FPDF_TEXTPAGE text_page = FPDFText_LoadPage(page);
int count = FPDFText_CountChars(text_page);

// `count` UCS-2 values plus a terminator.
unsigned short* text = malloc((count + 1) * sizeof(unsigned short));
FPDFText_GetText(text_page, 0, count, text);

// Every match on the page, with rectangles to highlight.
FPDF_SCHHANDLE search = FPDFText_FindStart(text_page, query, 0, 0);
while (FPDFText_FindNext(search)) {
  int start = FPDFText_GetSchResultIndex(search);
  int length = FPDFText_GetSchCount(search);
  int rect_count = FPDFText_CountRects(text_page, start, length);
  for (int i = 0; i < rect_count; i++) {
    double left, top, right, bottom;
    FPDFText_GetRect(text_page, i, &left, &top, &right, &bottom);
    add_highlight(page_index, left, top, right, bottom);
  }
}
FPDFText_FindClose(search);

free(text);
FPDFText_ClosePage(text_page);
```

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

`getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) loads a page and resolves with its paragraphs in content order, each with `id`, `content`, the 1-based `pageNumber`, the user-space box `bounds` (`{ x, y, width, height }`), and the paragraph layout, such as `textAlign` and `lineHeight`. A page's text is `paragraphs.map((paragraph) => paragraph.content).join('\n\n')`; across the whole document, `getAllParagraphIds` returns every paragraph id, and `getPlainTextParagraphs` resolves them to the same entries.

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

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

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

  return (
    <div role="search">
      <input
        value={criteria.query}
        onChange={(event) =>
          setSearchCriteria({ ...criteria, query: event.currentTarget.value })
        }
      />
      <label>
        <input
          type="checkbox"
          checked={criteria.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>
  )
}
```

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

* **Search options.** `isCaseSensitive` and `isWholeWord` replace the `FPDF_MATCHCASE` and `FPDF_MATCHWHOLEWORD` flags. A changed query is searched once typing pauses, and `status` is `'searching'` until the whole document is searched.
* **Matches, not rectangles.** Each match carries `id`, `pageId`, `paragraphId`, and the matched `text`, in document order, and the SDK draws the highlights itself: there is no `FPDFText_GetRect` per match. `goToMatch(match.id)` activates a match from a results list.
* **Paragraphs, not characters.** There are no per-character boxes (`FPDFText_GetCharBox`), no character index at a point (`FPDFText_GetCharIndexAtPos`), no font details outside the selection (`FPDFText_GetFontInfo`), and no web link detection (`FPDFLink_LoadWebLinks`). [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle) reads the font of the selected text, and there is no API that returns the selected text.
* **Page numbers are 1-based.** `pageNumber` counts from 1, where PDFium page indices count from 0.
* **Batch extraction can stay on the server.** Indexing documents for search doesn't need a viewer: keep it on PDFium and run it on the bytes your server stores.

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

### Before: PDFium [#before-pdfium-8]

Editing works on page objects: create them, place them with a matrix, insert them into the page, and generate the page content before saving:

```c
// FPDF_WIDESTRING text is UTF-16LE.
static const FPDF_WCHAR kApproved[] = {'A', 'p', 'p', 'r', 'o', 'v', 'e', 'd', 0};

// A line of text in a standard font, moved to (72, 720) in page coordinates.
FPDF_PAGEOBJECT text = FPDFPageObj_NewTextObj(doc, "Helvetica", 12.0f);
FPDFText_SetText(text, kApproved);
FPDFPageObj_Transform(text, 1, 0, 0, 1, 72, 720);
FPDFPage_InsertObject(page, text);

// A JPEG image, placed and sized with a transform matrix.
FPDF_PAGEOBJECT image = FPDFPageObj_NewImageObj(doc);
FPDFImageObj_LoadJpegFileInline(NULL, 0, image, &jpeg_access);
FPDFImageObj_SetMatrix(image, 200, 0, 0, 100, 72, 500);
FPDFPage_InsertObject(page, image);

// Remove the first page object; the caller owns it afterwards (experimental API).
FPDF_PAGEOBJECT first = FPDFPage_GetObject(page, 0);
if (FPDFPage_RemoveObject(page, first)) {
  FPDFPageObj_Destroy(first);
}

// Without this, the changes are lost on save or reload.
FPDFPage_GenerateContent(page);
```

Any UI for placing, moving, or retyping these objects is your viewer's code.

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

Programmatic text goes through paragraphs: [`useParagraphCreate`](https://sdk.pdf.net/docs/v1/reference/useParagraphCreate) creates an empty one, and `getParagraphEditor` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) types into it:

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

// Replaces FPDFPageObj_NewTextObj, FPDFText_SetText, and FPDFPage_InsertObject.
function useAddText() {
  const createParagraph = useParagraphCreate()
  const { getParagraphEditor } = useProgrammaticEditor()

  return async (pageId: string, text: string) => {
    // The position in user-space units.
    const paragraphId = await createParagraph(pageId, 72, 720)
    const editor = getParagraphEditor(paragraphId)
    // The paragraph may be gone already: deleted by an undo.
    if (!editor) return undefined

    editor.selectAll()
    editor.deleteAll()
    editor.insertText(text)
    return paragraphId
  }
}
```

For users, the placement UI you built becomes the **interaction mode**: a **content policy** (what may change) and an **interaction tool** (what a click or drag does). [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) switches both at once, and [`useInteractionTool`](https://sdk.pdf.net/docs/v1/reference/useInteractionTool) reads the tool back:

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

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

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

  async function placeImage(file: File) {
    // Any image the browser can decode, where PDFium loads JPEG files or bitmaps.
    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">
      {/* Select, move, and resize blocks: the counterpart of your FPDFPageObj_Transform
         UI. */}
      <button
        aria-pressed={interactionTool.type === 'select'}
        onClick={() => setInteractionMode(selectTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Select
      </button>
      <button
        aria-pressed={interactionTool.type === 'addText'}
        onClick={() => setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Add text
      </button>
      <label>
        Add image
        <input
          type="file"
          accept="image/*"
          onChange={(event) => {
            const file = event.currentTarget.files?.[0]
            if (file) placeImage(file).catch(showError)
          }}
        />
      </label>
    </div>
  )
}
```

### From page objects to blocks [#from-page-objects-to-blocks]

| PDFium                                                                    | @sdk-pdf/react                                                                                                                                                                                                               |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FPDFPageObj_NewTextObj`, `FPDFPageObj_CreateTextObj`, `FPDFText_SetText` | `createParagraph(pageId, x, y)` and `getParagraphEditor`, or the `{ type: 'addText' }` tool, which needs editable text                                                                                                       |
| `FPDFText_SetText` on an existing text object                             | Text is editable in place under `PDF_EDITOR`; `getParagraphEditor(paragraphId)` rewrites a paragraph from code                                                                                                               |
| The font and size of `FPDFPageObj_NewTextObj`, `FPDFPageObj_SetFillColor` | `fontFamily`, `fontSize`, `textColor`, and more from [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle), applied to the selection; `fontFamily.options` lists the available families                        |
| `FPDFPageObj_NewImageObj`, `FPDFImageObj_SetBitmap`                       | The `{ type: 'addImage', image }` tool with [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage), or [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate) at given coordinates; both create a stamp annotation |
| `FPDFPageObj_Transform`                                                   | The `select` tool, `moveSelection(direction, distance?)` from [`useMoveSelection`](https://sdk.pdf.net/docs/v1/reference/useMoveSelection), [`useSetParagraphProps`](https://sdk.pdf.net/docs/v1/reference/useSetParagraphProps)                                   |
| `FPDFPage_RemoveObject`, `FPDFPageObj_Destroy`                            | `deleteBlocks(blockIds)` from [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete), or [`useDeleteSelection`](https://sdk.pdf.net/docs/v1/reference/useDeleteSelection)                                                                        |
| `FPDFPageObj_CreateNewPath`, `FPDFPageObj_CreateNewRect`, `FPDFPath_*`    | No equivalent                                                                                                                                                                                                                |
| `FPDFPage_CreateAnnot` (experimental) for ink, `FPDFAnnot_AddInkStroke`   | [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from strokes your code collects                                                                                              |
| `FPDFPage_CreateAnnot` for squares, highlights, notes                     | No equivalent; existing shapes and lines display, and [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps) changes their border color and width, ink included                                     |
| `FPDFPage_GenerateContent`                                                | Nothing to call: edits are recorded as they happen                                                                                                                                                                           |

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

* **Paragraphs, not text runs.** A PDFium text object is one run of text at a fixed position. A paragraph wraps its text within its width, which `setParagraphProps([paragraphId], { width })` from `useSetParagraphProps` sets along with the position and line layout, and its height follows the text.
* **Switch the policy with the tool.** Pass `PDF_EDITOR` with the editing tools, as above, and compare the active tool by `.type`. `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).
* **Images become stamps.** `stageImage(blob)` validates and measures any image the browser can decode, SVGs with an absolute root width and height included. The `addImage` tool places it at the next click, and `createStamp({ pageId, image, x: 72, y: 500, width: 200, height: 100 })` from `useStampAnnotationCreate` places it like the `FPDFImageObj_SetMatrix` call above, with `x` and `y` the bottom-left corner in user space. Both take an optional `width` and `height`: a single dimension keeps the aspect ratio, and with none the image keeps its natural size, fit to the page.
* **Programmatic edits.** They flow through the same document as user edits; [Programmatic editing](https://sdk.pdf.net/docs/v1/guides/programmatic-editing#presence-for-collaborators) shows how to give your code a presence cursor for real-time collaboration, which is coming soon.
* **Selection commands.** [`useSelection`](https://sdk.pdf.net/docs/v1/reference/useSelection) tells a paragraph in text editing (`{ type: 'text', blockId }`) from selected blocks (`{ type: 'blocks', blockIds }`), and [`useSelectedBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectedBlocks), [`useSelectBlocks`](https://sdk.pdf.net/docs/v1/reference/useSelectBlocks), and [`useDuplicateSelection`](https://sdk.pdf.net/docs/v1/reference/useDuplicateSelection) read and act on what the user selected, and [`useFocusSelection`](https://sdk.pdf.net/docs/v1/reference/useFocusSelection) returns the focus to the text after a toolbar click.
* **Keyboard shortcuts are your code.** Wire delete, arrow-key nudging, and undo with [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys), with a memoized mapping.

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

### Before: PDFium [#before-pdfium-9]

Undo and redo work on the focused form field, and `FFI_OnChange` reports field value changes. Undo for page object edits, and tracking them as unsaved changes, is your own code:

```c
// Toolbar buttons: undo and redo inside the focused form field.
static void on_undo(Viewer* viewer) {
  if (FORM_CanUndo(viewer->form, viewer->focused_page)) {
    FORM_Undo(viewer->form, viewer->focused_page);
  }
}

static void on_redo(Viewer* viewer) {
  if (FORM_CanRedo(viewer->form, viewer->focused_page)) {
    FORM_Redo(viewer->form, viewer->focused_page);
  }
}

// FPDF_FORMFILLINFO.FFI_OnChange: some field value changed.
static void on_change(FPDF_FORMFILLINFO* info) {
  set_unsaved_changes(1);
}
```

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

[`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo) returns the commands and their availability for the whole document; `canUndo` and `canRedo` re-render the component as the history changes:

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

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

  // Window-level shortcuts; the mapping must be memoized.
  useHotkeys(useMemo(() => ({ 'Mod-z': undo, 'Shift-Mod-z': redo }), [undo, redo]))

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

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

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

* **One history for the document,** not for one field: deleting, moving, or rotating pages are undo steps, widget property changes are undo steps unless made with the `excludeFromHistory` option of [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps), and form values follow undo and redo.
* **Local and merged.** Undo reverts the local user's changes, and edits made within 500 ms of each other merge into one step, even across hook calls. To keep an operation a separate step, call `markUndoBoundary()` from [`useUndoBoundary`](https://sdk.pdf.net/docs/v1/reference/useUndoBoundary) before it starts and after its edits finish.
* **Events for user actions only.** Where `FFI_OnChange` fed other code, put `useDocumentRevision` in the dependencies of a `useEffect` to run on any change. [`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, so track them where you call them.

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

### Before: PDFium [#before-pdfium-10]

`FPDF_SaveAsCopy` writes the document through an `FPDF_FILEWRITE` whose `WriteBlock` receives the file in pieces; edited pages need `FPDFPage_GenerateContent` first:

```c
#include <stdio.h>

#include "public/fpdf_save.h"

typedef struct {
  FPDF_FILEWRITE base;  // First member, so PDFium's pointer is also ours.
  FILE* file;
} FileWriter;

static int write_block(FPDF_FILEWRITE* self, const void* data, unsigned long size) {
  FileWriter* writer = (FileWriter*)self;
  return fwrite(data, 1, size, writer->file) == size;
}

FPDF_BOOL save_document(FPDF_DOCUMENT doc, FILE* file) {
  FileWriter writer = {{1, write_block}, file};  // FPDF_FILEWRITE version 1.
  // Flags: 0, or FPDF_INCREMENTAL or FPDF_NO_INCREMENTAL (not both),
  // optionally combined with FPDF_REMOVE_SECURITY.
  return FPDF_SaveAsCopy(doc, &writer.base, 0);
}
```

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

[`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) applies the recorded changes to the original document and resolves with the bytes, which go back to your server:

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

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

  // Replaces the FFI_OnChange flag: every edit, including page edits; undoing back to the
  // saved state clears it.
  const hasUnsavedChanges = revision !== savedRevision

  async function save() {
    if (!hasUnsavedChanges || isSavingRef.current) return
    isSavingRef.current = true
    try {
      // Edits made after the save starts may be missing from the saved bytes, so only
      // this revision becomes saved.
      const startRevision = getRevision()
      // Replaces FPDFPage_GenerateContent and FPDF_SaveAsCopy.
      // The bytes go to fetch, so don't hand out the loaded document's buffer.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })
      const response = await fetch(documentUrl, {
        method: 'PUT',
        body: bytes as BodyInit,
      })
      // A failed upload keeps the unsaved-changes indicator.
      if (!response.ok) throw new Error(`Saving failed with status ${response.status}`)

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

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

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

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

* **Nothing to generate or stream.** There is no `FPDFPage_GenerateContent` step and no write callback: `prepareDocument()` resolves with the complete PDF as a `Uint8Array`, and `downloadDocument()` saves the same bytes on the user's device.
* **returnOriginalIfUnchanged** defaults to `true`, returning the buffer passed to `loadPdf` when nothing changed; set it to `false` when the bytes will be transferred or mutated, as above.
* **Page subsets.** `prepareDocument({ pageIds })` saves only the listed pages, in the listed order, where PDFium extracts pages with `FPDF_ImportPages` into a new document.
* **No save flags.** Incremental saves (`FPDF_INCREMENTAL`), removing security (`FPDF_REMOVE_SECURITY`), choosing the file version (`FPDF_SaveWithVersion`), and flattening (`FPDFPage_Flatten`) have no equivalent.
* **Your server keeps its PDFium steps.** The upload is a complete PDF, so work your server did after `FPDF_SaveAsCopy`, such as flattening, indexing, or rendering previews, can run on the uploaded bytes.

## Coordinates [#coordinates]

Both sides use PDF page space with the origin at the bottom-left corner and units of 1/72 inch, so positions such as widget boxes and paragraph bounds should carry over. Check placement on a test document before converting stored positions; page sizes come from [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages).

| Topic          | PDFium                                                                                 | @sdk-pdf/react                                                             |
| -------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Page reference | 0-based page index                                                                     | Session-scoped page ids; the position is the 0-based index in `usePages()` |
| Origin         | Page coordinates: bottom-left corner, y up; device coordinates: top left of the window | User space with the origin at the bottom-left corner of the page           |
| Units          | Points, 1/72 inch                                                                      | User-space units, 1/72 inch                                                |
| Page size      | `FPDF_GetPageWidth`, `FPDF_GetPageHeight`, with rotation applied                       | `width` and `height` of the `usePages()` entries, with rotation applied    |
| Boxes          | `left`, `top`, `right`, `bottom`, such as from `FPDFText_GetRect`                      | `x`, `y`, `width`, `height`, such as widget and paragraph bounds           |
| Conversion     | `FPDF_DeviceToPage`, `FPDF_PageToDevice`                                               | No conversion helpers                                                      |

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

### Before: PDFium [#before-pdfium-11]

Calls report failure through their return values, `FPDF_GetLastError` explains a failed load, and an unsupported-feature handler reports content PDFium doesn't support:

```c
#include "public/fpdf_ext.h"

static void on_unsupported(UNSUPPORT_INFO* info, int type) {
  // One of FPDF_UNSP_*, such as FPDF_UNSP_DOC_XFAFORM or FPDF_UNSP_ANNOT_3DANNOT.
  log_warning("PDFium: unsupported feature", type);
}

static UNSUPPORT_INFO unsupported_info = {1, on_unsupported};

void set_up_diagnostics(void) {
  FSDK_SetUnSpObjProcessHandler(&unsupported_info);
}

FPDF_DOCUMENT open_document(const void* data, int size) {
  FPDF_DOCUMENT doc = FPDF_LoadMemDocument(data, size, NULL);
  if (!doc) {
    // FPDF_GetLastError() explains failed loads; other calls signal failure
    // through their return value.
    report_load_error(FPDF_GetLastError());
  }
  return doc;
}
```

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

Failures are typed errors thrown by the call that failed, and problems the SDK works around arrive through [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages). [Error handling and logging](https://sdk.pdf.net/docs/v1/guides/error-handling-and-logging) has the full rules:

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

function useSdkLogs() {
  useEffect(() => {
    // Replaces FSDK_SetUnSpObjProcessHandler; the SDK never prints to the console.
    const unsubscribe = logMessages.on('message', (logMessage) => {
      if (logMessage.level >= LogLevel.Warning)
        reportToMonitoring(logMessage.message, logMessage.data)
    })
    return () => {
      unsubscribe()
    }
  }, [])
}

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

  return async (data: Uint8Array) => {
    try {
      // Pass a copy and keep `data` for a password retry.
      await loadPdf(data.slice())
    } catch (error) {
      // Replaces the switch on FPDF_GetLastError(): narrow with instanceof.
      if (error instanceof InvalidPasswordError) askForPassword()
      else showError(error)
    }
  }
}
```

### Error classes [#error-classes]

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

| @sdk-pdf/react           | Thrown when                                                             | Closest PDFium situation                                                             |
| ------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `InvalidPasswordError`   | The document is encrypted, and the password is missing or wrong.        | `FPDF_ERR_PASSWORD`                                                                  |
| `DocumentFormatError`    | The PDF content is malformed beyond what the SDK can work around.       | `FPDF_ERR_FORMAT`                                                                    |
| `NotSupportedError`      | The document uses a PDF feature the SDK doesn't support yet.            | A feature the unsupported-feature handler reports, when the SDK can't work around it |
| `InvalidInputError`      | The input passed to the API is invalid.                                 | A `NULL`, `FALSE`, or `-1` return for an invalid argument                            |
| `DocumentNotLoadedError` | An operation requires a document, and none is loaded.                   | —                                                                                    |
| `UninitializedError`     | An object is used before its asynchronous initialization has completed. | —                                                                                    |
| —                        | —                                                                       | `FPDF_ERR_FILE`: your own `fetch` fails instead                                      |

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

* **Nothing half-applied.** Functions that modify the document validate first and throw before touching anything, where a failed `FPDF_MovePages` may leave the document in an indeterminate state. Functions that only read return empty values instead of throwing, and hooks never throw during render; `Viewport` and `PageCanvas` throw `DocumentNotLoadedError` when rendered without a document.
* **Logs are opt-in.** Each message carries `level` (`LogLevel.Debug`, `Info`, `Warning`, or `Error`), `reason` (a `LogReason` member, such as an unsupported feature or a document format issue), a static `message`, `data`, and `stack`. Unsubscribe in the effect cleanup, as above.
* **Error UI is yours.** Map the error classes to your own messages, as you did for the `FPDF_ERR_*` codes.

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

### Before: PDFium [#before-pdfium-12]

Every handle is closed in reverse order of creation, and the library is destroyed last. PDFium calls aren't thread-safe, so integrations also keep every call on one thread or behind a mutex:

```c
FPDFText_ClosePage(text_page);
FPDFBitmap_Destroy(bitmap);

FORM_DoPageAAction(page, form, FPDFPAGE_AACTION_CLOSE);
FORM_OnBeforeClosePage(page, form);
FPDF_ClosePage(page);

FORM_DoDocumentAAction(form, FPDFDOC_AACTION_WC);
FPDFDOC_ExitFormFillEnvironment(form);
FPDF_CloseDocument(doc);

// Once per process, after closing everything else.
FPDF_DestroyLibrary();
```

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

Most of this goes away:

| Concern                        | @sdk-pdf/react                                                                                                                                   |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Library                        | Nothing to initialize or destroy: [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the state.                                             |
| Pages, text pages, and bitmaps | Nothing to close: `Viewport` and [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) render on their own, and an off-screen `PageCanvas` does no work. |
| Form handle                    | Nothing to exit: forms are part of `Viewport`.                                                                                                   |
| Document                       | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) unloads the document and invalidates its ids, snapshots, and revisions.        |
| Event listeners                | [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) subscriptions follow the component lifecycle.                                          |
| Log subscriptions              | `logMessages.on()` returns an unsubscribe function.                                                                                              |
| Keyboard listeners             | [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) installs window-level shortcuts; pass a falsy value instead of the mapping to disable them.        |
| Buffers                        | Don't reuse a buffer passed to `loadPdf`; copy it first.                                                                                         |

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

Plan around these before you migrate:

| PDFium feature                                                                                                                                        | In @sdk-pdf/react                                                                                                                                                                      |
| ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rendering pages to images outside a browser, such as server-side previews                                                                             | Keep PDFium on the server; `PageCanvas` renders in the browser                                                                                                                         |
| Per-character data (`FPDFText_GetCharBox`, `FPDFText_GetCharIndexAtPos`, `FPDFText_GetFontInfo`) and match rectangles (`FPDFText_GetRect`)            | No equivalent; `getParagraphsByPage` returns paragraph text and bounds, `useDocumentSearch` highlights matches itself, and `useSelectionTextStyle` reads the font of the selected text |
| Web link detection (`FPDFLink_LoadWebLinks`)                                                                                                          | No equivalent                                                                                                                                                                          |
| Device-to-page conversion (`FPDF_DeviceToPage`, `FPDF_PageToDevice`)                                                                                  | No equivalent                                                                                                                                                                          |
| Render options: grayscale, LCD text, color schemes, view-only rotation                                                                                | No equivalent                                                                                                                                                                          |
| Printing (`FPDF_PRINTING`, `FPDF_RenderPage` on Windows)                                                                                              | No print API; `PageCanvas` can render pages for your own print layout, drawn as loaded from the file without the session's edits                                                       |
| Path objects (`FPDFPageObj_CreateNewPath`, `FPDFPath_*`) and loading fonts (`FPDFText_LoadFont`)                                                      | No equivalent                                                                                                                                                                          |
| Creating markup annotations other than ink (`FPDFPage_CreateAnnot`)                                                                                   | No equivalent; existing shapes and lines display, and their border color and width can be changed; `useInkAnnotationCreate` creates ink                                                |
| Importing pages from another document, N-up, and page boxes (`FPDF_ImportPages`, `FPDF_ImportNPagesToOne`, `FPDFPage_SetCropBox`)                     | No equivalent; merge on your server with PDFium and load the result                                                                                                                    |
| Flattening (`FPDFPage_Flatten`)                                                                                                                       | No equivalent                                                                                                                                                                          |
| Incremental saves, removing security, and choosing the file version                                                                                   | No equivalent                                                                                                                                                                          |
| Block readers and linearized loading (`FPDF_LoadCustomDocument`, `FPDFAvail_*`)                                                                       | Fetch the whole file; no load progress                                                                                                                                                 |
| JavaScript (`FORM_DoDocumentJSAction`, V8 builds) and XFA (`FPDF_LoadXFA`)                                                                            | No equivalent                                                                                                                                                                          |
| Bookmarks, named-destination lookup, page labels, and metadata (`FPDFBookmark_*`, `FPDF_GetNamedDestByName`, `FPDF_GetPageLabel`, `FPDF_GetMetaText`) | No equivalent                                                                                                                                                                          |
| Attachments (`FPDFDoc_GetAttachment`), signature inspection (`FPDF_GetSignatureCount`)                                                                | No equivalent; `signature` widgets carry a read-only `isCryptoSigned`, and the SDK never signs                                                                                         |
| Tagged structure (`FPDF_StructTree_*`)                                                                                                                | No equivalent                                                                                                                                                                          |
| Embedded thumbnails (`FPDFPage_GetThumbnailAsBitmap`)                                                                                                 | `PageCanvas` renders the page                                                                                                                                                          |

