# Migrating from pdf-lib

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



This guide moves the browser side of a pdf-lib integration to `@sdk-pdf/react`. pdf-lib loads, creates, modifies, and saves PDF bytes in any JavaScript runtime, and has no rendering or UI: apps show its output in an `<iframe>` or another viewer. `@sdk-pdf/react` is a React library for the browser that shows the document, lets users edit it, and builds the final PDF when you save. The guide covers the parts users see and change; pdf-lib code that runs without a browser, such as document generation on a server, has no equivalent and can stay where it runs. 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-pdf-lib#overview): how the two integrations differ, and the API map for every pdf-lib call you use.
* [ ] Sort your pdf-lib code by where it runs: the browser code that shows and changes documents for users moves; server jobs stay (see [Features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#features-without-an-equivalent)).
* [ ] [Install the package and set up `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#install-and-set-up) with the content policy your users need.
* [ ] [Load and reset documents](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#load-and-reset-a-document) with `loadPdf` and `resetPdf`, including the password flow for encrypted files.
* [ ] [Replace the Blob URL and `<iframe>` preview with `Viewport`](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#display-the-document).
* [ ] [Move page operations from indices to page ids](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#pages).
* [ ] [Move splitting to `prepareDocument` and plan for merging](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#merge-and-split-documents).
* [ ] [Move form filling and field creation to widgets](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#forms).
* [ ] [Replace `drawText` and `drawImage` with paragraphs and image stamps](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#draw-text-and-images).
* [ ] [Replace `save()` and Blob downloads with `useDocumentSaving`](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#saving-and-downloading).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#errors-and-logging), and [plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#features-without-an-equivalent).
* [ ] Remove `pdf-lib` and `@pdf-lib/fontkit` from the browser bundle, unless you keep merging there, along with the Blob URL and `<iframe>` code. Test encrypted, malformed, large, and form-heavy documents, page operations, and the save round-trip.

## Overview [#overview]

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

  **pdf-lib** creates and modifies PDF files in any JavaScript runtime (browsers, Node, Deno, and React Native) and has no rendering or UI. An integration built on it works on bytes, step by step:

  * `PDFDocument.load()` parses a `Uint8Array`, an `ArrayBuffer`, or a base64 string into an in-memory document; `PDFDocument.create()` starts an empty one.
  * Pages are `PDFPage` objects at zero-based indices: `getPage(index)`, `insertPage(index)`, `removePage(index)`.
  * New content is drawn onto pages with `drawText`, `drawImage`, and vector drawing calls. Text already on a page can't be read or edited; only form field values can.
  * Forms are reached through `getForm()`, with fields looked up by fully qualified name.
  * `save()` serializes the document to a `Uint8Array`. To show the result, apps hand the bytes to an `<iframe>` or a viewer library, and save again after every change.
  * Encrypted documents aren't supported: `load()` throws `EncryptedPDFError`.

  **@sdk-pdf/react** owns the document, and you own the UI:

  * [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the document and the configuration; every hook and component renders inside it.
  * [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) loads a document from bytes, encrypted documents included.
  * [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) shows the pages and handles selection, text editing, form filling, and zoom on its own; changes appear as they happen.
  * Pages, paragraphs, and widgets are addressed by session-scoped ids.
  * 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 the bytes.

  | Concern         | pdf-lib                                                    | @sdk-pdf/react                                                  |
  | --------------- | ---------------------------------------------------------- | --------------------------------------------------------------- |
  | Runtime         | Any JavaScript runtime: browsers, Node, Deno, React Native | The browser, in a React app                                     |
  | Style           | `PDFDocument` objects and promises                         | A React provider, state hooks, and command hooks                |
  | Display         | None: saved bytes go to an `<iframe>` or a viewer library  | `Viewport` for the main view, `PageCanvas` for thumbnails       |
  | Page addressing | Zero-based indices                                         | Session-scoped page ids from `usePages`                         |
  | Page text       | Drawn onto pages; existing text can't be read or edited    | Paragraphs that users edit in place and code reads and rewrites |
  | Forms           | Fields by fully qualified name                             | Widgets with a `fieldName`                                      |
  | Change history  | None                                                       | Undo/redo and document revisions                                |
  | Encrypted files | Not supported (`EncryptedPDFError`)                        | Opened with the password                                        |
  | New documents   | `PDFDocument.create()`                                     | Load an existing PDF                                            |
  | Output          | `save()`, `saveAsBase64()`                                 | `prepareDocument()`, `downloadDocument()`                       |
  | Diagnostics     | Thrown errors                                              | Typed errors thrown before any change; `logMessages`            |

  What this means for your code:

  1. **Split by runtime.** Code that runs in Node, Deno, or React Native, such as generating or merging documents on a server, has no equivalent and stays where it is. The browser code that shows documents to users and changes them moves to `@sdk-pdf/react`.
  2. **Drop the save-to-preview loop.** Calling `save()`, building a Blob URL, and reloading an `<iframe>` after each change become one `Viewport` that shows edits live.
  3. **Replace indices with ids.** Every page reference in your state becomes a page id; convert at the edges (deep links, stored page numbers) with `usePages`.
  4. **Let users do the editing.** Hard-coded `drawText` and `drawImage` calls often become user actions: the `addText` and `addImage` tools, typing in existing paragraphs, and filling fields in place. Code that still writes text uses the programmatic editor, and code that still places images creates stamp annotations.
  5. **Rethink forms as widgets.** Field lookups by name become widget lists, and values are read and written through `useWidgetValue` in components.
  6. **Choose the content policy.** It decides what users may change: everything by default (`PDF_EDITOR`), or form fields only (`PDF_VIEWER`).
  7. **Move into React.** Async functions that drive a `PDFDocument` become hooks and components under `PdfProviders`.

  pdf-lib has no viewer, so zoom, thumbnails, and undo have no pdf-lib counterpart and no section in this guide. `@sdk-pdf/react` gives your users all three: zoom gestures and [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale), thumbnails with [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas), and undo and redo with [`useUndoRedo`](https://sdk.pdf.net/docs/v1/reference/useUndoRedo). [Building a PDF application](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application) shows each part. There is no cleanup and lifecycle section either: pdf-lib has nothing to destroy, and in `@sdk-pdf/react`, `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) closes a document.
</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-pdf-lib#features-without-an-equivalent).

  | pdf-lib                                                                                              | @sdk-pdf/react                                                                                                                             | Notes                                                                                |
  | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
  | `npm install pdf-lib`, `@pdf-lib/fontkit`, `registerFontkit()`                                       | The package and [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders)                                                                          | No font setup.                                                                       |
  | `PDFDocument.load(bytes)`                                                                            | `loadPdf(data)` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                                         | `loadPdf` takes a `Uint8Array`.                                                      |
  | `PDFDocument.load()` on an encrypted file, `EncryptedPDFError`, `ignoreEncryption`                   | `loadPdf(data, { password })`, catch `InvalidPasswordError`                                                                                | Encrypted documents open with the password.                                          |
  | `PDFDocument.create()`                                                                               | **No equivalent**                                                                                                                          | Load a PDF file, such as a blank template.                                           |
  | Loading another document                                                                             | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf), then `loadPdf`                                                          |                                                                                      |
  | `save()` into a Blob URL or `saveAsBase64({ dataUri: true })` for an `<iframe>`                      | [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport)                                                                                                  | Shows changes live, with no save.                                                    |
  | `pdfDoc.getPageCount()`                                                                              | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                          | `0` while no document is loaded.                                                     |
  | `pdfDoc.getPages()`, `getPage(index)`                                                                | [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages), `usePages()[index].id`                                                                          | Entries carry `id`, `width`, `height`, and `rotation`.                               |
  | `page.getSize()`, `page.getRotation()`                                                               | `width`, `height`, and `rotation` of a `usePages` entry                                                                                    | Sizes have the rotation applied.                                                     |
  | `addPage([width, height])`, `addPage(PageSizes.A4)`                                                  | `createPage(width, height)` from [`usePageCreate`](https://sdk.pdf.net/docs/v1/reference/usePageCreate)                                                       | Appends; the size is required.                                                       |
  | `insertPage(index, size)`                                                                            | `createPage(width, height, insertAfterPageId)`                                                                                             | Inserts after a page id.                                                             |
  | `removePage(index)`                                                                                  | `deletePages(pageIdOrIds)` from [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete)                                                        |                                                                                      |
  | `page.setRotation(degrees(angle))`                                                                   | `setPageRotation(pageIdOrIds, angle)` from [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation)                                   | A plain number of degrees.                                                           |
  | —                                                                                                    | `moveBefore`, `moveAfter` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove)                                                             |                                                                                      |
  | —                                                                                                    | `duplicatePage(sourcePageId, insertAfterPageId?)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate)                           | Copies a page within the loaded document.                                            |
  | `copyPages(srcDoc, indices)` into a new document, to split                                           | `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                            | Saves only the listed pages, in the listed order.                                    |
  | `copyPages(srcDoc, indices)` from another document, to merge                                         | **No equivalent**                                                                                                                          | See [Merge and split documents](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#merge-and-split-documents).                         |
  | `embedPdf()`, `embedPage()`, `page.drawPage()`                                                       | **No equivalent**                                                                                                                          |                                                                                      |
  | `setSize()`, `setMediaBox()`, `setCropBox()`, `scale()`, `scaleContent()`                            | **No equivalent**                                                                                                                          |                                                                                      |
  | `form.getFields()`                                                                                   | [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets), [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter)                                   | Widgets in reading order, not fields.                                                |
  | `getTextField(name).setText()`, `getText()`                                                          | [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue) on a `text` widget                                                                   | One widget per hook call.                                                            |
  | `getCheckBox(name).check()`, `uncheck()`, `isChecked()`                                              | `useWidgetValue` on a `checkbox` widget                                                                                                    | A `boolean`.                                                                         |
  | `getRadioGroup(name).select(option)`, `getDropdown()`, `getOptionList()`                             | `useWidgetValue` on `radio`, `choice`, and `multiChoice` widgets                                                                           | See [Forms](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#forms).                                                                 |
  | `createTextField(name)` and the other `create` methods, `addToPage()`, `addOptionToPage()`           | `createWidgets(props)` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate)                                                      | One widget per props object.                                                         |
  | `field.enableReadOnly()`                                                                             | `setWidgetProps(ids, { isReadOnly: true }, { excludeFromHistory: true })` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps) |                                                                                      |
  | `setMaxLength()`, `enableMultiline()`, `enableCombing()`, `setAlignment()`, `setFontSize()`          | `maxLength`, `isMultiline`, `isMonospaced`, `horizontalAlign`, `fontSize` widget props                                                     |                                                                                      |
  | `form.removeField(field)`                                                                            | `deleteBlocks(widgetIds)` from [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete)                                                       | The stored field value stays.                                                        |
  | `form.flatten()`                                                                                     | **No equivalent**                                                                                                                          | Lock widgets with `isReadOnly`.                                                      |
  | `setImage()` on text fields and buttons                                                              | **No equivalent**                                                                                                                          |                                                                                      |
  | `page.drawText(text, { x, y })`                                                                      | `createParagraph(pageId, x, y)` from [`useParagraphCreate`](https://sdk.pdf.net/docs/v1/reference/useParagraphCreate), then `insertText(text)`                | An editable paragraph.                                                               |
  | `drawText` options `maxWidth`, `lineHeight`                                                          | `setParagraphProps([paragraphId], { width, lineHeight })` from [`useSetParagraphProps`](https://sdk.pdf.net/docs/v1/reference/useSetParagraphProps)           | Also sets alignment, indent, and rotation.                                           |
  | —                                                                                                    | `getParagraphsByPage(pageId)` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)                                     | Reads the text already on a page.                                                    |
  | `embedFont()`, `widthOfTextAtSize()`, `heightAtSize()`                                               | **No equivalent**                                                                                                                          | Fonts come from [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle). |
  | `embedPng()`, `embedJpg()`, `page.drawImage()`                                                       | [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage), then [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate)              | A stamp annotation; or the user places it with the `addImage` tool.                  |
  | `drawRectangle()`, `drawLine()`, `drawCircle()`, `drawEllipse()`, `drawSvgPath()`, `pushOperators()` | **No equivalent**                                                                                                                          |                                                                                      |
  | `pdfDoc.save()`                                                                                      | `prepareDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                                                       | Resolves with a `Uint8Array`.                                                        |
  | A Blob and a temporary download link                                                                 | `downloadDocument()`                                                                                                                       | No file name option.                                                                 |
  | `saveAsBase64()`, `SaveOptions`                                                                      | **No equivalent**                                                                                                                          | The save options are `returnOriginalIfUnchanged` and `pageIds`.                      |
  | `setTitle()`, `getTitle()`, and the other metadata methods                                           | **No equivalent**                                                                                                                          |                                                                                      |
  | `catalog.getOrCreateViewerPreferences()`, `attach()`, `addJavaScript()`                              | **No equivalent**                                                                                                                          |                                                                                      |
  | `EncryptedPDFError` and other thrown errors                                                          | `InvalidPasswordError` and the other SDK error classes, [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages)                                    | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#errors-and-logging).                                       |
</details>

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

### Before: pdf-lib [#before-pdf-lib]

pdf-lib installs as `pdf-lib`. Custom fonts need a second package, `@pdf-lib/fontkit`, registered on each document before a font is embedded:

```sh
npm install --save pdf-lib @pdf-lib/fontkit
```

```js
import { PDFDocument } from 'pdf-lib'
import fontkit from '@pdf-lib/fontkit'

const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)
```

Without a bundler, the UMD build exposes the same API on a global `PDFLib`.

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

Then wrap the part of your app that shows documents in [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders). The configuration decides what users may do: an editor starts with everything editable, and a form-filling app with read-only text and fillable forms. Keep its identity stable: define it outside the component and read the prop once:

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

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

const editorConfig = {
  initialInteractionTool: selectTool,
  initialContentPolicy: CONTENT_POLICY_PRESET.PDF_EDITOR,
}
const formFillerConfig = {
  initialInteractionTool: textSelectTool,
  initialContentPolicy: CONTENT_POLICY_PRESET.PDF_VIEWER,
}

export function App({ canEditContent }: { canEditContent: boolean }) {
  // Read the prop once: the initial values apply when the viewport first mounts, and a
  // prop that changes identity re-renders the PDF application. Switch later with
  // useSetInteractionMode.
  const [config] = useState(() => (canEditContent ? editorConfig : formFillerConfig))

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

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

* **Browser and React only.** The SDK renders React components in the browser. Node, Deno, and React Native code that uses pdf-lib has no counterpart here.
* **No font setup.** There is no fontkit to register and no font file to embed. The fonts users can pick are the `fontFamily.options` of [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle): `regular` families, always available, and `extracted` ones, taken from the document.
* **Pick the content policy.** `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. `PDF_VIEWER` keeps text and annotations read-only and lets users fill forms; `PDF_PRINTER` makes everything read-only, with links still clickable. The initial values apply when the viewport first mounts; switch later with [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode).

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

### Before: pdf-lib [#before-pdf-lib-1]

`PDFDocument.load()` takes a `Uint8Array`, an `ArrayBuffer`, or a base64 string or data URI. pdf-lib can't decrypt documents: an encrypted file makes `load()` throw `EncryptedPDFError`:

```js
import { EncryptedPDFError, PDFDocument } from 'pdf-lib'

async function openFile(file) {
  try {
    return await PDFDocument.load(await file.arrayBuffer())
  } catch (error) {
    if (error instanceof EncryptedPDFError) {
      showError('Encrypted documents are not supported')
      return null
    }
    throw error
  }
}
```

The `ignoreEncryption: true` load option opens an encrypted file without decrypting it, so later changes may fail or behave unexpectedly. To work on another document, you load another `PDFDocument`.

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

`loadPdf(pdfFileContent: Uint8Array, options?: { password?: string }): Promise<void>` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) loads the document and drives the load state. An encrypted file opens once the user enters its password:

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

