# Migrating from iText

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



This guide moves the part of an iText Core application that users see and change into a React app built on `@sdk-pdf/react`. iText Core is a Java library that runs on your server and has no viewer: the app fills, stamps, and rearranges a PDF during a request and sends the result to the browser. iText for .NET has the same classes with C# method names, such as `GetNumberOfPages()`, so the mapping applies to it as well. With `@sdk-pdf/react`, the browser shows the document, and users fill forms, edit pages, and save; your Java backend keeps fetching and storing the bytes, and keeps the work only a server does, such as generating documents, signing, and producing PDF/A. 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-itext#overview): how the two integrations differ, and the API map for every iText call that serves the browser.
* [ ] Decide what moves to the browser (showing, filling, page edits, and stamps users ask for) and what [stays on the server](https://sdk.pdf.net/docs/v1/migration/from-itext#features-without-an-equivalent) (generation, signing, compliance).
* [ ] [Install the package and set up `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-itext#install-and-set-up), with a content policy that fits your users.
* [ ] [Load and reset documents](https://sdk.pdf.net/docs/v1/migration/from-itext#load-and-reset-a-document) from your Java endpoint with `loadPdf` and `resetPdf`, including the password flow.
* [ ] [Replace the served PDF and the browser's viewer with `Viewport`](https://sdk.pdf.net/docs/v1/migration/from-itext#display-the-document).
* [ ] [Move page operations from numbers to page ids](https://sdk.pdf.net/docs/v1/migration/from-itext#page-manipulation).
* [ ] [Move form filling from `PdfAcroForm` to widgets](https://sdk.pdf.net/docs/v1/migration/from-itext#forms).
* [ ] [Map stamped text and images to paragraphs and image stamps](https://sdk.pdf.net/docs/v1/migration/from-itext#draw-text-and-images).
* [ ] [Keep merging on the server, and move page ranges and splitting](https://sdk.pdf.net/docs/v1/migration/from-itext#merge-and-split-pages).
* [ ] [Move text extraction and search](https://sdk.pdf.net/docs/v1/migration/from-itext#text-extraction-and-search).
* [ ] [Replace the close-and-return round trip with `prepareDocument`](https://sdk.pdf.net/docs/v1/migration/from-itext#saving-and-downloading).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-itext#errors-and-logging) and [cleanup](https://sdk.pdf.net/docs/v1/migration/from-itext#cleanup-and-lifecycle).
* [ ] [Plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-itext#features-without-an-equivalent).
* [ ] Remove the iText endpoints that only served the browser (per-request filling, stamping, and page operations) and the modules only they used; keep iText where the server still generates, signs, or archives. Test encrypted, malformed, large, and form-heavy PDFs, page operations, and the save round trip, including reading the saved file back with iText.

## Overview [#overview]

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

  **iText Core** is a server-side library:

  * It ships as Maven artifacts under `com.itextpdf`. The `itext-core` POM brings in the kernel (PDF objects, pages, `PdfCanvas`, text extraction), layout (`Document`, `Paragraph`, `Table`, `Image`), forms, sign, pdfa, and pdfua modules, among others. Add-ons cover HTML conversion (pdfHTML), redaction (pdfSweep), and OCR (pdfOCR), which are open source under the AGPL like iText Core, and optimization (pdfOptimizer) and XFA flattening (pdfXFA), which need a commercial license. iText is licensed under the AGPL or commercially; a commercial license is a key file loaded at startup.
  * A `PdfDocument` opens in one of three modes: reading (a `PdfReader`), writing (a `PdfWriter` and a new, empty document), or stamping (both), which reads the original and writes a new file when the document closes. Each request opens, changes, and closes its own document.
  * iText Core doesn't render pages and has no UI. Users see the output in a viewer you don't control, such as the browser's built-in PDF viewer.
  * Pages are 1-based numbers. Positions are user-space units (72 per inch), with the origin usually at the lower-left corner, as set by the page's media box.
  * Failures are exceptions (`PdfException`, `BadPasswordException`, `IOException`), and problems iText works around, such as a damaged cross-reference table, are logged through SLF4J.

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

  * [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the document and its configuration in your React tree, and [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) loads the bytes your server returns.
  * [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages, and users work on them directly: selecting and editing text, filling forms, and placing text and images. The toolbars and panels around it are your React components, driven by hooks.
  * A loaded document lives for the session. Edits don't rewrite the file: each change is an operation in a CRDT, which powers undo and redo, and [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) builds the final PDF when you save.
  * Pages and blocks are addressed by session-scoped ids. Positions are user-space units; widget positions have their origin at the bottom-left corner of the page.

  | Concern           | iText Core                                                        | @sdk-pdf/react                                           |
  | ----------------- | ----------------------------------------------------------------- | -------------------------------------------------------- |
  | Where it runs     | Java (or .NET) on your server                                     | The browser, inside your React app                       |
  | What users see    | The output file, in a viewer you don't control                    | `Viewport`, with your own toolbars and panels            |
  | Document lifetime | Opened and closed within a request                                | Loaded once per session; built into a file when you save |
  | Changes           | Written to a new file when the document closes                    | Recorded as undoable operations                          |
  | Pages             | 1-based numbers                                                   | Session-scoped page ids                                  |
  | Coordinates       | User space, origin set by the media box (usually the bottom left) | User space; widgets measured from the bottom-left corner |
  | Forms             | Fields by full name, string values, flattening                    | Widgets with a `fieldName`, typed values, no flattening  |
  | Adding content    | `PdfCanvas` operators and the layout engine                       | Paragraphs and image stamps by hook or tool, ink by hook |
  | Diagnostics       | Exceptions and SLF4J logs                                         | Typed errors and `logMessages`                           |
  | Licensing         | AGPL, or a commercial license key loaded at startup               | The package from a private registry                      |

  What this means for your code:

  1. **Split the work.** Decide per feature: what users see and change moves to React; generation, signing, compliance, and storage stay on the server. [Features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-itext#features-without-an-equivalent) lists what stays.
  2. **Replace the round trip.** Instead of a request per change that returns a new PDF (fill these fields, rotate this page), the browser loads the bytes once, users make their changes, and the app uploads the result of `prepareDocument()`.
  3. **Keep the endpoints that move bytes.** The endpoint that returned iText output returns the stored file, and another one accepts the saved file. Server-side steps after the upload, such as flattening or signing, keep using iText.
  4. **Replace numbers with ids** in the browser, and convert at the edges with [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages).
  5. **Rethink forms as widgets.** Users fill fields in the viewport, and each widget's value is typed: a string, a boolean, or option indices.
  6. **Choose the content policy.** The default `PDF_EDITOR` makes everything editable; a form-filling app starts with `PDF_VIEWER`.
  7. **Build the viewer UI.** iText has no viewer, so navigation, zoom, thumbnails, selection, and undo and redo have no iText counterpart to migrate: [Building a PDF application](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application) covers them.
</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-itext#features-without-an-equivalent).

  | iText                                                                                  | @sdk-pdf/react                                                                                                                  | Notes                                                                                                                                                                                                                                                                        |
  | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `com.itextpdf:itext-core`, `bouncy-castle-adapter`                                     | `@sdk-pdf/react` from the private registry, `<PdfProviders>`                                                                    | The server keeps its Maven setup.                                                                                                                                                                                                                                            |
  | `LicenseKey.loadLicenseFile(...)`                                                      | Nothing to call                                                                                                                 | `PdfProviders` takes no key.                                                                                                                                                                                                                                                 |
  | `new PdfDocument(new PdfReader(...))`                                                  | `loadPdf(Uint8Array)` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                        | `fetch` the bytes from your server first.                                                                                                                                                                                                                                    |
  | `new ReaderProperties().setPassword(bytes)`                                            | `loadPdf(data, { password })`                                                                                                   |                                                                                                                                                                                                                                                                              |
  | `BadPasswordException`                                                                 | `InvalidPasswordError`                                                                                                          | A rejected `loadPdf` promise.                                                                                                                                                                                                                                                |
  | `pdfDoc.close()` on a document opened for reading                                      | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf)                                                               |                                                                                                                                                                                                                                                                              |
  | `pdfDoc.getNumberOfPages()`                                                            | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                               | `0` while no document is loaded.                                                                                                                                                                                                                                             |
  | `pdfDoc.getPage(pageNumber)`                                                           | `usePages()[pageNumber - 1]`                                                                                                    | Entries carry `id`, `width`, `height`, and `rotation`.                                                                                                                                                                                                                       |
  | `page.getPageSizeWithRotation()`                                                       | `width` and `height` of the [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) entries                                                   | With the rotation applied.                                                                                                                                                                                                                                                   |
  | `page.getRotation()`, `page.setRotation(degrees)`                                      | `rotation`, and `setPageRotation(pageIdOrPageIds, rotation)` from [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation) | Absolute angles in both.                                                                                                                                                                                                                                                     |
  | `pdfDoc.addNewPage(index, pageSize)`                                                   | `createPage(width, height, insertAfterPageId?)` from [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate)                        | Inserts after a page, not at a position.                                                                                                                                                                                                                                     |
  | `pdfDoc.removePage(pageNumber)`                                                        | `deletePages(pageIdOrPageIds)` from [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete)                                         |                                                                                                                                                                                                                                                                              |
  | `pdfDoc.movePage(pageNumber, insertBefore)`                                            | `moveBefore` or `moveAfter` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove)                                                |                                                                                                                                                                                                                                                                              |
  | —                                                                                      | `duplicatePage(sourcePageId, insertAfterPageId?)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate)                |                                                                                                                                                                                                                                                                              |
  | `page.setCropBox()`, `page.setMediaBox()`                                              | **No equivalent**                                                                                                               |                                                                                                                                                                                                                                                                              |
  | `PdfFormCreator.getAcroForm(pdfDoc, true)`, `getAllFormFields()`                       | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets), [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter)                        | Widgets in reading order, not a map of fields.                                                                                                                                                                                                                               |
  | `form.getField(name).setValue(value)`, `getValueAsString()`                            | [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                                                                           | One widget per hook call.                                                                                                                                                                                                                                                    |
  | `TextFormFieldBuilder`, `CheckBoxFormFieldBuilder`, and the other builders, `addField` | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate)                                           |                                                                                                                                                                                                                                                                              |
  | `field.setReadOnly(true)`                                                              | `setWidgetProps(ids, { isReadOnly: true })` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps)                    |                                                                                                                                                                                                                                                                              |
  | `form.renameField(oldName, newName)`                                                   | `setWidgetProps(ids, { fieldName })`                                                                                            |                                                                                                                                                                                                                                                                              |
  | `form.removeField(name)`                                                               | `deleteBlocks(widgetIds)` from [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete)                                            | Deleting a widget keeps its stored field value.                                                                                                                                                                                                                              |
  | `form.flattenFields()`, `partialFormFlattening(name)`                                  | **No equivalent**                                                                                                               | Flatten on the server after the upload.                                                                                                                                                                                                                                      |
  | `PdfCanvas` `beginText()`, `moveText()`, `showText()`                                  | `createParagraph(pageId, x, y)` from [`useParagraphCreate`](https://sdk.pdf.net/docs/v1/reference/useParagraphCreate), `getParagraphEditor`        | Or the `addText` tool.                                                                                                                                                                                                                                                       |
  | `PdfCanvas.addImageFittedIntoRectangle()`, `addImageAt()`, layout `Image`              | [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate) with [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage)    | Places the image as a stamp annotation at given coordinates; the `{ type: 'addImage', image }` tool places it where the user clicks.                                                                                                                                         |
  | `PdfCanvas` paths, `PdfExtGState`                                                      | **No equivalent**                                                                                                               | Freehand strokes can become an ink annotation with [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate).                                                                                                                                                    |
  | Creating text and link annotations (`PdfTextAnnotation`, `PdfLinkAnnotation`)          | **No equivalent**                                                                                                               | Stamps and ink are the annotations the SDK creates. Existing annotations are drawn and, under `PDF_EDITOR`, editable; [`useSelectionAnnotationProps`](https://sdk.pdf.net/docs/v1/reference/useSelectionAnnotationProps) changes the border color and width of selected shapes, lines, and ink. |
  | `PdfMerger.merge()`, `copyPagesTo()` into another document                             | **No equivalent**                                                                                                               | Merge on the server and load the result.                                                                                                                                                                                                                                     |
  | `copyPagesTo()` for one page range                                                     | `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                 | Saves only the listed pages, in the listed order. See [Merge and split pages](https://sdk.pdf.net/docs/v1/migration/from-itext#merge-and-split-pages).                                                                                                                                                                       |
  | `PdfSplitter` (several files)                                                          | One `prepareDocument({ pageIds })` call per part                                                                                | Splitting by size has no equivalent.                                                                                                                                                                                                                                         |
  | `PdfTextExtractor.getTextFromPage(page)`                                               | `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)                          | Paragraph text and bounds.                                                                                                                                                                                                                                                   |
  | `RegexBasedLocationExtractionStrategy`                                                 | [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch)                                                                     | Plain-text queries, no regular expressions; matches carry page and paragraph ids, not rectangles.                                                                                                                                                                            |
  | `new PdfWriter(outputStream)` and `close()`                                            | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                            | Resolves with a `Uint8Array`.                                                                                                                                                                                                                                                |
  | An `application/pdf` response with `Content-Disposition: attachment`                   | `downloadDocument()`                                                                                                            | No file name option.                                                                                                                                                                                                                                                         |
  | `StampingProperties.useAppendMode()`, `WriterProperties.setStandardEncryption()`       | **No equivalent**                                                                                                               |                                                                                                                                                                                                                                                                              |
  | `PdfException`, `IOException`                                                          | The SDK error classes                                                                                                           | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-itext#errors-and-logging).                                                                                                                                                                                                                               |
  | SLF4J logging                                                                          | [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages)                                                                                 | The SDK never prints to the console.                                                                                                                                                                                                                                         |
  | `close()`, try-with-resources                                                          | Nothing to close                                                                                                                | See [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-itext#cleanup-and-lifecycle).                                                                                                                                                                                                                         |
  | `PdfSigner`, `PdfADocument`, `PdfUADocument`, pdfHTML, pdfSweep, pdfOCR                | **No equivalent**                                                                                                               | Keep them on your server.                                                                                                                                                                                                                                                    |
</details>

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

### Before: iText [#before-itext]

iText Core ships as Maven artifacts under `com.itextpdf`: the `itext-core` POM brings in its modules, and the `bouncy-castle-adapter` artifact goes next to it:

```xml
<properties>
  <itext.version>9.8.0</itext.version>
</properties>

<dependencies>
  <dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>${itext.version}</version>
    <type>pom</type>
  </dependency>
  <dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>bouncy-castle-adapter</artifactId>
    <version>${itext.version}</version>
  </dependency>
</dependencies>
```

Under the AGPL, that is the whole setup. A commercial license adds the `com.itextpdf.licensing:licensing-base` artifact from iText's own Maven repository and loads the key file before any other iText call:

```java
// Before the first iText call.
LicenseKey.loadLicenseFile(new File("path/to/itextkey.json"));
```

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

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

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

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

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

Wrap the page that shows documents in [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders). The PDF application re-renders whenever a prop changes identity, so memoize the configuration:

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

export function ContractPage({
  contractId,
  canEdit,
}: {
  contractId: string
  canEdit: boolean
}) {
  const config = useMemo(
    () => ({
      // PDF_VIEWER: fillable widgets and clickable links, read-only text and annotations.
      // Applied when the viewport first mounts; switch later with useSetInteractionMode.
      initialContentPolicy: canEdit
        ? CONTENT_POLICY_PRESET.PDF_EDITOR
        : CONTENT_POLICY_PRESET.PDF_VIEWER,
    }),
    [canEdit]
  )

  return (
    <PdfProviders {...config}>
      <DocumentLoader url={`/api/contracts/${contractId}/file`} />
    </PdfProviders>
  )
}
```

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

* **The server keeps iText.** The Maven setup stays for the work in [Features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-itext#features-without-an-equivalent); the React app needs no Java and no request to the server to change the document.
* **No license key in the browser.** `LicenseKey.loadLicenseFile` has no counterpart: the registry token in `.npmrc` installs the package, and `PdfProviders` takes no key.
* **Pick the content policy deliberately.** `PdfProviders` defaults to [`CONTENT_POLICY_PRESET.PDF_EDITOR`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET), where text, annotations, links, and widgets are all editable. An app that replaces server-side form filling starts with `PDF_VIEWER`, which keeps widgets fillable and links clickable, with read-only text and annotations. Each level can also be a function of the block id, such as `widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive')`, and `'hidden'` doesn't render that kind of content at all; [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) lists the levels.

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

### Before: iText [#before-itext-1]

`PdfReader` reads a file name, a `File`, or an `InputStream`, and `PdfDocument` opens it. `ReaderProperties` carries the password of an encrypted file, which opens with the user or the owner password; stamping it needs the owner password, and with only the user password the `PdfDocument` constructor throws `BadPasswordException`:

```java
byte[] stored = storage.read(documentId);
ReaderProperties properties = new ReaderProperties();
if (password != null) {
    properties.setPassword(password.getBytes(StandardCharsets.UTF_8));
}

try (PdfDocument pdfDoc = new PdfDocument(
        new PdfReader(new ByteArrayInputStream(stored), properties))) {
    int pageCount = pdfDoc.getNumberOfPages();
} catch (BadPasswordException e) {
    // The password is missing or wrong: ask for it, then open the file again.
} catch (IOException e) {
    // Reading the bytes failed.
}
```

Each request opens its own document and closes it when the work is done; no document stays open between requests.

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

`loadPdf` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) takes the file bytes, so the browser fetches them from your Java endpoint first. The document then stays loaded until `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) unloads it, which the loader does before opening a document from another URL:

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

function DocumentLoader({ url }: { url: string }) {
  const [loadState, loadPdf] = useLoadPdf()
  const resetPdf = useResetPdf()
  // The bytes and the URL they came from, kept for a password retry.
  const bytesRef = useRef<{ url: string; data: Uint8Array } | null>(null)
  const [loadedUrl, setLoadedUrl] = useState<string>()
  const [needsPassword, setNeedsPassword] = useState(false)
  const [errorMessage, setErrorMessage] = useState<string>()

  async function open(password?: string) {
    setErrorMessage(undefined)
    try {
      let data = bytesRef.current?.url === url ? bytesRef.current.data : undefined
      if (!data) {
        // The endpoint that used to stream iText output now returns the stored file.
        const response = await fetch(url, { credentials: 'include' })
        if (!response.ok) {
          setErrorMessage(`Loading the document failed with ${response.status}`)
          return
        }
        data = new Uint8Array(await response.arrayBuffer())
        bytesRef.current = { url, data }
      }

      // A document from another URL may still be loaded: unload it, which invalidates its
      // ids.
      if (loadState.status !== 'initial') resetPdf()
      setLoadedUrl(undefined)

      // loadPdf reads from the buffer it receives for the document's lifetime,
      // so keep the original for a retry and pass a copy.
      await loadPdf(data.slice(), { password })
      setLoadedUrl(url)
      setNeedsPassword(false)
    } catch (error) {
      // Replaces catching BadPasswordException.
      if (error instanceof InvalidPasswordError) setNeedsPassword(true)
      else setErrorMessage('The document could not be opened')
    }
  }

  if (needsPassword)
    return <PasswordPrompt onSubmit={(password) => void open(password)} />
  if (loadState.status === 'loading') return <p>Loading the document…</p>
  // Render the application only for the document loaded from the current URL.
  if (loadState.status === 'ready' && loadedUrl === url) return <PdfApplication />

  return (
    <>
      {errorMessage && <p role="alert">{errorMessage}</p>}
      <button onClick={() => void open()}>Open</button>
    </>
  )
}
```

To close the document without opening another one, call `resetPdf()` directly:

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

function CloseDocumentButton() {
  const resetPdf = useResetPdf()

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

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

* **Bytes, not files or streams.** `loadPdf` takes a `Uint8Array`. Authentication, headers, and error responses belong to your own `fetch`.
* **Keep a copy for retries.** The document reads from the given buffer for its whole lifetime, and another `loadPdf` call can detach it. To reuse the bytes for a password retry, pass a copy: `loadPdf(data.slice(), { password })`.
* **Passwords are a rejection.** A missing or wrong password rejects with `InvalidPasswordError`, the counterpart of `BadPasswordException`. Without a password, the empty password is tried, as the PDF specification prescribes. The password now goes from the user to `loadPdf` in the browser: the server sends the encrypted file as stored.
* **Another document.** When the URL changes, the loader shows its Open button again and renders the application only after the new document has loaded; `open()` unloads the previous one with `resetPdf()` first.
* **A session, not a request.** The document stays loaded while users work on it. After `resetPdf()`, page and block ids, widget snapshots, and document revisions from the old document are no longer valid.

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

### Before: iText [#before-itext-2]

iText Core doesn't render pages. An iText backend writes the PDF into the HTTP response, buffered first so the response carries the content length, and the browser shows it in its built-in viewer:

```java
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws IOException {
    byte[] stored = storage.read(request.getParameter("id"));

    ByteArrayOutputStream output = new ByteArrayOutputStream();
    try (PdfDocument pdfDoc = new PdfDocument(
            new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output))) {
        // Per-request changes go here: filled fields, stamps, page operations.
    }

    response.setContentType("application/pdf");
    response.setContentLength(output.size());
    output.writeTo(response.getOutputStream());
}
```

```html
<iframe src="/documents?id=42" title="Contract"></iframe>
```

Every change users ask for, such as a filled field or a rotated page, is another request that runs iText and reloads the frame.

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

[`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages and takes the input; your components render everything around it:

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

function PdfApplication() {
  // Fit the page width once per loaded document.
  useViewportAutoFitScale('width')

  return (
    <div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: '100vh' }}>
      {/* Your toolbar: the page, form, 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>
  )
}
```

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

* **Changes happen in place.** Users fill fields, select text, follow links, and, under `PDF_EDITOR`, edit text and move blocks inside `Viewport`, with no request to the server. The content policy decides what is allowed.
* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded, so render it when the load state is `'ready'`.
* **Previews without server rendering.** [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders one page onto a canvas anywhere outside the main view, lazily and at the canvas's CSS size, for thumbnails and previews. It draws the page as loaded from the file; edits made during the session show in the main view. Previews that show those edits are coming soon.
* **Styling is yours.** The SDK renders unstyled: `theme.classNames`, `theme.styles`, and CSS variables such as `--selection-color` style its elements, as [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization) describes.

## Page manipulation [#page-manipulation]

### Before: iText [#before-itext-3]

`PdfDocument` changes pages by 1-based number, and each `PdfPage` holds its size and rotation:

```java
try (PdfDocument pdfDoc = new PdfDocument(
        new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output))) {
    // getPageSize() ignores the rotation; getPageSizeWithRotation() applies it.
    Rectangle firstPageSize = pdfDoc.getPage(1).getPageSizeWithRotation();

    // Insert a blank A4 page at position 2: the pages after it shift by one.
    pdfDoc.addNewPage(2, PageSize.A4);
    // Move the last page to the front.
    pdfDoc.movePage(pdfDoc.getNumberOfPages(), 1);
    // The rotation is absolute: read the current angle to turn the page a quarter
    // clockwise.
    PdfPage firstPage = pdfDoc.getPage(1);
    firstPage.setRotation((firstPage.getRotation() + 90) % 360);
    // Remove page 3.
    pdfDoc.removePage(3);
}
```

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

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). [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) gives the order and the sizes:

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

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

  const index = pages.findIndex((page) => page.id === pageId)
  if (index === -1) return null
  const page = pages[index]

  return (
    <div role="toolbar" aria-label={`Page ${index + 1} of ${pages.length}`}>
      {/* User-space units with the rotation applied, like getPageSizeWithRotation(). */}
      <span>
        {Math.round(page.width)} × {Math.round(page.height)}
      </span>
      {/* A4 is roughly 595 × 842; the new page goes after this one. */}
      <button onClick={() => createPage(595, 842, pageId)}>
        Insert blank page after
      </button>
      {/* Without a target page, moveBefore inserts at the start of the document. */}
      <button onClick={() => moveBefore(pageId)}>Move to start</button>
      {/* Absolute, as with setRotation: add to the current angle yourself. */}
      <button onClick={() => setPageRotation(pageId, page.rotation + 90)}>
        Rotate clockwise
      </button>
      <button disabled={pages.length <= 1} onClick={() => deletePages(pageId)}>
        Delete
      </button>
    </div>
  )
}
```

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

| Operation | iText                                                                     | @sdk-pdf/react                                                                                                                      |
| --------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Count     | `getNumberOfPages()`                                                      | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount), which re-renders when the count changes                                          |
| Size      | `getPageSize()` without the rotation, `getPageSizeWithRotation()` with it | `width` and `height` of each `usePages()` entry, with the rotation applied                                                          |
| Insert    | `addNewPage(index, pageSize)` at a 1-based position                       | `createPage(width, height, insertAfterPageId?)` after the given page, or at the end; no default size                                |
| Move      | `movePage(pageNumber, insertBefore)`                                      | `moveBefore(ids, beforePageId?)` (the start when omitted) or `moveAfter(ids, afterPageId?)` (the end when omitted)                  |
| Rotate    | `setRotation(degrees)`, absolute; a multiple of 90, not checked           | `setPageRotation(ids, rotation)`, absolute; `-90` and `450` are normalized, and angles off a quarter turn throw `InvalidInputError` |
| Remove    | `removePage(pageNumber)`                                                  | `deletePages(ids)`; an undo can restore the page                                                                                    |
| Duplicate | —                                                                         | `duplicatePage(sourcePageId, insertAfterPageId?)`, asynchronous, after the source by default                                        |

* **Edits, not a new file.** Each call changes the loaded document at once and shows in the viewport; deleting, moving, or rotating several pages in one call is a single undo step. The PDF file is built when you save.
* **Ids instead of numbers.** Page numbers from your server, such as a stored page reference, become ids at the edge with `usePages()[n - 1].id`, and `getPageIndex(pageId)` from [`usePageIndexGetter`](https://sdk.pdf.net/docs/v1/reference/usePageIndexGetter) goes the other way with a zero-based index (add 1 for the page number; `undefined` when the page is gone). Ids are session-scoped: persist numbers, not ids.
* **No cropping or resizing.** `setCropBox()` and `setMediaBox()` have no equivalent.

## Forms [#forms]

### Before: iText [#before-itext-4]

`PdfFormCreator.getAcroForm(pdfDoc, true)` returns the document's `PdfAcroForm`, creating one when the file has none. Fields are looked up by full name, values are strings, and a checkbox takes the name of an appearance state, such as `Yes` or `Off`. New fields come from builders, with a `Rectangle` given by its lower-left corner, width, and height:

```java
try (PdfDocument pdfDoc = new PdfDocument(
        new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output))) {
    PdfAcroForm form = PdfFormCreator.getAcroForm(pdfDoc, true);

    form.getField("customer.name").setValue("Jane Doe");
    form.getField("terms").setValue("Yes");

    PdfTextFormField notes = new TextFormFieldBuilder(pdfDoc, "notes")
            .setWidgetRectangle(new Rectangle(72, 600, 300, 24))
            .createText();
    notes.setMultiline(true);
    form.addField(notes, pdfDoc.getPage(1));

    // Lock a field, or turn every field into page content with form.flattenFields().
    form.getField("customer.name").setReadOnly(true);
}
```

Reading a submitted form is `getAllFormFields()` and `getValueAsString()` on the uploaded file.

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

When the content policy allows interacting with widgets, as `PDF_VIEWER` does, users fill the fields in `Viewport`, with nothing to build. The hooks are for the form UI around it, as [Working with forms](https://sdk.pdf.net/docs/v1/guides/working-with-forms) explains: [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets) lists the widgets, [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue) binds one widget's value, [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate) adds fields, and [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps) locks them:

```tsx
import {
  CheckboxWidget,
  useDocumentSaving,
  useSetWidgetProps,
  useWidgets,
  useWidgetsCreate,
  useWidgetsGetter,
  useWidgetValue,
  Widget,
} from '@sdk-pdf/react'

function isCheckboxWidget(widget: Widget): widget is CheckboxWidget {
  return widget.widgetType === 'checkbox'
}

// The counterpart of getField("terms"): the value is a boolean, not an appearance state
// name.
function TermsCheckbox() {
  const { widgets } = useWidgets()
  const widget = widgets
    .filter(isCheckboxWidget)
    .find((widget) => widget.fieldName === 'terms')

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

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

  return (
    <label>
      <input
        type="checkbox"
        checked={isChecked}
        onChange={(event) => setIsChecked(event.currentTarget.checked)}
      />
      I accept the terms
    </label>
  )
}

// The counterpart of TextFormFieldBuilder and addField(field, page).
function useAddNotesField() {
  const createWidgets = useWidgetsCreate()

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

// Upload the filled form, then lock it: the counterpart of setReadOnly(true) on every
// field.
function useSubmitForm() {
  const getWidgets = useWidgetsGetter()
  const setWidgetProps = useSetWidgetProps()
  const { prepareDocument } = useDocumentSaving()

  return async (url: string) => {
    const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })
    const response = await fetch(url, { method: 'POST', body: bytes as BodyInit })
    // A failed upload leaves the form editable, so the user can try again.
    if (!response.ok)
      throw new Error(`Submitting the form failed with ${response.status}`)

    const widgets = await getWidgets()
    // excludeFromHistory keeps undo from unlocking the submitted form.
    await setWidgetProps(
      widgets.map((widget) => widget.id),
      { isReadOnly: true },
      { excludeFromHistory: true }
    )
  }
}
```

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

| Concept              | iText                                                                                                                                                            | @sdk-pdf/react                                                                                                                                                                             |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Lookup               | `getField(name)`, `getAllFormFields()`                                                                                                                           | `useWidgets()` (re-renders), or `await getWidgets()` from [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter) (one-time, loads every page with widgets first); filter by `fieldName` |
| Shared values        | A field with several widgets                                                                                                                                     | Every widget with the same field name shows and synchronizes the same value                                                                                                                |
| Values               | `setValue(String)`, `getValueAsString()`                                                                                                                         | `useWidgetValue(widget)`: `string` for `text` and `choice`, `boolean` for `checkbox` and `radio`, option indices for `multiChoice`                                                         |
| Checkbox states      | Appearance state names, such as `Yes` and `Off`                                                                                                                  | `true` or `false`; the stored value when checked is the widget's `checkedValue`                                                                                                            |
| Creation             | `TextFormFieldBuilder`, `CheckBoxFormFieldBuilder`, `ChoiceFormFieldBuilder`, `RadioFormFieldBuilder`, `PushButtonFormFieldBuilder`, `SignatureFormFieldBuilder` | `createWidgets(props)` with `widgetType` `text`, `checkbox`, `choice` or `multiChoice`, `radio`, `pushButton`, or `signature`; a unique `fieldName` is generated when omitted              |
| Interactive creation | —                                                                                                                                                                | The `{ type: 'addWidget', widgetProps }` interaction tool, with editable widgets                                                                                                           |
| Read-only            | `setReadOnly(true)`                                                                                                                                              | `setWidgetProps(ids, { isReadOnly: true }, options?)`                                                                                                                                      |
| Rename and remove    | `renameField(oldName, newName)`, `removeField(name)`                                                                                                             | `setWidgetProps(ids, { fieldName })`, and `deleteBlocks(widgetIds)` from [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete), which keeps the stored field value                         |
| Flattening           | `flattenFields()`, `partialFormFlattening(name)`                                                                                                                 | No equivalent                                                                                                                                                                              |

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

* **Coordinates.** A widget's `x`, `y`, `width`, and `height` are user-space units measured from the bottom-left corner of the page. iText's `Rectangle` matches them when the page's media box starts at 0, 0. Check the placement on a test document before converting stored positions.
* **Prefilling can stay on the server.** Filling fields with `setValue()` before sending the file keeps working: users see the values in the viewport and can change them. In the browser, values are set per widget through `useWidgetValue`; there is no setter by field name.
* **Read submitted values on the server.** The SDK reads values per widget, inside components. To process a submitted form, upload the file and read it with `getAllFormFields()` and `getValueAsString()`, as before.
* **No flattening.** `flattenFields()` has no equivalent: flatten on the server after the upload when the stored file must not stay fillable.
* **Read-only fields.** `isReadOnly` restricts filling the field, not authoring the widget; with `{ excludeFromHistory: true }`, an undo can't unlock a submitted form.
* **Date fields.** A text widget's `format` exposes Acrobat date formats as `FormatType.DateTime`, recognized without running scripts; see [`FormatType`](https://sdk.pdf.net/docs/v1/reference/FormatType).
* **Signatures.** `signature` widgets carry a read-only `isCryptoSigned`, and the SDK never signs: keep `PdfSigner` on your server.

## Draw text and images [#draw-text-and-images]

### Before: iText [#before-itext-5]

iText adds content to a page in two ways: `PdfCanvas` writes low-level operators (text objects, images, and paths), and the layout engine positions elements such as `Paragraph` for you. Closing the layout `Document` also closes the `PdfDocument`:

```java
PdfDocument pdfDoc = new PdfDocument(
        new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output));
