# Migrating from Ghostscript

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



This guide moves the document previews of a web application built on Ghostscript to `@sdk-pdf/react`. It covers both common setups: a back end that runs the `gs` command, and one that embeds the interpreter through the `gsapi` C API, rendering pages to PNG or JPEG files that the browser shows as images. Ghostscript runs on the server and `@sdk-pdf/react` runs in the browser, so the guide moves the part users see and interact with into a React app: the server keeps fetching and storing the PDF bytes, and its conversion jobs, such as `pdfwrite` optimization and PDF/A, stay where they are. 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-ghostscript#overview): how the two integrations differ, and the API map for every switch and `gsapi` call you use.
* [ ] [List what your back end runs Ghostscript for](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#conversion-and-optimization): previews and thumbnails move to the browser, and conversion jobs stay on the server.
* [ ] [Install the package and set up `PdfProviders`](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#install-and-set-up), with a viewer content policy if your previews were images.
* [ ] [Serve the PDF bytes and load them with `loadPdf`](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#load-and-reset-a-document), including the password flow.
* [ ] [Replace the page images with `Viewport`](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#display-the-document).
* [ ] [Move page numbers and page ranges to page ids](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#pages-and-navigation).
* [ ] [Replace re-rendering at a new resolution with the viewport scale](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#zoom-and-resolution).
* [ ] [Replace thumbnail renders with `PageCanvas`](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#thumbnails).
* [ ] [Let users fill forms in the page](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#forms).
* [ ] [Save and download with `prepareDocument` and `downloadDocument`](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#saving-and-downloading).
* [ ] [Move error handling and logging](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#errors-and-logging) and [cleanup](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#cleanup-and-lifecycle).
* [ ] [Move text extraction](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#text-extraction), [keep conversion on the server](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#conversion-and-optimization), and [plan for features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#features-without-an-equivalent).
* [ ] Remove the preview and thumbnail endpoints, the page-image cache, and the Ghostscript jobs that filled it; keep Ghostscript only where conversion still needs it. Test encrypted, malformed, large, and form-heavy PDFs, and the save round-trip.

## Overview [#overview]

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

  **Ghostscript** is an interpreter for PostScript and PDF built on a graphics library, and it runs where your back end runs:

  * It's a native program: the `gs` executable, or the interpreter library (`libgs.so` on Linux, a DLL on Windows) that C code drives through `gsapi_new_instance`, `gsapi_init_with_args`, `gsapi_exit`, and `gsapi_delete_instance`. It's available under the AGPL and under a commercial license.
  * A job is a command line: input files, `-d` and `-s` switches, an output device, and an output file. Raster devices such as `png16m` and `jpeg` write page images; high-level devices such as `pdfwrite` and `txtwrite` write a new PDF or text.
  * Nothing stays open between jobs. Every preview, zoom level, and thumbnail size is another render, and the output files are yours to serve, cache, and delete.
  * Pages are 1-based numbers (`-dFirstPage`, `-dLastPage`, `-sPageList`), and the output size is a resolution in pixels per inch (`-r`, usually 72 by default).
  * The browser sees images: no selectable text, clickable links, or fillable fields.

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

  * [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) hosts the document and viewport state, and [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf) loads the document from its bytes.
  * [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages and handles selection, text editing, form filling, link navigation, and zoom gestures, within the limits of the content policy. [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders single pages for thumbnails and previews.
  * The document stays loaded. Pages are addressed by session-scoped ids, and state hooks re-render your components when it changes.
  * Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers undo/redo; [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) builds the final PDF when you need it.

  | Concern         | Ghostscript                                                               | @sdk-pdf/react                                                              |
  | --------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
  | Where it runs   | The server: the `gs` process or the interpreter library in C              | The browser, as React components and hooks                                  |
  | Output          | Page images, a rewritten PDF, text files                                  | A live page view (`Viewport`), canvas renders (`PageCanvas`), the final PDF |
  | Interaction     | None: the browser shows pictures of the pages                             | Selection, links, form filling, and editing, per content policy             |
  | Input           | PostScript, EPS, and PDF files, by path or from standard input            | PDF bytes                                                                   |
  | Page addressing | 1-based page numbers                                                      | Session-scoped page ids                                                     |
  | Size            | A resolution per render (`-r`), or a fixed size (`-g`)                    | The viewport scale, with zoom gestures                                      |
  | Saving          | `pdfwrite` writes a new PDF from what the interpreter draws               | `prepareDocument()` applies the recorded changes to the original document   |
  | Diagnostics     | `gsapi_*` return codes, messages on standard output and standard error    | Typed errors thrown before any change; `logMessages`                        |
  | Distribution    | A native binary or shared library, under the AGPL or a commercial license | A package from a private registry                                           |

  What this means for your code:

  1. **Move the preview into the browser.** The render endpoints, the page-image cache, and the lists of `<img>` elements become `Viewport` and `PageCanvas`.
  2. **Serve the PDF instead of images.** The browser fetches the file and passes its bytes to `loadPdf`, so the access checks of your image endpoints move to the file endpoint.
  3. **Replace page numbers with ids** at the edges: deep links, analytics, and stored positions.
  4. **Replace resolutions with a scale.** A render at `-r144` becomes `setScale(144 / 72)`; see [Zoom and resolution](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#zoom-and-resolution).
  5. **Choose the content policy.** An image can't be selected, clicked, or filled; pick how much of that the users get now.
  6. **Keep conversion on the server.** `pdfwrite`, PDF/A, image exports, and printing stay on the server, now fed the bytes that `prepareDocument` returns.
  7. **Editing is new.** Ghostscript has no editing tools, undo, or change tracking, so this guide has no sections for them: [Building a PDF application](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application) covers interaction tools and history.
</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-ghostscript#features-without-an-equivalent).

  | Ghostscript                                                              | @sdk-pdf/react                                                                                                                           | Notes                                                  |
  | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
  | The `gs` executable, `gsapi_new_instance()` and `gsapi_init_with_args()` | `<PdfProviders>`                                                                                                                         | Runs in the browser; nothing to install on the server. |
  | An input file path, or `-` for standard input                            | `loadPdf(Uint8Array)` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf)                                                                 | `fetch` the file into bytes first.                     |
  | `-sPDFPassword=password`                                                 | `loadPdf(data, { password })`, catch `InvalidPasswordError`                                                                              | The user types the password in the browser.            |
  | `-dSAFER`, `--permit-file-read`                                          | Nothing to configure                                                                                                                     | `loadPdf` takes bytes, not paths.                      |
  | PostScript and EPS input, `ps2pdf`                                       | **No equivalent**                                                                                                                        | PDF only: convert on the server first.                 |
  | `-sDEVICE=png16m`, `-sOutputFile=page-%03d.png`, an image per page       | [`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport)                                                                                                | Real pages instead of pictures.                        |
  | `-dTextAlphaBits`, `-dGraphicsAlphaBits`, `-dDownScaleFactor`            | Nothing to configure                                                                                                                     | `Viewport` takes no props.                             |
  | `.PDFInfo` `/NumPages`, counting the rendered files                      | [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount)                                                                                        | `0` while no document is loaded.                       |
  | `-dFirstPage`, `-dLastPage`, `-sPageList`                                | `scrollToPage(pageId)` from [`useScrollToPage`](https://sdk.pdf.net/docs/v1/reference/useScrollToPage)                                                      | The viewport shows the whole document.                 |
  | Page `n` (1-based)                                                       | `usePages()[n - 1].id` with [`usePages`](https://sdk.pdf.net/docs/v1/reference/usePages)                                                                    | See [Pages and navigation](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#pages-and-navigation).     |
  | `-r` per zoom level                                                      | [`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale)                                                                                | `-rN` shown at its natural size is `setScale(N / 72)`. |
  | `-g` with `-dPDFFitPage`                                                 | [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale), [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale) | Fits into the viewport, not into a pixel box.          |
  | Low-resolution thumbnail renders                                         | [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas)                                                                                            | Sized with CSS, rendered lazily.                       |
  | `-dShowAnnots=false`, `-dShowAcroForm=false`                             | Content policy `annotations: 'hidden'`, `widgets: 'hidden'`; `hideAnnotations` on `PageCanvas`                                           | The policy hides them in `Viewport`.                   |
  | `-dPrinted`                                                              | **No equivalent**                                                                                                                        |                                                        |
  | Field appearances drawn into the image                                   | Live widgets: [`useWidgets`](https://sdk.pdf.net/docs/v1/reference/useWidgets), [`useWidgetValue`](https://sdk.pdf.net/docs/v1/reference/useWidgetValue)                       | Users fill fields in `Viewport` under `PDF_VIEWER`.    |
  | `-sDEVICE=pdfwrite` to produce the downloadable PDF                      | `prepareDocument()` and `downloadDocument()` from [`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)                            | Applies the changes to the original document.          |
  | `-dPDFSETTINGS`, `-dCompatibilityLevel`, `-dFastWebView`                 | **No equivalent**                                                                                                                        | Keep `pdfwrite` on the server.                         |
  | `-dPDFA`, `-dPDFX`, `-sOwnerPassword`, `-sUserPassword`                  | **No equivalent**                                                                                                                        | Keep `pdfwrite` on the server.                         |
  | `-sDEVICE=txtwrite`                                                      | `getAllParagraphIds()` and `getPlainTextParagraphs()` from [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor)           | Paragraph text and bounds, not a text file.            |
  | `gsapi_*` return codes                                                   | The rejected promise or the thrown error                                                                                                 | See [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#errors-and-logging).         |
  | `gsapi_set_stdio()` callbacks, `-q`, `-dQUIET`                           | [`logMessages`](https://sdk.pdf.net/docs/v1/reference/logMessages)                                                                                          | The SDK never prints to the console.                   |
  | `-dPDFSTOPONERROR`, `-dPDFSTOPONWARNING`                                 | **No equivalent**                                                                                                                        | Repairs arrive as log messages.                        |
  | `gsapi_exit()`, `gsapi_delete_instance()`                                | Nothing to call; `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) closes the document                                   | See [Cleanup and lifecycle](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#cleanup-and-lifecycle).   |
  | Temporary files in `TMPDIR`, rendered files                              | Nothing to delete                                                                                                                        |                                                        |
  | `-sDEVICE=jpeg`, `png16m` for consumers outside your app                 | **No equivalent**                                                                                                                        | Keep these renders on the server.                      |
</details>

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

### Before: Ghostscript [#before-ghostscript]

Ghostscript is a native program. A source release builds with `./configure`, `make`, and `make install`, which installs `gs` in `/usr/local/bin` by default, with the fonts installed separately; Windows has installers, with `gswin64c.exe` as the console binary, and Linux also has a snap. `gs -h` prints the version and the output devices the build includes:

```sh
./configure
make
make install

gs -h
```

The back end runs `gs` as a process for each job, or embeds the interpreter library and passes the same arguments through `gsapi`. Embedding needs the shared library: build it with `make so` and install it with `make soinstall`:

```c
#include "ierrors.h"
#include "iapi.h"

/* Runs one job with command-line arguments; argv[0] is ignored. */
int run_ghostscript(int argc, char **argv)
{
    void *instance = NULL;
    int code, exit_code;

    code = gsapi_new_instance(&instance, NULL);
    if (code < 0)
        return code;

    code = gsapi_set_arg_encoding(instance, GS_ARG_ENCODING_UTF8);
    if (code == 0)
        code = gsapi_init_with_args(instance, argc, argv);

    /* Required once gsapi_init_with_args has been called, and before deleting the
       instance. */
    exit_code = gsapi_exit(instance);
    if (code == 0 || code == gs_error_Quit)
        code = exit_code;

    gsapi_delete_instance(instance);
    return code;
}
```

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

The package comes 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.

Then wrap the part of your app that shows documents in [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders). Its props must keep their identity between renders, so define static configuration outside the component:

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

// Static configuration lives outside the component, so its identity never changes.
const initialInteractionTool: InteractionTool = { type: 'textSelect' }

export function DocumentPage({ documentId }: { documentId: string }) {
  return (
    <PdfProviders
      initialInteractionTool={initialInteractionTool}
      initialContentPolicy={CONTENT_POLICY_PRESET.PDF_VIEWER}
    >
      {/* The endpoint that used to return page images now returns the PDF. */}
      <DocumentLoader url={`/api/documents/${documentId}/file`} />
    </PdfProviders>
  )
}
```

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

* **Nothing to install on the server for display.** The install is the registry scope, the package, and `PdfProviders`. Ghostscript stays installed only on the servers that still run conversion jobs.
* **Pick the content policy deliberately.** A page image can't be selected, clicked, or filled. `PdfProviders` defaults to the `select` tool and [`CONTENT_POLICY_PRESET.PDF_EDITOR`](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET), which makes text, annotations, links, and widgets editable. For a reader, start with `PDF_VIEWER` (text and annotations read-only, links and widgets interactive) and the `textSelect` tool, as above; `PDF_PRINTER` also makes forms read-only.
* **Links stay clickable** under both read-only presets. To keep them inert, like in an image, pass a custom policy such as `{ ...CONTENT_POLICY_PRESET.PDF_VIEWER, links: 'hidden' }`, which keeps text read-only for the `textSelect` tool; the levels are listed in [`useSetInteractionMode`](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode). `'hidden'` works for every content kind, and a level can also be a function of the block id, such as `widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive')`.
* **React only.** The viewer is React components and hooks; the code that built image lists in the browser goes away.

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

### Before: Ghostscript [#before-ghostscript-1]

Ghostscript reads the input files named on the command line. A PDF needs random access, so a PDF piped to standard input (`-`) is first copied to a temporary file. `-sPDFPassword` sets the user or owner password of an encrypted file. Since 9.50, `-dSAFER` is the default: file access from PostScript input is limited to permitted paths, which you extend with `--permit-file-read` and the related options, and since 10.04.0, PostScript input can only switch to the device given on the command line (or to `nulldevice`) unless you pass `--permit-devices`:

```sh
gs -dSAFER -dBATCH -dNOPAUSE -sPDFPassword="$DOCUMENT_PASSWORD" \
  -sDEVICE=png16m -r144 \
  -sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdf
```

Each request runs a new job, so no document stays open between requests.

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

The browser fetches the file and passes the bytes to `loadPdf` from [`useLoadPdf`](https://sdk.pdf.net/docs/v1/reference/useLoadPdf); the load state decides what to render. When the document id changes, `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) unloads the open document, the way each Ghostscript job started from a clean state:

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

function DocumentLoader({ url }: { url: string }) {
  const [loadState, loadPdf] = useLoadPdf()
  const resetPdf = useResetPdf()
  // The fetched bytes and their URL, kept for a password retry.
  const bytesRef = useRef<{ url: string; data: Uint8Array } | null>(null)
  const [openedUrl, setOpenedUrl] = useState<string | null>(null)
  const [passwordUrl, setPasswordUrl] = useState<string | null>(null)
  const [errorUrl, setErrorUrl] = useState<string | null>(null)

  // Another document id while a document is open: unload it first.
  useEffect(() => {
    if (loadState.status === 'ready' && openedUrl !== url) resetPdf()
  }, [loadState.status, openedUrl, url, resetPdf])

  async function open(password?: string) {
    const requestedUrl = url
    try {
      if (!bytesRef.current || bytesRef.current.url !== requestedUrl) {
        const response = await fetch(requestedUrl)
        if (!response.ok)
          throw new Error(`The document request failed with ${response.status}`)
        bytesRef.current = {
          url: requestedUrl,
          data: new Uint8Array(await response.arrayBuffer()),
        }
      }

      // 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(bytesRef.current.data.slice(), { password })
      setOpenedUrl(requestedUrl)
      setPasswordUrl(null)
    } catch (error) {
      // Replaces -sPDFPassword: ask the user, then call open(password) again.
      if (error instanceof InvalidPasswordError) setPasswordUrl(requestedUrl)
      else setErrorUrl(requestedUrl)
    }
  }

  if (errorUrl === url) return <p role="alert">The document couldn't be opened.</p>
  if (passwordUrl === url) return <PasswordPrompt onSubmit={open} />
  if (loadState.status === 'loading') return <p>Loading the document…</p>
  if (loadState.status === 'ready' && openedUrl === url) return <PdfApplication />

  return <button onClick={() => void open()}>Open</button>
}
```

A close action uses the same command:

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

* **The browser gets the file, not pictures of it.** With Ghostscript, the browser received images of the pages you rendered; now it holds the whole PDF. Serve the file endpoint only to users who may have the document, with the same checks your image endpoints made.
* **Bytes, not paths.** `loadPdf` takes a `Uint8Array`, so headers, cookies, and caching belong to your `fetch` and your endpoint. `-dSAFER` and `--permit-file-read` have no counterpart: there are no file permissions to configure.
* **Passwords move to the browser.** Instead of the server passing `-sPDFPassword`, the user types the password. Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with `InvalidPasswordError`.
* **Keep a copy for retries.** The document reads from the given buffer for its whole lifetime, and another `loadPdf` call can detach it. Pass `bytesRef.current.data.slice()` and keep the original.
* **PDF only.** `useLoadPdf` loads PDF documents. For PostScript or EPS uploads, keep converting them to PDF on the server and serve the result.
* **Switching documents invalidates ids.** After `resetPdf()`, whether from a close action or a new document id, page and block ids, widget snapshots, and document revisions from the old document are no longer valid.

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

### Before: Ghostscript [#before-ghostscript-2]

The back end renders every page to a PNG file with a raster device, a resolution, and anti-aliasing for text and graphics (`4` is the highest setting):

```sh
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -r144 \
  -dTextAlphaBits=4 -dGraphicsAlphaBits=4 \
  -sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdf
```

The `%03d` in the output file name becomes the page number, so each page gets its own file. `-dDownScaleFactor` is the other route to smooth edges: `-r432 -dDownScaleFactor=3` renders at 432 dpi and writes 144 dpi images. The server caches the files, and the browser shows one `<img>` per page, asking for new renders when the user zooms.

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

[`Viewport`](https://sdk.pdf.net/docs/v1/reference/Viewport) renders the pages, and your components render everything around them, as [Building a PDF application](https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application) walks through:

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

function PdfApplication() {
  // Fit the page width once per loaded document, like previews sized to their column.
  useViewportAutoFitScale('width')

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

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

* **Real pages, not pictures.** Text selection, link navigation, form filling, and zoom gestures work inside `Viewport`, within the limits of the content policy.
* **No render settings.** The device, `-dTextAlphaBits`, `-dGraphicsAlphaBits`, and `-dDownScaleFactor` have no counterpart: `Viewport` takes no props, and [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) configures the theme, the interaction mode, and the gestures. `-r` becomes the viewport scale; see [Zoom and resolution](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#zoom-and-resolution).
* **No image pipeline.** The render jobs, the cache of page images, and the cleanup of old renders go away for display.
* **Render only when ready.** `Viewport` throws `DocumentNotLoadedError` when no document is loaded, so render it once the load state is `'ready'`.
* **Styling.** `theme.classNames` and `theme.styles` target the SDK's elements, such as `scrollViewport` and `page`, and `theme.viewportPadding` sets the gap around the pages; see [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization).
* **No page boxes or color management.** `-dUseCropBox`, `-dUseTrimBox`, `-dUseArtBox`, `-dUseBleedBox`, and the ICC profile switches have no equivalent.

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

### Before: Ghostscript [#before-ghostscript-3]

The browser asks for pages by number, and the back end renders the requested range. The page count comes from the PDF interpreter, whose `.PDFInfo` operator returns a dictionary with `/NumPages`; the file has to be permitted for reading:

```sh
# Prints the page count.
gs -q -dNODISPLAY --permit-file-read=/srv/files/42.pdf \
  -c "<< >> .PDFInit dup (/srv/files/42.pdf) exch .PDFFile .PDFInfo /NumPages get = quit"

# Renders page 3 only, for /documents/42/pages/3.
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -r144 \
  -dFirstPage=3 -dLastPage=3 \
  -sOutputFile=/var/cache/previews/42/page-3.png /srv/files/42.pdf
```

`-sPageList` selects several pages or ranges, such as `-sPageList=1,3,5-7`; it overrides `-dFirstPage` and `-dLastPage`, and the `%d` in the output file name then counts the rendered pages, not the original page numbers.

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

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

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

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

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

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const pageNumber = Number(new FormData(event.currentTarget).get('page'))
    // A 1-based number, like -dFirstPage, is an index in the page list.
    const page = pages[pageNumber - 1]
    if (page) scrollToPage(page.id, { behavior: 'smooth' })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="page"
        type="number"
        min={1}
        max={pages.length}
        aria-label="Page number"
      />
      <button type="submit">Go</button>
      <span>
        {currentIndex === -1 ? '–' : currentIndex + 1} of {pages.length}
      </span>
    </form>
  )
}
```

When only the count matters, such as for a label that replaces the `/NumPages` lookup, [`usePageCount`](https://sdk.pdf.net/docs/v1/reference/usePageCount) re-renders only when the count changes.

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

| You have                  | You need     | Use                                                                                                                          |
| ------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| A 1-based page number `n` | A page id    | `usePages()[n - 1].id`                                                                                                       |
| A page id                 | Its position | `getPageIndex(pageId)` from [`usePageIndexGetter`](https://sdk.pdf.net/docs/v1/reference/usePageIndexGetter): a zero-based index or `undefined` |
| A page id, rendered       | Its position | `usePageIndexGetter` paired with [`useRenderOnPageOrderChange`](https://sdk.pdf.net/docs/v1/reference/useRenderOnPageOrderChange)               |
| Only the count            |              | `usePageCount()`, `0` while no document is loaded                                                                            |

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

* **Ranges become scroll targets.** `-dFirstPage`, `-dLastPage`, and `-sPageList` choose what to render; `Viewport` shows the document's pages in one scrollable area, so a page range in a deep link becomes `scrollToPage` to its first page.
* **Ids are session-scoped.** Keep page numbers in URLs and stored state, such as `/documents/42/pages/3`, and resolve them to ids once the document is ready.
* **Pages can change during a session.** [`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), [`usePageDuplicate`](https://sdk.pdf.net/docs/v1/reference/usePageDuplicate), and [`useSetPageRotation`](https://sdk.pdf.net/docs/v1/reference/useSetPageRotation) edit the loaded document. An id keeps pointing at the same page while pages are added, removed, and rearranged, where a number would drift to another page.
* **Stale ids.** `usePageIndexGetter` returns `undefined` for an unknown id, and `scrollToPage` throws `InvalidInputError`.

## Zoom and resolution [#zoom-and-resolution]

### Before: Ghostscript [#before-ghostscript-4]

A zoom level is another render at a different resolution. A fit is a fixed image size: `-g` sets the width and height in pixels, and `-dPDFFitPage` scales each page to fit it:

```sh
# 150% of a 72 dpi preview: 72 × 1.5 = 108 dpi.
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -r108 \
  -dFirstPage=3 -dLastPage=3 -dTextAlphaBits=4 -dGraphicsAlphaBits=4 \
  -sOutputFile=/var/cache/previews/42/page-3-r108.png /srv/files/42.pdf

# Every page fitted into a 1200 × 1600 pixel image.
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -g1200x1600 -dPDFFitPage \
  -dTextAlphaBits=4 -dGraphicsAlphaBits=4 \
  -sOutputFile=/var/cache/previews/42/fit-%03d.png /srv/files/42.pdf
```

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

[`useViewportScale`](https://sdk.pdf.net/docs/v1/reference/useViewportScale) reads and sets the scale, [`useViewportFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportFitScale) calculates fit scales on demand, and [`useViewportAutoFitScale`](https://sdk.pdf.net/docs/v1/reference/useViewportAutoFitScale) (in `PdfApplication` above) fits once per loaded document:

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

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

  // Every scale change, from these buttons or from pinch and wheel gestures.
  useEffect(() => saveZoomPreference(scale), [scale])

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

  return (
    <div role="toolbar">
      <button onClick={() => setScale(scale / 1.25)}>−</button>
      <span>{Math.round(scale * 100)}%</span>
      <button onClick={() => setScale(scale * 1.25)}>+</button>
      {/* Call the getter inside the handler: the result reflects the viewport size at
         that moment. */}
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={fitPage}>Fit page</button>
    </div>
  )
}
```

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

* **Resolution becomes scale.** Ghostscript sizes output in pixels per inch; the viewport scale is CSS pixels per user-space unit, and a user-space unit is 1/72 of an inch. An image rendered at `-rN` and shown at its natural size is `setScale(N / 72)`: `-r72` is `1` (100%), and `-r144` is `2`. Convert stored zoom levels this way.
* **Zooming is local.** A zoom changes the scale of the loaded document in the browser instead of starting a render job on the server.
* **Fits are one-time calculations.** `useViewportAutoFitScale` applies when a document loads and doesn't re-apply on viewport resizes or manual zooming; `useViewportFitScale` reflects the viewport size at the moment of the call. `fitWidthScale` fits the widest page and `fitHeightScale` the tallest, both accounting for `viewportPadding`.
* **Gestures are built in.** Pinch and wheel zoom go through the same scale and are tuned with `zoomGestures` on [`PdfProviders`](https://sdk.pdf.net/docs/v1/reference/PdfProviders) (`minScale` `0.5` and `maxScale` `4` by default). During a gesture, `scale` keeps its value until the gesture ends.
* **Zoom around a point.** `setScale(scale, pivot?)` keeps the document still at `pivot`, a point `{ x, y }` given as fractions of the viewport area (`0` the top or left edge, `1` the bottom or right edge), the center by default. The third item of `useViewportScale()`, `animateScale(scale, pivot?)`, animates the change and resolves when it ends. Both throw `InvalidInputError` unless the scale is a positive finite number.

## Thumbnails [#thumbnails]

### Before: Ghostscript [#before-ghostscript-5]

Thumbnails are another render job at a small fixed size, with `-g` for the box and `-dPDFFitPage` to fit pages of any size into it:

```sh
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -g160x208 -dPDFFitPage \
  -dTextAlphaBits=4 -dGraphicsAlphaBits=4 \
  -sOutputFile=/var/cache/thumbnails/42/page-%03d.png /srv/files/42.pdf
```

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

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

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

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

  return (
    <nav aria-label="Pages">
      {pages.map((page, index) => (
        <button key={page.id} onClick={() => scrollToPage(page.id)}>
          {/* The render resolution follows the canvas CSS size: style .thumbnail to your
             old thumbnail width. */}
          <PageCanvas
            pageId={page.id}
            className="thumbnail"
            role="img"
            aria-label={`Page ${index + 1}`}
          />
          <span>{index + 1}</span>
        </button>
      ))}
    </nav>
  )
}
```

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

* **Sized with CSS, not -g.** The render resolution follows the canvas's displayed CSS size, or an explicit `scale` prop, so the pixels stay sharp at the size you style.
* **Lazy by default.** An off-screen canvas does no work until it becomes visible. In a virtualized list, pass `isVisible` to skip the per-canvas intersection observer.
* **Annotations.** `PageCanvas` draws annotations, form widgets included; `hideAnnotations` leaves them all out, like `-dShowAnnots=false` and `-dShowAcroForm=false` together. `hideExtractableText` skips the text content.
* **Thumbnails that show session edits are coming soon.** `PageCanvas` draws the page as loaded from the file; the main view shows the edits. The page rotation is applied and followed live.
* **Render only when ready.** `PageCanvas` throws `DocumentNotLoadedError` when no document is loaded.

## Forms [#forms]

### Before: Ghostscript [#before-ghostscript-6]

Form fields are drawn into the image like the rest of the page. `-dShowAcroForm=false` leaves out the annotations of the form dictionary, and `-dShowAnnots=false` the page's annotations. Devices that write an output file, such as `png16m`, use the file's print settings for annotations; `-dPrinted=false` switches to the screen settings:

```sh
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -r144 -dPrinted=false \
  -sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdf
```

The image shows the fields drawn into the page; users can't type into a picture.

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

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

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

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

function TextFieldsPanel() {
  const { widgets, isLoaded } = useWidgets()

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

  return (
    <form>
      {widgets.filter(isTextWidget).map((widget) => (
        <TextField key={widget.id} widget={widget} />
      ))}
    </form>
  )
}

function TextField({ widget }: { widget: TextWidget }) {
  // Typing here and typing into the field in the main view update the same value.
  const [value, setValue] = useWidgetValue(widget)

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

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

* **Fields are live.** Under `PDF_VIEWER`, users fill fields in the page; under `PDF_PRINTER`, widgets are read-only, the closest match to an image.
* **Values are shaped like HTML form controls:** `text` and `choice` are strings, `checkbox` and `radio` booleans, and `multiChoice` the selected option indices. Every widget with the same field name shows and synchronizes the same value.
* **Values follow the document.** Filling in the main view and undo/redo show up as they happen. `useWidgetValue` binds one widget per hook call.
* **Display switches.** `hideAnnotations` on `PageCanvas` hides annotations and form widgets together, like `-dShowAnnots=false` with `-dShowAcroForm=false`. In `Viewport`, the content policy levels `annotations: 'hidden'` and `widgets: 'hidden'` leave them out separately. `-dPrinted` has no equivalent.
* **The filled PDF comes from the browser.** `prepareDocument()` returns the document with the entered values; see [Saving and downloading](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#saving-and-downloading).
* **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).

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

### Before: Ghostscript [#before-ghostscript-7]

The download is the stored file, or a copy that `pdfwrite` rewrites on the way out, such as with a `-dPDFSETTINGS` preset:

```sh
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -dPDFSETTINGS=/ebook \
  -sOutputFile=/var/cache/downloads/42.pdf /srv/files/42.pdf
```

`pdfwrite` doesn't edit the input: it builds a new PDF from what the interpreter draws, which should look the same as the original while its insides differ.

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

[`useDocumentSaving`](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving) applies the changes recorded during the session to the original document. `prepareDocument()` returns the bytes for your server, and `downloadDocument()` saves them on the user's device:

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

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

  // Every edit changes the revision; undoing back to the saved state restores it.
  const hasUnsavedChanges = revision !== savedRevision

  async function saveToServer() {
    if (!hasUnsavedChanges || isSavingRef.current) return
    isSavingRef.current = true
    try {
      // Edits made after the save starts may be missing from the saved bytes, so only
      // this revision becomes saved.
      const startRevision = getRevision()
      // The buffer goes to fetch, so don't hand out the loaded document's original
      // buffer.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })
      // The server stores the file and runs its Ghostscript jobs on it.
      const response = await fetch(uploadUrl, { method: 'PUT', body: bytes as BodyInit })
      // A failed upload keeps the unsaved-changes state.
      if (!response.ok) return

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

  return (
    <div role="toolbar">
      <button onClick={() => void saveToServer()}>Save</button>
      {/* Builds the file in the browser instead of a download endpoint. */}
      <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-7]

* **Applied, not redrawn.** `pdfwrite` creates a new file from the drawing operations, and not everything that doesn't make marks on the page carries over. `prepareDocument()` applies the recorded changes to the original document instead.
* **returnOriginalIfUnchanged** defaults to `true`: with no changes, the buffer passed to `loadPdf` comes back with no copy. Set it to `false` when you transfer or mutate the result, or the loaded document's buffer is detached and later reads throw.
* **Saving selected pages.** `prepareDocument({ pageIds })` saves only the listed pages, in the listed order, like `-sPageList` with `pdfwrite`.
* **No optimization options.** There is no `-dPDFSETTINGS`, compatibility level, linearization (`-dFastWebView`), or encryption on save, and `downloadDocument()` has no file name option. Run those jobs on the server on the uploaded bytes; see [Conversion and optimization](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#conversion-and-optimization).
* **Check post-processing with forms.** If the server still runs `pdfwrite` on saved files, test it with filled forms and annotations, since it writes a new file rather than copying the saved one.
* **Both functions throw DocumentNotLoadedError** when no document is loaded.

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

### Before: Ghostscript [#before-ghostscript-8]

The PDF interpreter tries to repair broken files and prints a warning; `-dPDFSTOPONERROR` makes it stop with an error instead, and `-q` drops the startup messages:

```sh
gs -q -dSAFER -dBATCH -dNOPAUSE -dPDFSTOPONERROR -sDEVICE=png16m -r144 \
  -sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdf
```

Through `gsapi`, stdio callbacks collect the messages, and the return code tells success from failure: `0` is no error, `gs_error_Quit` isn't an error, a negative code is an error, and a code at or below `gs_error_Fatal` is fatal:

```c
#include <stdio.h>
#include "ierrors.h"
#include "iapi.h"

static int GSDLLCALL read_nothing(void *caller_handle, char *buf, int len)
{
    return 0; /* End of input. */
}

static int GSDLLCALL write_to_log(void *caller_handle, const char *str, int len)
{
    fwrite(str, 1, len, (FILE *)caller_handle);
    return len;
}

int run_with_log(int argc, char **argv, FILE *log)
{
    void *instance = NULL;
    int code, exit_code;

    /* The handle passed here reaches the stdio callbacks. */
    code = gsapi_new_instance(&instance, log);
    if (code < 0)
        return code;

    gsapi_set_stdio(instance, read_nothing, write_to_log, write_to_log);
    code = gsapi_set_arg_encoding(instance, GS_ARG_ENCODING_UTF8);
    if (code == 0)
        code = gsapi_init_with_args(instance, argc, argv);

    exit_code = gsapi_exit(instance);
    if (code == 0 || code == gs_error_Quit)
        code = exit_code;
    gsapi_delete_instance(instance);

    /* Negative: the job failed, and the log holds the messages. */
    return code;
}
```

### 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 useReportSdkLogs(documentId: string) {
  // Replaces the stdio callbacks: subscribe, and unsubscribe on unmount.
  useEffect(() => {
    const unsubscribe = logMessages.on('message', (logMessage) => {
      // Where Ghostscript printed a warning about a repaired file, the SDK sends a
      // message.
      if (logMessage.level >= LogLevel.Warning) {
        reportToMonitoring(logMessage.message, {
          documentId,
          reason: logMessage.reason,
          data: logMessage.data,
        })
      }
    })
    return () => {
      unsubscribe()
    }
  }, [documentId])
}

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

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

### Error classes [#error-classes]

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

| @sdk-pdf/react           | Thrown when                                                             | Closest Ghostscript situation                       |
| ------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------- |
| `InvalidPasswordError`   | The document is encrypted, and the password is missing or wrong.        | An encrypted file without the right `-sPDFPassword` |
| `DocumentFormatError`    | The PDF content is malformed beyond what the SDK can work around.       | A broken file that fails the job                    |
| `NotSupportedError`      | The document uses a PDF feature the SDK doesn't support yet.            | —                                                   |
| `DocumentNotLoadedError` | An operation requires a document, and none is loaded.                   | —                                                   |
| `InvalidInputError`      | The input passed to the API is invalid.                                 | —                                                   |
| `UninitializedError`     | An object is used before its asynchronous initialization has completed. | —                                                   |

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

* **Repairs are log messages.** Like Ghostscript, the SDK works around malformed documents; it reports each problem as a log message whose `reason` names the kind of problem, such as a document format issue. There is no switch like `-dPDFSTOPONERROR` to turn repairs into failures: decide in your subscriber what to do with them.
* **Nothing printed.** The SDK never prints to the browser console, so there is nothing like `-q` to silence. Each message carries `level` (`LogLevel.Debug`, `Info`, `Warning`, `Error`), `reason`, a static `message`, `data`, and `stack`; filter by `level` in your subscriber.
* **Throw-versus-return rules.** Functions that modify the document validate first and throw before touching anything; functions that only read return empty values instead of throwing; hooks never throw during render. `Viewport` and `PageCanvas` are the exception: they throw `DocumentNotLoadedError` when rendered without a document.
* **Network errors are yours.** Failures of the file request come from your own `fetch`, as in `DocumentLoader` above.

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

### Before: Ghostscript [#before-ghostscript-9]

In the `gsapi` integration, every job ends with `gsapi_exit` and `gsapi_delete_instance`, as in `run_ghostscript` above: `gsapi_exit` is required once `gsapi_init_with_args` has been called, and comes before deleting the instance. Ghostscript writes temporary files to `/tmp` on Unix, or to the directory in `TMPDIR` or `TEMP`, and after a job that exits on an error some may stay behind. The rendered files stay until your own cleanup removes them:

```sh
TMPDIR=/var/tmp/gs-jobs gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -r144 \
  -sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdf

# Later, when the file changes or the cache expires:
rm -r /var/cache/previews/42 /var/cache/thumbnails/42
```

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

Most of this moves into the SDK and React's component lifecycle:

| Concern            | @sdk-pdf/react                                                                                                                                            |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Instances          | Nothing to create or delete: `PdfProviders` hosts the document state.                                                                                     |
| Rendered files     | Nothing to store: `Viewport` renders in the page, and [`PageCanvas`](https://sdk.pdf.net/docs/v1/reference/PageCanvas) renders lazily and does no work off-screen.           |
| Event listeners    | [`useViewportEvent`](https://sdk.pdf.net/docs/v1/reference/useViewportEvent) subscriptions follow the component lifecycle.                                                   |
| Log subscriptions  | `logMessages.on()` returns an unsubscribe function; call it in the effect cleanup, as in [Errors and logging](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#errors-and-logging).                       |
| Keyboard listeners | [`useHotkeys`](https://sdk.pdf.net/docs/v1/reference/useHotkeys) installs window-level shortcuts; memoize the mapping, and pass a falsy value instead of it to disable them. |
| Closing a document | `resetPdf()` from [`useResetPdf`](https://sdk.pdf.net/docs/v1/reference/useResetPdf) unloads the document and invalidates its ids, snapshots, and revisions.                 |
| Buffers            | Don't reuse a buffer passed to `loadPdf`; copy it first.                                                                                                  |

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

* **The server keeps less state.** The page-image cache and its expiry go away with the preview endpoints; the stored PDF and any conversion outputs remain.
* **Temporary files remain a server concern** only where conversion jobs still run Ghostscript.

## Text extraction [#text-extraction]

Ghostscript extracts text with the `txtwrite` device, which by default writes UTF-8 text that approximates the page layout:

```sh
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=txtwrite \
  -sOutputFile=/srv/text/42.txt /srv/files/42.pdf
```

In `@sdk-pdf/react`, [`useProgrammaticEditor`](https://sdk.pdf.net/docs/v1/reference/useProgrammaticEditor) reads the loaded document's text as paragraphs: `getAllParagraphIds()` lists every paragraph in page and content order, and `getPlainTextParagraphs(ids)` resolves them to entries with `id`, `content`, `pageNumber`, the user-space box `bounds` (`{ x, y, width, height }`), and the paragraph layout, such as `textAlign` and `lineHeight`:

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

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

  return async () => {
    // Loads every page in turn, which takes a while on a large document;
    // getParagraphsByPage(pageId) reads a single page.
    const paragraphIds = await getAllParagraphIds()
    const paragraphs = await getPlainTextParagraphs(paragraphIds)
    return paragraphs.map((paragraph) => paragraph.content).join('\n\n')
  }
}
```

The result is plain paragraph text with bounds in user space, not a layout-preserving text file or XML. For indexing documents that no browser opens, keep `txtwrite` on the server.

## Conversion and optimization [#conversion-and-optimization]

Ghostscript's other jobs don't move to the browser: `pdfwrite` compression and presets, PDF/A and PDF/X, encryption, image exports for email or link previews, PostScript and EPS conversion, and printing. Keep them on the server. What changes is their input: when users edit or fill a document, the current version is the file that `prepareDocument()` uploads, so run the jobs on that:

```sh
# PDF/A-2 from the uploaded file. PDFA_def.ps is your copy of the sample definition file,
# pointing at an ICC profile in a directory that SAFER permits reading.
gs -dSAFER -dBATCH -dNOPAUSE --permit-file-read=/srv/icc/ -sDEVICE=pdfwrite -dPDFA=2 \
  -sColorConversionStrategy=RGB -sBlendConversionStrategy=Simple \
  -sOutputFile=/srv/archive/42.pdf PDFA_def.ps /srv/files/42.pdf
```

`@sdk-pdf/react` doesn't compress, convert, encrypt, or print, so none of these jobs has a browser-side replacement; [Features without an equivalent](https://sdk.pdf.net/docs/v1/migration/from-ghostscript#features-without-an-equivalent) lists them.

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

Plan around these before you migrate:

| Ghostscript feature                                                                                   | In @sdk-pdf/react                                                                                                     |
| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Page images (`png16m`, `jpeg`, and other raster devices) for email, link previews, or other consumers | No equivalent; keep these renders on the server                                                                       |
| `pdfwrite` rewriting: `-dPDFSETTINGS`, `-dCompatibilityLevel`, `-dFastWebView`                        | No equivalent; run it on the server on the bytes from `prepareDocument()`                                             |
| PDF/A and PDF/X output                                                                                | No equivalent                                                                                                         |
| Encryption on save (`-sOwnerPassword`, `-sUserPassword`, `-dPermissions`)                             | No equivalent                                                                                                         |
| Merging files (several inputs to `pdfwrite`)                                                          | No equivalent                                                                                                         |
| PostScript and EPS input, `ps2pdf`                                                                    | PDF only; convert on the server first                                                                                 |
| Printing through printer devices, `ps2write`, or `pxlcolor`                                           | No print API; `PageCanvas` can render pages for your own print layout, as loaded from the file, without session edits |
| Page boxes (`-dUseCropBox`, `-dUseTrimBox`, `-dUseArtBox`, `-dUseBleedBox`)                           | No equivalent                                                                                                         |
| Color management: ICC profiles, overprint simulation, spot color output                               | No equivalent                                                                                                         |
| Print or screen appearances (`-dPrinted`)                                                             | No equivalent                                                                                                         |
| Stopping on repairable problems (`-dPDFSTOPONERROR`, `-dPDFSTOPONWARNING`)                            | No equivalent; `logMessages` reports the problems                                                                     |
| Text files (`txtwrite`) and DOCX output (`docxwrite`)                                                 | `useProgrammaticEditor` reads paragraph text; no file output                                                          |
| N-up layout (`-sNupControl`)                                                                          | No equivalent                                                                                                         |