function DocumentLoader() {
  const [loadState, loadPdf] = useLoadPdf()
  const bytesRef = useRef<Uint8Array | null>(null)
  const [isPasswordNeeded, setIsPasswordNeeded] = useState(false)

  async function open(password?: string) {
    if (!bytesRef.current) return

    try {
      // The document reads from the buffer it receives for its whole lifetime,
      // so keep the original for a password retry and pass a copy.
      await loadPdf(bytesRef.current.slice(), { password })
      setIsPasswordNeeded(false)
    } catch (error) {
      if (error instanceof InvalidPasswordError) {
        // Where pdf-lib stops at EncryptedPDFError, ask for the password and open again.
        setIsPasswordNeeded(true)
      } else {
        showError(error)
      }
    }
  }

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

    // loadPdf takes a Uint8Array: wrap the ArrayBuffer.
    bytesRef.current = new Uint8Array(await file.arrayBuffer())
    await open()
  }

  if (isPasswordNeeded)
    return <PasswordPrompt onSubmit={(password) => void open(password)} />
  if (loadState.status === 'initial')
    return <input type="file" accept=".pdf" onChange={handleFileChange} />
  if (loadState.status === 'loading') return <p>Loading the document…</p>

  return <PdfApplication />
}

function CloseButton() {
  const resetPdf = useResetPdf()

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

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

* **Bytes as a Uint8Array.** Wrap an `ArrayBuffer` in `new Uint8Array(buffer)`, and decode base64 strings and data URIs to bytes before calling `loadPdf`. A URL becomes a `fetch` of your own.
* **Encrypted files open.** Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`. `ignoreEncryption` has no counterpart: an encrypted document opens with its password or not at all.
* **One load option.** `password` is the only option of `loadPdf`; `parseSpeed`, `updateMetadata`, `throwOnInvalidObject`, and `capNumbers` have no equivalent.
* **Keep a copy for retries.** Anything else touching the buffer passed to `loadPdf`, such as another `loadPdf` call, can detach it, and every later document read throws. To reuse the bytes, pass `data.slice()`.
* **No empty documents.** `PDFDocument.create()` has no equivalent: load an existing PDF, such as a blank template your app ships.
* **Reset invalidates ids.** After `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf), page and block ids, widget snapshots, and document revisions from the old document are no longer valid.

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