try (Document document = new Document(pdfDoc)) {
    PdfPage page = pdfDoc.getPage(1);
    Rectangle pageSize = page.getPageSize();

    // A text object at an absolute position.
    PdfCanvas canvas = new PdfCanvas(page);
    canvas.beginText()
            .setFontAndSize(PdfFontFactory.createFont(StandardFonts.HELVETICA), 12)
            .moveText(72, pageSize.getTop() - 72)
            .showText("Reviewed by the legal team")
            .endText();

    // An image fitted into a rectangle given by its lower-left corner, width, and height.
    ImageData logo = ImageDataFactory.create(logoBytes);
    canvas.addImageFittedIntoRectangle(logo, new Rectangle(72, 72, 150, 50), false);

    // A layout paragraph, centered on page 1.
    document.showTextAligned(new Paragraph("DRAFT").setFontSize(48),
            pageSize.getX() + pageSize.getWidth() / 2,
            pageSize.getY() + pageSize.getHeight() / 2, 1,
            TextAlignment.CENTER, VerticalAlignment.MIDDLE, 0);
}
```

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

Text becomes a paragraph, created from code with [`useParagraphCreate`](https://sdk.pdf.net/docs/v1/reference/useParagraphCreate) and filled through [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor), or placed by users with the `addText` tool. An image is staged with [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage) and becomes a stamp annotation, placed from code with [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate) or by users with the `addImage` tool. The tools are editing, so the policy switches with the tool through [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode), here to `PDF_EDITOR`:

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

// The counterpart of beginText(), moveText(x, y), and showText(text).
function useStampText() {
  const createParagraph = useParagraphCreate()
  const { getParagraphEditor } = useProgrammaticEditor()

  return async (pageId: string, x: number, y: number, text: string) => {
    const paragraphId = await createParagraph(pageId, x, y)
    const editor = getParagraphEditor(paragraphId)
    // The paragraph may be gone already: deleted by an undo.
    if (!editor) return

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

// The counterpart of addImageFittedIntoRectangle(logo, new Rectangle(72, 72, 150, 50),
// false).
function useStampLogo() {
  const createStamp = useStampAnnotationCreate()

  return async (pageId: string, logo: Blob) => {
    const image = await stageImage(logo)
    // x and y are the bottom-left corner in user space; with only a width, the height
    // follows the aspect ratio.
    return createStamp({ pageId, image, x: 72, y: 72, width: 150 })
  }
}

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

function StampToolbar({ logo }: { logo: Blob }) {
  const interactionTool = useInteractionTool()
  const setInteractionMode = useSetInteractionMode()

  async function placeLogo() {
    const image = await stageImage(logo)
    // The next click on a page places the image, 150 user-space units wide.
    setInteractionMode(
      { type: 'addImage', image, width: 150 },
      CONTENT_POLICY_PRESET.PDF_EDITOR
    )
  }

  return (
    <div role="toolbar">
      {/* The next click creates a paragraph and focuses it. */}
      <button
        aria-pressed={interactionTool.type === 'addText'}
        onClick={() => setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Add text
      </button>
      <button
        aria-pressed={interactionTool.type === 'addImage'}
        onClick={() => placeLogo().catch(showError)}
      >
        Place logo
      </button>
    </div>
  )
}
```

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