### Before: pdf-lib [#before-pdf-lib-2]

pdf-lib can't render pages. To show a document, apps save it and hand the bytes to an `<iframe>`, as a Blob URL or as a data URI from `saveAsBase64({ dataUri: true })`, and repeat that after every change:

```js
const pdfBytes = await pdfDoc.save()
const blob = new Blob([pdfBytes], { type: 'application/pdf' })
document.getElementById('preview').src = URL.createObjectURL(blob)
```

Apps that need more control render the saved bytes with a separate library, such as pdf.js.

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

[`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the loaded document and every change made to it, by users or by your code, with nothing to save first. It takes no props: [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) configures it and hooks control it:

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

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

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

* **No save-and-reload loop.** The Blob URL, the data URI, and the `<iframe>` go away. `prepareDocument()` is needed only when you want the file, as in [Saving and downloading](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#saving-and-downloading).
* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded, so render it when the load state is `'ready'`.
* **Interaction is built in.** Scrolling, zoom gestures, selection, typing, and form filling work inside `Viewport`, limited by the content policy. Toolbars, page navigation with [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage), and zoom controls with [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale) are your components.
* **Styling is yours.** The SDK renders unstyled; class names, inline styles, and CSS variables go through the `theme` configuration, as [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization) describes.

## Pages [#pages]

### Before: pdf-lib [#before-pdf-lib-3]

pdf-lib addresses pages by zero-based index and takes sizes as `[width, height]` pairs, often from `PageSizes`:

```js
import { degrees, PageSizes } from 'pdf-lib'

const pageCount = pdfDoc.getPageCount()
const { width, height } = pdfDoc.getPage(0).getSize()

// Append an A4 page, and insert a Letter page as the second page.
pdfDoc.addPage(PageSizes.A4)
pdfDoc.insertPage(1, PageSizes.Letter)

// Remove the last page.
pdfDoc.removePage(pdfDoc.getPageCount() - 1)

// Turn the first page by another quarter turn.
const firstPage = pdfDoc.getPage(0)
firstPage.setRotation(degrees(firstPage.getRotation().angle + 90))
```

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

[`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages) returns the pages in document order, so an index is a position in that list. The page commands take 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), and [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation):

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

// PageSizes.A4 and PageSizes.Letter, in user-space units.
const A4 = { width: 595.28, height: 841.89 }
const LETTER = { width: 612, height: 792 }

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

  if (pages.length === 0) return null
  const firstPage = pages[0]
  const lastPage = pages[pages.length - 1]

  function insertAtStart() {
    // insertPage(0, PageSizes.Letter): create the page, then move it to the start.
    const pageId = createPage(LETTER.width, LETTER.height)
    moveBefore(pageId)
  }

  return (
    <div role="toolbar">
      {/* addPage(PageSizes.A4): without a target page, the new page is appended. */}
      <button onClick={() => createPage(A4.width, A4.height)}>Add page</button>
      {/* insertPage(1, PageSizes.Letter): the new page goes after the first one. */}
      <button onClick={() => createPage(LETTER.width, LETTER.height, firstPage.id)}>
        Insert as page 2
      </button>
      <button onClick={insertAtStart}>Insert at start</button>
      <button disabled={pages.length <= 1} onClick={() => deletePages(lastPage.id)}>
        Delete last page
      </button>
      {/* Rotation is absolute: add to the current angle yourself. */}
      <button onClick={() => setPageRotation(firstPage.id, firstPage.rotation + 90)}>
        Rotate first page
      </button>
    </div>
  )
}
```

### From indices to ids [#from-indices-to-ids]

| pdf-lib                                       | @sdk-pdf/react                                                                                                                                  |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `pdfDoc.getPage(index)`                       | `usePages()[index]`: an entry with `id`, `width`, `height`, and `rotation`                                                                      |
| `pdfDoc.getPageCount()`                       | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount), which re-renders only when the count changes                                                 |
| The index of a page you hold                  | `getPageIndex(pageId)` from [`usePageIndexGetter`](https://sdk.pdf.net/docs/v1/reference/usePageIndexGetter): a zero-based index, or `undefined` for an unknown id |
| `page.getSize()`, `getWidth()`, `getHeight()` | `width` and `height` of the entry, with the rotation applied                                                                                    |
| `page.getRotation().angle`                    | `rotation` of the entry: `0`, `90`, `180`, or `270`                                                                                             |

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

* **Ids, not indices.** An id keeps pointing at the same page while pages are added, removed, and moved by the user or your code, where an index would shift to another page. Ids are session-scoped: store page numbers, and resolve them to ids once the document is ready.
* **Sizes are required.** `createPage(width, height, insertAfterPageId?)` always takes the size in user-space units, one unit being 1/72 of an inch; the `PageSizes` pairs carry over as numbers. pdf-lib's `getSize()` reads the MediaBox, while `usePages` sizes have the rotation applied.
* **Positions are page ids.** A new page goes after `insertAfterPageId`, or at the end without it. For the first position, create the page and call `moveBefore(pageId)`, which moves pages to the start when no target page is given.
* **Rotation is a number.** `setPageRotation(pageIdOrPageIds, rotation)` takes degrees instead of `degrees(...)`. It sets an absolute angle; `-90` and `450` are normalized, and an angle off a quarter turn throws `InvalidInputError`.
* **Undo.** Deleting, moving, or rotating several pages in one call is a single undo step, 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. An undo can restore a deleted page with its content.
* **New operations.** `moveBefore` and `moveAfter` reorder pages, and `duplicatePage` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate) copies one. Page boxes and resizing (`setSize`, `setMediaBox`, `setCropBox`, `scale`, `scaleContent`) have no equivalent.

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

### Before: pdf-lib [#before-pdf-lib-4]

pdf-lib merges documents by loading each source and copying its pages into the target with `copyPages`, followed by `addPage` or `insertPage`. It splits a document by copying some of its pages into a document from `PDFDocument.create()`, and draws pages of other documents onto a page with `embedPdf`, `embedPage`, and `drawPage`:

```js
import { PDFDocument } from 'pdf-lib'