* **Text is a paragraph, not operators.** Text from `createParagraph` or the `addText` tool is a block that users can select, edit, and move. Programmatic edits flow through the same document as user edits, as [Programmatic editing](https://sdk.pdf.net/docs/v1/guides/programmatic-editing) shows.
* **Styling goes through the selection.** `createParagraph(pageId, x, y)` takes no font or size, unlike `setFontAndSize()`. Users style text with a toolbar built on [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle): font family, size, color, bold, italic, alignment, and spacing.
* **Editing existing text is new.** Under `PDF_EDITOR`, users edit page text in place, and `getParagraphEditor(paragraphId)` rewrites a paragraph from code.
* **Images are stamp annotations.** `stageImage(blob)` validates and measures the image. The size goes on the placement, `width` and `height` in user-space units, for both `createStamp` and the `addImage` tool: a single dimension keeps the aspect ratio, and with neither the image keeps its natural size, fit to the page. SVGs are accepted and rasterized when the PDF is saved.
* **Freehand ink.** [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from strokes your code collects; there is no drawing tool.
* **Other drawing and every-page stamps stay on the server.** Paths and shapes, `PdfExtGState` opacity, text and link annotations, and watermarks have no equivalent: add them with iText after the upload. A plain-text header, footer, or page number is a `createParagraph` call per page.

## Merge and split pages [#merge-and-split-pages]

### Before: iText [#before-itext-6]

`PdfMerger` appends page ranges from source documents to a target, and `copyPagesTo` copies an inclusive, 1-based range into another document, which is how a range becomes its own file. Source documents stay open unless `setCloseSourceDocuments(true)`:

```java
ByteArrayOutputStream merged = new ByteArrayOutputStream();
try (PdfDocument result = new PdfDocument(new PdfWriter(merged));
     PdfDocument cover = new PdfDocument(
             new PdfReader(new ByteArrayInputStream(coverBytes)));
     PdfDocument contract = new PdfDocument(
             new PdfReader(new ByteArrayInputStream(contractBytes)))) {
    PdfMerger merger = new PdfMerger(result);
    merger.merge(cover, 1, 1);
    merger.merge(contract, 1, contract.getNumberOfPages());
}

ByteArrayOutputStream excerpt = new ByteArrayOutputStream();
try (PdfDocument source = new PdfDocument(
        new PdfReader(new ByteArrayInputStream(contractBytes)));
     PdfDocument part = new PdfDocument(new PdfWriter(excerpt))) {
    // Pages 2 to 4, inclusive.
    source.copyPagesTo(2, 4, part);
}
```

`PdfSplitter` cuts a document into several by page numbers, by page count, or by size.

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

`@sdk-pdf/react` works on one loaded document, so merging stays on the server. A page range becomes its own file in the browser: `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) saves only the listed pages, and leaves the loaded document as it is:

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

// The browser-side counterpart of copyPagesTo(from, to, part): 1-based and inclusive.
function useSavePageRange() {
  const pages = usePages()
  const { prepareDocument } = useDocumentSaving()

  return async (from: number, to: number) => {
    if (from < 1 || to > pages.length || from > to)
      throw new RangeError('The page range is outside the document')

    const pageIds = pages.slice(from - 1, to).map((page) => page.id)
    return prepareDocument({ pageIds })
  }
}
```

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

* **No merging.** `PdfMerger` and `copyPagesTo` into another document have no equivalent, and neither has inserting pages from another file. Merge with iText on the server, then load the result with `loadPdf`.
* **Splitting is one save per part.** For `PdfSplitter`'s several output documents, call `prepareDocument({ pageIds })` once per part. The saved file has the pages in the order of `pageIds`, each page at most once. Splitting by file size has no equivalent.
* **Within the document.** `duplicatePage(sourcePageId, insertAfterPageId?)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate) copies a page with all its content, and [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove) reorders pages, as in [Page manipulation](https://sdk.pdf.net/docs/v1/migration/from-itext#page-manipulation).

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

### Before: iText [#before-itext-7]

`PdfTextExtractor` reads a page's text through an extraction strategy, a new one per page; `LocationTextExtractionStrategy` is the default:

```java
try (PdfDocument pdfDoc = new PdfDocument(
        new PdfReader(new ByteArrayInputStream(stored)))) {
    StringBuilder text = new StringBuilder();
    for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) {
        text.append(PdfTextExtractor.getTextFromPage(
                pdfDoc.getPage(i), new LocationTextExtractionStrategy()));
        text.append('\n');
    }
}
```

`RegexBasedLocationExtractionStrategy` finds text by pattern, with its locations.

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

[`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) reads the text as paragraphs: `getParagraphsByPage(pageId)` for one page, or `getAllParagraphIds()` and `getPlainTextParagraphs(ids)` for the whole document:

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

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

  return async () => {
    // Loads every page in turn, which takes a while on a large document.
    const paragraphIds = await getAllParagraphIds()
    const paragraphs = await getPlainTextParagraphs(paragraphIds)

    return paragraphs
      .map((paragraph) => `[page ${paragraph.pageNumber}] ${paragraph.content}`)
      .join('\n\n')
  }
}
```

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

* **Paragraphs, not strategies.** Each entry carries `id`, `content`, the 1-based `pageNumber`, the user-space box `bounds` (`{ x, y, width, height }`), and the paragraph layout, such as `textAlign` and `lineHeight`. There are no extraction strategies or text chunks to choose.
* **One page when you can.** `getAllParagraphIds()` loads every page sequentially; when the page is known, `getParagraphsByPage(pageId)` is faster.
* **Search for users.** [`useDocumentSearch`](https://sdk.pdf.net/docs/v1/reference/useDocumentSearch) finds a plain-text query, with `isCaseSensitive` and `isWholeWord` options, highlights the matches on the pages, and scrolls to the active one. It takes no regular expressions, and a match carries its `pageId`, `paragraphId`, and `text` rather than the rectangles of `RegexBasedLocationExtractionStrategy`. For a pattern, match `content` yourself and scroll to the paragraph with `scrollToBlock(paragraphId)` from [`useScrollToBlock`](https://sdk.pdf.net/docs/v1/reference/useScrollToBlock).
* **Indexing stays on the server.** Extraction for search indexes, and other jobs without a user, keeps running with iText on the stored files.

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

### Before: iText [#before-itext-8]

Closing a stamped `PdfDocument` writes the new file into the `PdfWriter`'s stream. With `StampingProperties.useAppendMode()`, the changes follow the original bytes as an incremental update. A download is the same bytes with an attachment header:

```java
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (PdfDocument pdfDoc = new PdfDocument(
        new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output),
        new StampingProperties().useAppendMode())) {
    PdfFormCreator.getAcroForm(pdfDoc, true).getField("status").setValue("Approved");
}
storage.save(documentId, output.toByteArray());