// Merge: copy every page of each source into a new document.
const mergedPdf = await PDFDocument.create()
for (const sourceBytes of [firstPdfBytes, secondPdfBytes]) {
  const sourcePdf = await PDFDocument.load(sourceBytes)
  const copiedPages = await mergedPdf.copyPages(sourcePdf, sourcePdf.getPageIndices())
  for (const page of copiedPages) mergedPdf.addPage(page)
}

// Split: copy the first two pages into a document of their own.
const firstPartPdf = await PDFDocument.create()
const [firstPage, secondPage] = await firstPartPdf.copyPages(mergedPdf, [0, 1])
firstPartPdf.addPage(firstPage)
firstPartPdf.addPage(secondPage)

const mergedBytes = await mergedPdf.save()
const firstPartBytes = await firstPartPdf.save()
```

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

Splitting becomes a save option: `prepareDocument({ pageIds })` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) builds a PDF of the listed pages only, in the listed order, with the session's edits:

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

// Replaces copying pages into a new document: the first two pages become a file of their
// own.
function useSaveFirstPart() {
  const pages = usePages()
  const { prepareDocument } = useDocumentSaving()

  return () => prepareDocument({ pageIds: pages.slice(0, 2).map((page) => page.id) })
}
```

Merging has no equivalent: the page hooks work on the loaded document, and there is no API to import pages from another file. Keep merging where it runs today, on your server or in the browser with pdf-lib, and pass the resulting bytes to `loadPdf`. Inside the loaded document:

* `duplicatePage(sourcePageId, insertAfterPageId?)` from [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate) copies a page with all its content and resolves with the new page's id;
* `moveBefore` and `moveAfter` from [`usePageMove`](https://sdk.pdf.net/docs/v1/reference/usePageMove) reorder pages, several at once;
* `deletePages` from [`usePageDelete`](https://sdk.pdf.net/docs/v1/reference/usePageDelete) removes them.

Each page goes into a saved file once: to repeat a page, duplicate it first. Saving some of the pages doesn't change the loaded document.

## Forms [#forms]

### Before: pdf-lib [#before-pdf-lib-5]

`getForm()` returns the document's form. Fields are looked up by fully qualified name and typed by getter; a missing name, or a field of another type, throws:

```js
const form = pdfDoc.getForm()

form.getTextField('customer.name').setText('Jane Doe')
form.getCheckBox('terms.accepted').check()
form.getRadioGroup('delivery').select('Express')

const customerName = form.getTextField('customer.name').getText()

// A new radio group: each addOptionToPage call adds one widget.
const page = pdfDoc.getPage(0)
const contactField = form.createRadioGroup('contact')
contactField.addOptionToPage('Email', page, { x: 72, y: 540, width: 16, height: 16 })
contactField.addOptionToPage('Phone', page, { x: 72, y: 510, width: 16, height: 16 })

// Lock every field before the document goes out.
for (const field of form.getFields()) field.enableReadOnly()

const pdfBytes = await pdfDoc.save()
```

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

Users fill fields in [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) when the content policy lets them interact with widgets. For values your code sets or reads, the unit is the widget: [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets) lists the widgets in reading order, each with its `fieldName`, 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) covers the model:

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

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

// Replaces form.getTextField(name).setText(text) for each entry of `values`.
function PrefillTextFields({ values }: { values: Record<string, string> }) {
  const { widgets, isLoaded } = useWidgets()

  // Wait for the complete list: it's partial until every page with widgets has loaded.
  if (!isLoaded) return null

  return (
    <>
      {widgets
        .filter(isTextWidget)
        .filter((widget) => widget.fieldName in values)
        .map((widget) => (
          <PrefillTextField
            key={widget.id}
            widget={widget}
            text={values[widget.fieldName]}
          />
        ))}
    </>
  )
}

function PrefillTextField({ widget, text }: { widget: TextWidget; text: string }) {
  const [, setValue] = useWidgetValue(widget)
  const isPrefilled = useRef(false)

  useEffect(() => {
    // Write once, so what the user types afterwards stays.
    if (isPrefilled.current) return
    isPrefilled.current = true
    setValue(text)
  }, [setValue, text])

  return null
}

// Replaces createRadioGroup(name) and one addOptionToPage call per option.
function useAddContactField() {
  const createWidgets = useWidgetsCreate()

  return (pageId: string) => {
    // Radio widgets that share a field name form one group.
    const option = {
      pageId,
      widgetType: 'radio',
      x: 72,
      width: 16,
      height: 16,
      fieldName: 'contact',
    } as const

    return createWidgets([
      { ...option, y: 540, checkedValue: 'Email' },
      { ...option, y: 510, checkedValue: 'Phone' },
    ])
  }
}
```

Render `PrefillTextFields` once inside `PdfApplication`, with your values keyed by field name.

### Mapping the field API [#mapping-the-field-api]

| pdf-lib                                                                                            | @sdk-pdf/react                                                                                                                                                    |
| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PDFTextField`: `setText()`, `getText()`                                                           | A `text` widget; its value is a `string`                                                                                                                          |
| `PDFCheckBox`: `check()`, `uncheck()`, `isChecked()`                                               | A `checkbox` widget; its value is a `boolean`                                                                                                                     |
| `PDFRadioGroup`: `select(option)`, `getSelected()`                                                 | Several `radio` widgets sharing a field name, each with a `checkedValue`; each widget's value is a `boolean`, whether it's checked                                |
| `PDFDropdown`: `select()`, `addOptions()`, `enableEditing()`                                       | A `choice` widget with `isCombo`; its value is a `string`. Options are the `options` prop, and `allowsCustomValue` accepts typed text                             |
| `PDFOptionList`, with or without `enableMultiselect()`                                             | A `choice` widget without `isCombo`, a list box; with multiple selection, a `multiChoice` widget, whose value is the selected option indices                      |
| `PDFButton`: `addToPage(text, page)`                                                               | A `pushButton` widget with a `caption`; it has no value                                                                                                           |
| `PDFRadioGroup`: `enableOffToggling()`; mutual exclusion (`isMutuallyExclusive()` and its setters) | `canToggleToOff: true`; `isInUnison`, the inverse of mutual exclusion: options sharing a `checkedValue` check together                                            |
| `PDFTextField`: `setMaxLength()`, `enableMultiline()`, `enableCombing()`                           | The `maxLength`, `isMultiline`, and `isMonospaced` props                                                                                                          |
| `setAlignment()`, `setFontSize()`                                                                  | The `horizontalAlign` and `fontSize` props; a `fontSize` of `0` sizes the text automatically                                                                      |
| `addToPage()` and `addOptionToPage()` options, such as `backgroundColor`                           | `createWidgets(props)`: `pageId`, `widgetType`, `x`, `y`, `width`, and `height` are required; `fillColor`, `borderColor`, and the other widget props are optional |

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

* **Lookups find nothing instead of throwing.** Where `getTextField(name)` throws for a missing name or another field type, filtering `useWidgets()` by `fieldName` and `widgetType` returns no widget. For a one-time read inside a handler, `await getWidgets()` from [`useWidgetsGetter`](https://sdk.pdf.net/docs/v1/reference/useWidgetsGetter) loads every page with widgets first.
* **Values are read and written per widget.** `useWidgetValue` binds one widget per hook call, inside a component, so a fill by name becomes a component per widget, like `PrefillTextField` above. Its value follows the document: filling in the main view and undo/redo show up as they happen.
* **Field names are shared, not unique.** `createTextField(name)` throws when the name exists. `createWidgets` from [`useWidgetsCreate`](https://sdk.pdf.net/docs/v1/reference/useWidgetsCreate) with an existing `fieldName` adds a widget to that field, and every widget with the same name shows and synchronizes the same value; without a `fieldName`, a unique one is generated.
* **Boxes are required.** `addToPage` options are optional, while `createWidgets` needs the widget box: user-space units with the origin at the bottom-left corner of the page. Check placement on a test document when you port coordinates.
* **Locking, not flattening.** `enableReadOnly()` becomes `setWidgetProps(widgetIds, { isReadOnly: true }, { excludeFromHistory: true })` from [`useSetWidgetProps`](https://sdk.pdf.net/docs/v1/reference/useSetWidgetProps), where `excludeFromHistory` keeps an undo from unlocking the form. The change applies at once, and the returned promise resolves when the widgets show it. `form.flatten()`, which turns fields into page content, has no equivalent.
* **Removing fields.** `form.removeField(field)` becomes `deleteBlocks(widgetIds)` from [`useBlockDelete`](https://sdk.pdf.net/docs/v1/reference/useBlockDelete) with the ids of every widget of that field. The stored field value stays.
* **No images in fields.** `setImage` on text fields and buttons has no equivalent.

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

### Before: pdf-lib [#before-pdf-lib-6]

pdf-lib embeds a font or an image into the document, then draws it at page coordinates:

```js
import { rgb, StandardFonts } from 'pdf-lib'

const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
const pngImage = await pdfDoc.embedPng(pngImageBytes)
const page = pdfDoc.getPage(0)

page.drawText('Approved for release', {
  x: 72,
  y: 720,
  size: 14,
  font: helveticaFont,
  color: rgb(0.8, 0.1, 0.1),
  maxWidth: 300,
})

const { width, height } = pngImage.scale(0.5)
page.drawImage(pngImage, { x: 72, y: 600, width, height })
```

The drawn text becomes part of the page. pdf-lib has no API to read or edit text on a page afterwards, its own included.

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

Text from code becomes a paragraph: [`useParagraphCreate`](https://sdk.pdf.net/docs/v1/reference/useParagraphCreate) creates an empty one, [`useSetParagraphProps`](https://sdk.pdf.net/docs/v1/reference/useSetParagraphProps) sets its wrap width and layout, and the editor mirror from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) types into it. An image staged with [`stageImage`](https://sdk.pdf.net/docs/v1/reference/stageImage) goes onto the page as a stamp annotation, from code with [`useStampAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useStampAnnotationCreate) or by the user with the `addImage` tool:

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

// Replaces drawText: the result is a paragraph users can edit later.
function useAddApprovalNote() {
  const createParagraph = useParagraphCreate()
  const setParagraphProps = useSetParagraphProps()
  const { getParagraphEditor } = useProgrammaticEditor()

  return async (pageId: string) => {
    const paragraphId = await createParagraph(pageId, 72, 720)
    // Like maxWidth: the text wraps at 300 units.
    setParagraphProps([paragraphId], { width: 300 })

    // undefined when the paragraph no longer exists.
    const editor = getParagraphEditor(paragraphId)
    editor?.selectAll()
    editor?.insertText('Approved for release')
    return paragraphId
  }
}

// Replaces embedPng and drawImage at fixed coordinates.
function useAddLogo() {
  const createStampAnnotation = useStampAnnotationCreate()

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

// Or let the user choose the spot: the next click on a page places the image.
function ImagePicker() {
  const setInteractionMode = useSetInteractionMode()

  async function placeImage(file: File) {
    const image = await stageImage(file)
    // Placing images is editing, so switch the policy with the tool.
    setInteractionMode(
      { type: 'addImage', image, width: 200 },
      CONTENT_POLICY_PRESET.PDF_EDITOR
    )
  }

  return (
    <input
      type="file"
      accept="image/*"
      onChange={(event) => {
        const file = event.currentTarget.files?.[0]
        if (file) placeImage(file).catch(showError)
      }}
    />
  )
}
```

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

* **Text stays editable.** A paragraph is text, not drawing: users edit it in [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) under a policy with editable text, and code reads and rewrites any paragraph, those already in the document included, with `getParagraphsByPage(pageId)` and `getParagraphEditor(paragraphId)`, as [Programmatic editing](https://sdk.pdf.net/docs/v1/guides/programmatic-editing) shows.
* **Users can add text too.** The `{ type: 'addText' }` tool creates a paragraph where the user clicks and focuses it for typing; it requires editable text.
* **No font or color options.** `insertText(text)` takes the text only, and there are no `font`, `size`, or `color` options. Users style text with a selection toolbar built on [`useSelectionTextStyle`](https://sdk.pdf.net/docs/v1/reference/useSelectionTextStyle). The paragraph-wide `lineHeight`, `textAlign`, `textIndent`, and rotation through `matrix` are props of `setParagraphProps`. Embedding fonts and measuring text (`embedFont`, `widthOfTextAtSize`) have no equivalent.
* **Check placement.** `createParagraph(pageId, x, y)` takes the paragraph position in user-space units, and `setParagraphProps([paragraphId], { width })` sets the width the text wraps at; the height follows the text. Like `drawText`, a paragraph's `x` and `y` are the start of its first baseline. Check the result on a test page when you port `drawText` coordinates.
* **Images are stamp annotations.** `drawImage` paints into the page content; `@sdk-pdf/react` places the image as a stamp annotation that users can select, move, and resize. `stageImage(blob)` accepts any image the browser can decode, not only PNG and JPEG, and throws `InvalidInputError` when the blob isn't a loadable image or has no intrinsic size; SVGs are rasterized when the PDF is saved. The size goes on the placement, in user-space units: a single dimension keeps the aspect ratio, and without `width` and `height`, the natural size is used, fit down to the page. Like `drawImage`, `x` and `y` are the bottom-left corner.
* **No vector drawing.** `drawRectangle`, `drawLine`, `drawCircle`, `drawEllipse`, `drawSquare`, `drawSvgPath`, and `pushOperators` have no equivalent. For freehand strokes, such as a handwritten mark collected in your UI, [`useInkAnnotationCreate`](https://sdk.pdf.net/docs/v1/reference/useInkAnnotationCreate) creates an ink annotation from the points.
* **One document.** Programmatic edits go through the same document as user edits. [`setEditorAwarenessProperties`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) gives your code a presence cursor for real-time collaboration, which is coming soon.

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

### Before: pdf-lib [#before-pdf-lib-7]

`save()` serializes the document, with all changes, into a `Uint8Array`; `saveAsBase64()` returns a base64 string or a data URI. Saving also refreshes the appearance of form fields when the form was accessed through `getForm()`:

```js
const pdfBytes = await pdfDoc.save()
await fetch('/api/documents', { method: 'PUT', body: pdfBytes })
```

For a download, apps wrap the bytes in a `Blob` and click a temporary link. pdf-lib keeps no change history, so apps that warn about unsaved changes track edits themselves.

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

[`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) builds the final PDF from the loaded document and the recorded changes. [`useDocumentRevision`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevision) tells whether anything changed since the last save: keep the revision you saved, and compare:

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

function SaveControls() {
  const revision = useDocumentRevision()
  const getRevision = useDocumentRevisionGetter()
  // One component owns the saved revision: 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()

  // Every edit changes the revision; undoing back to the saved state clears the
  // indicator.
  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()
      // Like pdfDoc.save(): the bytes go to fetch, so never hand out the loaded
      // document's own buffer.
      const pdfBytes = await prepareDocument({ returnOriginalIfUnchanged: false })
      const response = await fetch('/api/documents', {
        method: 'PUT',
        body: pdfBytes as BodyInit,
      })
      // A failed upload keeps the unsaved-changes indicator.
      if (!response.ok) return

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

  return (
    <div role="toolbar">
      <button onClick={() => void save()}>Save</button>
      {/* Replaces the Blob and the temporary link. */}
      <button onClick={() => void downloadDocument()}>Download</button>
      {hasUnsavedChanges && <span>Unsaved changes</span>}
    </div>
  )
}
```

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

* **returnOriginalIfUnchanged** defaults to `true`: with no changes, `prepareDocument()` returns the buffer passed to `loadPdf` without a copy. Set it to `false` when you transfer or mutate the bytes, or the loaded document's buffer is detached and later reads throw.
* **Two options.** Besides `returnOriginalIfUnchanged`, `pageIds` saves only the listed pages, as in [Merge and split documents](https://sdk.pdf.net/docs/v1/migration/from-pdf-lib#merge-and-split-documents). `useObjectStreams`, `addDefaultPage`, `objectsPerTick`, `updateFieldAppearances`, base64 output, and a download file name have no equivalent; encode the bytes yourself where you need base64.
* **Change tracking is new.** The revision changes on every edit by the user or your code, and undo and redo bring back the revision of that point in the history, so equal revisions mean no unsaved changes. Render `SaveControls` once the document is ready, so its first revision is the loaded document's. To read the revision in a handler without re-rendering, use [`useDocumentRevisionGetter`](https://sdk.pdf.net/docs/v1/reference/useDocumentRevisionGetter).
* **Both functions throw DocumentNotLoadedError** when no document is loaded.

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

### Before: pdf-lib [#before-pdf-lib-8]

pdf-lib reports problems by throwing. `load()` throws `EncryptedPDFError` for encrypted files, the `PDFForm` getters throw when a field is missing or has another type, and text a font can't encode, such as `Ω` in the standard Helvetica, throws an encoding error:

```js
import { EncryptedPDFError, PDFDocument } from 'pdf-lib'

async function fillAndSave(existingPdfBytes, customerName) {
  try {
    const pdfDoc = await PDFDocument.load(existingPdfBytes)
    pdfDoc.getForm().getTextField('customer.name').setText(customerName)
    // An encoding error for field text surfaces on save.
    return await pdfDoc.save()
  } catch (error) {
    if (error instanceof EncryptedPDFError)
      showError('Encrypted documents are not supported')
    else showError(error)
    return null
  }
}
```

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

The SDK reports problems through two channels. When an operation is impossible, it throws a typed error and changes nothing. When it hits a problem it can work around, such as a malformed document, it completes the operation and emits a log message through [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages). [Error handling and logging](https://sdk.pdf.net/docs/v1/guides/error-handling-and-logging) has the full rules:

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

function useSdkLogReporting() {
  useEffect(() => {
    // The SDK never prints to the console: subscribe, and unsubscribe on unmount.
    const unsubscribe = logMessages.on('message', (logMessage) => {
      if (logMessage.level >= LogLevel.Warning)
        reportToMonitoring(logMessage.message, logMessage.data)
    })
    return () => {
      unsubscribe()
    }
  }, [])
}

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

  return async (data: Uint8Array, password?: string) => {
    try {
      // Pass a copy and keep `data` for a password retry.
      await loadPdf(data.slice(), { password })
    } catch (error) {
      if (error instanceof InvalidPasswordError) askForPassword()
      // Narrow the other SDK error classes with instanceof in the same way.
      else showError(error)
    }
  }
}
```

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

* **Error classes.** `InvalidPasswordError` (the password is missing or wrong), `DocumentFormatError` (the PDF is malformed beyond what the SDK can work around), `NotSupportedError` (the document uses a PDF feature the SDK doesn't support yet), `DocumentNotLoadedError`, `InvalidInputError`, and `UninitializedError`. All extend `GenericError` and carry a static `message` and an optional `data` object.
* **Nothing half-applied.** Functions that change the document validate their input and throw before touching anything; functions that only read return empty values instead of throwing; hooks never throw during render.
* **Logs are opt-in.** Each message carries `level` (`LogLevel.Debug`, `Info`, `Warning`, `Error`), `reason`, a static `message`, `data`, and `stack`. There is no verbosity setting: filter by `level` in your subscriber.

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

Plan around these before you migrate. Jobs that run outside the browser, such as generating, merging, or flattening documents on a server, can keep running where they are and hand their output to `loadPdf`:

| pdf-lib feature                                                                                           | In @sdk-pdf/react                                                                  |
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Running in Node, Deno, or React Native                                                                    | No equivalent; the SDK runs in React in the browser                                |
| Creating a document from scratch (`PDFDocument.create()`)                                                 | Load an existing PDF, such as a blank template                                     |
| Copying pages from other documents and merging (`copyPages`)                                              | No equivalent; `usePageDuplicate` copies pages within the loaded document          |
| Drawing pages of other documents (`embedPdf`, `embedPage`, `drawPage`)                                    | No equivalent                                                                      |
| Page boxes and resizing (`setMediaBox`, `setCropBox`, `setSize`, `scale`, `scaleContent`)                 | No equivalent                                                                      |
| Vector drawing (`drawRectangle`, `drawLine`, `drawCircle`, `drawEllipse`, `drawSvgPath`, `pushOperators`) | No equivalent; SVG images placed as stamps are rasterized on save                  |
| Embedding fonts and measuring text (`embedFont`, `@pdf-lib/fontkit`, `widthOfTextAtSize`)                 | No equivalent; fonts come from the `fontFamily` options of `useSelectionTextStyle` |
| Text style options on drawn text (`font`, `size`, `color`)                                                | Users style paragraphs through the selection                                       |
| Flattening forms (`form.flatten()`)                                                                       | No equivalent; lock widgets with `isReadOnly`                                      |
| Images in fields (`setImage`)                                                                             | No equivalent                                                                      |
| Document metadata (`setTitle`, `getAuthor`, and the other metadata methods)                               | No equivalent                                                                      |
| Viewer preferences, attachments (`attach`), document JavaScript (`addJavaScript`)                         | No equivalent                                                                      |
| Save options (`useObjectStreams`, `addDefaultPage`, `objectsPerTick`) and `saveAsBase64()`                | No equivalent; encode the bytes yourself                                           |