response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=\"contract.pdf\"");
output.writeTo(response.getOutputStream());
```

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

[`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) builds the PDF from the loaded document and the recorded changes. [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) changes with every edit, and undo and redo bring back the revision of that point in the history: keep the revision the server has, and a different current revision means unsaved changes. The save reads the revision it starts from with [`useDocumentRevisionGetter`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevisionGetter) and records it after the server accepts the file:

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

function SaveControls({ url }: { url: 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()

  // Undoing back to the saved state clears it too.
  const hasUnsavedChanges = revision !== savedRevision

  async function save() {
    if (isSavingRef.current) return
    isSavingRef.current = true
    try {
      // Edits made after the save starts may be missing from the saved bytes, so only
      // this revision becomes saved.
      const startRevision = getRevision()
      // The buffer goes to fetch, so don't hand out the loaded document's original
      // buffer.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })
      const response = await fetch(url, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/pdf' },
        body: bytes as BodyInit,
      })
      // Keep the unsaved state when the server rejects the file.
      if (!response.ok) throw new Error(`Saving failed with ${response.status}`)

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

  return (
    <div role="toolbar">
      <button disabled={!hasUnsavedChanges} 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-8]

* **The server receives a complete PDF.** `prepareDocument()` resolves with the file's bytes as a `Uint8Array`. The Java endpoint stores them, or opens them with `PdfReader` for server-side steps such as flattening or signing.
* **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, or the loaded document's buffer is detached and later reads throw.
* **Save options.** Besides `returnOriginalIfUnchanged`, `pageIds` saves only the listed pages, as in [Merge and split pages](https://sdk.pdf.net/docs/v1/migration/from-itext#merge-and-split-pages). There is no append mode, encryption, compression setting, or download file name. Encrypt or compress on the server when the stored file needs it.
* **Downloads stay in the browser.** `downloadDocument()` builds the same bytes and saves them on the user's device, with no request to the server.

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

### Before: iText [#before-itext-9]

iText throws `BadPasswordException` for a missing or wrong password, and other problems as `PdfException`; both are unchecked, while the `PdfReader` constructors throw the checked `IOException`. Problems it works around, such as a damaged cross-reference table it rebuilds, go to SLF4J, so the app supplies the binding, such as Logback:

```java
try (PdfDocument pdfDoc = new PdfDocument(
        new PdfReader(new ByteArrayInputStream(stored)))) {
    if (pdfDoc.getReader().hasRebuiltXref()) {
        metrics.increment("pdf.repaired");
    }
} catch (BadPasswordException e) {
    // A missing or wrong password.
} catch (PdfException e) {
    // A file iText can't process.
} catch (IOException e) {
    // Reading the bytes failed.
}
```

```xml
<!-- logback.xml: iText's loggers are named after its classes, under com.itextpdf. -->
<logger name="com.itextpdf" level="WARN" />
```

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

Failures are typed errors from the call that failed, and problems the SDK works around are log messages you subscribe to with [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages). [Error handling and logging](https://sdk.pdf.net/docs/v1/guides/error-handling-and-logging) has the full rules. Forwarding the browser's messages to your server keeps them next to the iText logs:

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

function useForwardSdkLogs(url: string) {
  useEffect(() => {
    // The SDK never prints to the console: subscribe, and unsubscribe on unmount.
    const unsubscribe = logMessages.on('message', (logMessage) => {
      if (logMessage.level < LogLevel.Warning) return

      // The message is static, so it groups cleanly in the server's logs.
      navigator.sendBeacon(
        url,
        JSON.stringify({
          level: logMessage.level,
          reason: logMessage.reason,
          message: logMessage.message,
        })
      )
    })
    return () => {
      unsubscribe()
    }
  }, [url])
}
```

Catch load failures where you call `loadPdf`, as `DocumentLoader` does in [Load and reset a document](https://sdk.pdf.net/docs/v1/migration/from-itext#load-and-reset-a-document).

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

| iText                                                                                 | @sdk-pdf/react                                                                                                                                          |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BadPasswordException`                                                                | `InvalidPasswordError`: the document is encrypted, and the password is missing or wrong                                                                 |
| `PdfException` for a file that can't be processed                                     | `DocumentFormatError`: the PDF is malformed beyond what the SDK can work around; `NotSupportedError`: it uses a PDF feature the SDK doesn't support yet |
| `IOException` from `PdfReader`                                                        | Errors from your own `fetch`                                                                                                                            |
| Invalid arguments, such as `IndexOutOfBoundsException` for a page number out of range | `InvalidInputError`                                                                                                                                     |
| —                                                                                     | `DocumentNotLoadedError`: an operation needs a document, and none is loaded                                                                             |
| A cross-reference table iText rebuilds, logged at error level                         | A log message whose `reason` is a document format issue                                                                                                 |

* **Nothing half-applied.** Commands validate first and throw before touching anything; functions that only read return empty values instead of throwing; hooks never throw during render.
* **Every error class** extends `GenericError` and carries a static `message` and an optional `data` object; `UninitializedError` covers objects used before their asynchronous initialization completes.
* **Logs are opt-in.** Each message carries `level` (`LogLevel.Debug`, `Info`, `Warning`, or `Error`), `reason`, a static `message`, `data`, and `stack`. There are no loggers to configure: filter by `level` in your subscriber, where Logback filtered by logger.

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

### Before: iText [#before-itext-10]

`PdfDocument` is `AutoCloseable`. `close()` writes the output and closes the reader and the writer, unless `setCloseReader(false)` or `setCloseWriter(false)` keeps them open; after it, calls on the document throw `PdfException`. `PdfMerger.close()` closes the target document:

```java
PdfDocument pdfDoc = new PdfDocument(new PdfReader(input), new PdfWriter(output));
// Keep the caller's output stream open after close().
pdfDoc.setCloseWriter(false);
try {
    // Work with the document.
} finally {
    // Writes the file; later calls on pdfDoc throw PdfException.
    pdfDoc.close();
}
```

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

* **Nothing to close.** The document lives in `PdfProviders` for the session; there is no `close()` and no stream to manage.
* **Subscriptions follow components.** [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) subscribes for the component's lifetime, with callbacks that don't need memoizing. [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) installs window-level shortcuts, and passing a falsy value instead of the mapping disables them; memoize the mapping, or the listener re-installs on every render.
* **Log subscriptions** return an unsubscribe function: call it in the effect cleanup, as in [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-itext#errors-and-logging).
* **Unloading a document** is `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf), which returns the load state to `'initial'` and invalidates the old document's ids, snapshots, and revisions.
* **Buffers.** Don't reuse a buffer passed to `loadPdf`; pass a copy instead.

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

Plan around these before you migrate. Most of them keep working where they are: the server opens the file that `prepareDocument()` produced and continues with iText.

| iText feature                                                                                            | In @sdk-pdf/react                                                                                                   |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Generating documents with the layout engine (`Document`, `Paragraph`, `Table`), barcodes, and vector SVG | No equivalent: generate on the server and load the result; SVG images placed with `addImage` are rasterized on save |
| HTML to PDF (pdfHTML, `HtmlConverter`)                                                                   | No equivalent: keep it on your server                                                                               |
| Digital signatures (`PdfSigner`) and signature validation                                                | No equivalent; `signature` widgets show `isCryptoSigned`, and the SDK never signs                                   |
| PDF/A, PDF/UA, and Well-Tagged PDF documents (`PdfADocument`, `PdfUADocument`, `WellTaggedPdfDocument`)  | No equivalent: keep them on your server                                                                             |
| Encryption (`setStandardEncryption`, `setPublicKeyEncryption`), compression settings, pdfOptimizer       | No equivalent: apply them on the server after the upload                                                            |
| Append mode (`useAppendMode()`)                                                                          | No save option for it                                                                                               |
| Redaction (pdfSweep) and OCR (pdfOCR)                                                                    | No equivalent                                                                                                       |
| Form flattening (`flattenFields`) and XFA flattening (pdfXFA)                                            | No equivalent: flatten on the server                                                                                |
| Merging and copying pages between documents (`PdfMerger`, `copyPagesTo`)                                 | No equivalent: merge on the server and load the result                                                              |
| Paths, shapes, opacity (`PdfExtGState`), and watermarks                                                  | No equivalent; freehand strokes can become ink with `useInkAnnotationCreate`                                        |
| Creating text and link annotations (`PdfTextAnnotation`, `PdfLinkAnnotation`)                            | No equivalent; the border color and width of selected shapes, lines, and ink can be changed                         |
| Outlines (`PdfOutline`) and document metadata (`PdfDocumentInfo`)                                        | No equivalent                                                                                                       |
| Regular-expression search and match rectangles (`RegexBasedLocationExtractionStrategy`)                  | `useDocumentSearch` finds plain text; match a pattern in paragraph `content` yourself                               |
| Cropping and resizing pages (`setCropBox`, `setMediaBox`)                                                | No equivalent                                                                                                       |
| Batch jobs without a user, such as indexing or conversion pipelines                                      | Keep them on your server                                                                                            |

