Migrating 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/reactis 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
- Read the overview: how the two integrations differ, and the API map for every switch and
gsapicall you use. - List what your back end runs Ghostscript for: previews and thumbnails move to the browser, and conversion jobs stay on the server.
- Install the package and set up
PdfProviders, with a viewer content policy if your previews were images. - Serve the PDF bytes and load them with
loadPdf, including the password flow. - Replace the page images with
Viewport. - Move page numbers and page ranges to page ids.
- Replace re-rendering at a new resolution with the viewport scale.
- Replace thumbnail renders with
PageCanvas. - Let users fill forms in the page.
- Save and download with
prepareDocumentanddownloadDocument. - Move error handling and logging and cleanup.
- Move text extraction, keep conversion on the server, and plan for 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
How the two integrations differ
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
gsexecutable, or the interpreter library (libgs.soon Linux, a DLL on Windows) that C code drives throughgsapi_new_instance,gsapi_init_with_args,gsapi_exit, andgsapi_delete_instance. It's available under the AGPL and under a commercial license. - A job is a command line: input files,
-dand-sswitches, an output device, and an output file. Raster devices such aspng16mandjpegwrite page images; high-level devices such aspdfwriteandtxtwritewrite 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.
PdfProvidershosts the document and viewport state, anduseLoadPdfloads the document from its bytes.Viewportrenders the pages and handles selection, text editing, form filling, link navigation, and zoom gestures, within the limits of the content policy.PageCanvasrenders 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;
useDocumentSavingbuilds 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:
- Move the preview into the browser. The render endpoints, the page-image cache, and the lists of
<img>elements becomeViewportandPageCanvas. - 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. - Replace page numbers with ids at the edges: deep links, analytics, and stored positions.
- Replace resolutions with a scale. A render at
-r144becomessetScale(144 / 72); see Zoom and resolution. - Choose the content policy. An image can't be selected, clicked, or filled; pick how much of that the users get now.
- Keep conversion on the server.
pdfwrite, PDF/A, image exports, and printing stay on the server, now fed the bytes thatprepareDocumentreturns. - Editing is new. Ghostscript has no editing tools, undo, or change tracking, so this guide has no sections for them: Building a PDF application covers interaction tools and history.
API map
Rows marked No equivalent are covered in 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 | 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 | Real pages instead of pictures. |
-dTextAlphaBits, -dGraphicsAlphaBits, -dDownScaleFactor | Nothing to configure | Viewport takes no props. |
.PDFInfo /NumPages, counting the rendered files | usePageCount | 0 while no document is loaded. |
-dFirstPage, -dLastPage, -sPageList | scrollToPage(pageId) from useScrollToPage | The viewport shows the whole document. |
Page n (1-based) | usePages()[n - 1].id with usePages | See Pages and navigation. |
-r per zoom level | useViewportScale | -rN shown at its natural size is setScale(N / 72). |
-g with -dPDFFitPage | useViewportFitScale, useViewportAutoFitScale | Fits into the viewport, not into a pixel box. |
| Low-resolution thumbnail renders | 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, useWidgetValue | Users fill fields in Viewport under PDF_VIEWER. |
-sDEVICE=pdfwrite to produce the downloadable PDF | prepareDocument() and downloadDocument() from 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 | Paragraph text and bounds, not a text file. |
gsapi_* return codes | The rejected promise or the thrown error | See Errors and logging. |
gsapi_set_stdio() callbacks, -q, -dQUIET | 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 closes the document | See 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. |
Install and set up
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:
./configure
make
make install
gs -hThe 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:
#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
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 has the details:
@sdk-pdf:registry=https://registry.sdk.pdf.net
//registry.sdk.pdf.net/:_authToken=${PDF_NET_REGISTRY_TOKEN}npm install @sdk-pdf/reactSet up the project as Getting started 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. Its props must keep their identity between renders, so define static configuration outside the component:
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
- 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.
PdfProvidersdefaults to theselecttool andCONTENT_POLICY_PRESET.PDF_EDITOR, which makes text, annotations, links, and widgets editable. For a reader, start withPDF_VIEWER(text and annotations read-only, links and widgets interactive) and thetextSelecttool, as above;PDF_PRINTERalso 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 thetextSelecttool; the levels are listed inuseSetInteractionMode.'hidden'works for every content kind, and a level can also be a function of the block id, such aswidgets: (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
Before: Ghostscript
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:
gs -dSAFER -dBATCH -dNOPAUSE -sPDFPassword="$DOCUMENT_PASSWORD" \
-sDEVICE=png16m -r144 \
-sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdfEach request runs a new job, so no document stays open between requests.
After: @sdk-pdf/react
The browser fetches the file and passes the bytes to loadPdf from useLoadPdf; the load state decides what to render. When the document id changes, resetPdf() from useResetPdf unloads the open document, the way each Ghostscript job started from a clean state:
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:
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
- 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.
loadPdftakes aUint8Array, so headers, cookies, and caching belong to yourfetchand your endpoint.-dSAFERand--permit-file-readhave 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 withInvalidPasswordError. - Keep a copy for retries. The document reads from the given buffer for its whole lifetime, and another
loadPdfcall can detach it. PassbytesRef.current.data.slice()and keep the original. - PDF only.
useLoadPdfloads 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
Before: Ghostscript
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):
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -r144 \
-dTextAlphaBits=4 -dGraphicsAlphaBits=4 \
-sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdfThe %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
Viewport renders the pages, and your components render everything around them, as Building a PDF application walks through:
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
- 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-dDownScaleFactorhave no counterpart:Viewporttakes no props, andPdfProvidersconfigures the theme, the interaction mode, and the gestures.-rbecomes the viewport scale; see 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.
ViewportthrowsDocumentNotLoadedErrorwhen no document is loaded, so render it once the load state is'ready'. - Styling.
theme.classNamesandtheme.stylestarget the SDK's elements, such asscrollViewportandpage, andtheme.viewportPaddingsets the gap around the pages; see Theming and customization. - No page boxes or color management.
-dUseCropBox,-dUseTrimBox,-dUseArtBox,-dUseBleedBox, and the ICC profile switches have no equivalent.
Pages and navigation
Before: Ghostscript
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:
# 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
usePages returns the pages in document order, so a page number is a position in that list. useScrollToPage scrolls to a page id, and useCurrentPageId re-renders with the page in the middle of the viewport:
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 re-renders only when the count changes.
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: a zero-based index or undefined |
| A page id, rendered | Its position | usePageIndexGetter paired with useRenderOnPageOrderChange |
| Only the count | usePageCount(), 0 while no document is loaded |
What to know
- Ranges become scroll targets.
-dFirstPage,-dLastPage, and-sPageListchoose what to render;Viewportshows the document's pages in one scrollable area, so a page range in a deep link becomesscrollToPageto 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,usePageDelete,usePageMove,usePageDuplicate, anduseSetPageRotationedit 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.
usePageIndexGetterreturnsundefinedfor an unknown id, andscrollToPagethrowsInvalidInputError.
Zoom and resolution
Before: Ghostscript
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:
# 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.pdfAfter: @sdk-pdf/react
useViewportScale reads and sets the scale, useViewportFitScale calculates fit scales on demand, and useViewportAutoFitScale (in PdfApplication above) fits once per loaded document:
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
- 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
-rNand shown at its natural size issetScale(N / 72):-r72is1(100%), and-r144is2. 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.
useViewportAutoFitScaleapplies when a document loads and doesn't re-apply on viewport resizes or manual zooming;useViewportFitScalereflects the viewport size at the moment of the call.fitWidthScalefits the widest page andfitHeightScalethe tallest, both accounting forviewportPadding. - Gestures are built in. Pinch and wheel zoom go through the same scale and are tuned with
zoomGesturesonPdfProviders(minScale0.5andmaxScale4by default). During a gesture,scalekeeps its value until the gesture ends. - Zoom around a point.
setScale(scale, pivot?)keeps the document still atpivot, a point{ x, y }given as fractions of the viewport area (0the top or left edge,1the bottom or right edge), the center by default. The third item ofuseViewportScale(),animateScale(scale, pivot?), animates the change and resolves when it ends. Both throwInvalidInputErrorunless the scale is a positive finite number.
Thumbnails
Before: Ghostscript
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:
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -g160x208 -dPDFFitPage \
-dTextAlphaBits=4 -dGraphicsAlphaBits=4 \
-sOutputFile=/var/cache/thumbnails/42/page-%03d.png /srv/files/42.pdfAfter: @sdk-pdf/react
PageCanvas renders one page onto a canvas anywhere outside the main view; with usePages and useScrollToPage, it makes a navigation sidebar:
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
- Sized with CSS, not -g. The render resolution follows the canvas's displayed CSS size, or an explicit
scaleprop, 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
isVisibleto skip the per-canvas intersection observer. - Annotations.
PageCanvasdraws annotations, form widgets included;hideAnnotationsleaves them all out, like-dShowAnnots=falseand-dShowAcroForm=falsetogether.hideExtractableTextskips the text content. - Thumbnails that show session edits are coming soon.
PageCanvasdraws 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.
PageCanvasthrowsDocumentNotLoadedErrorwhen no document is loaded.
Forms
Before: Ghostscript
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:
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -r144 -dPrinted=false \
-sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdfThe image shows the fields drawn into the page; users can't type into a picture.
After: @sdk-pdf/react
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 lists the widgets in reading order, and useWidgetValue binds one widget's value like a useState backed by the document. Working with forms builds a full sidebar:
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
- Fields are live. Under
PDF_VIEWER, users fill fields in the page; underPDF_PRINTER, widgets are read-only, the closest match to an image. - Values are shaped like HTML form controls:
textandchoiceare strings,checkboxandradiobooleans, andmultiChoicethe 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.
useWidgetValuebinds one widget per hook call. - Display switches.
hideAnnotationsonPageCanvashides annotations and form widgets together, like-dShowAnnots=falsewith-dShowAcroForm=false. InViewport, the content policy levelsannotations: 'hidden'andwidgets: 'hidden'leave them out separately.-dPrintedhas no equivalent. - The filled PDF comes from the browser.
prepareDocument()returns the document with the entered values; see Saving and downloading. - Date fields. A text widget's
formatexposes Acrobat date formats asFormatType.DateTime, recognized without running scripts; seeFormatType.
Saving and downloading
Before: Ghostscript
The download is the stored file, or a copy that pdfwrite rewrites on the way out, such as with a -dPDFSETTINGS preset:
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -dPDFSETTINGS=/ebook \
-sOutputFile=/var/cache/downloads/42.pdf /srv/files/42.pdfpdfwrite 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
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:
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
- Applied, not redrawn.
pdfwritecreates 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 toloadPdfcomes back with no copy. Set it tofalsewhen 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-sPageListwithpdfwrite. - No optimization options. There is no
-dPDFSETTINGS, compatibility level, linearization (-dFastWebView), or encryption on save, anddownloadDocument()has no file name option. Run those jobs on the server on the uploaded bytes; see Conversion and optimization. - Check post-processing with forms. If the server still runs
pdfwriteon 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
Before: Ghostscript
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:
gs -q -dSAFER -dBATCH -dNOPAUSE -dPDFSTOPONERROR -sDEVICE=png16m -r144 \
-sOutputFile=/var/cache/previews/42/page-%03d.png /srv/files/42.pdfThrough 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:
#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
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. Error handling and logging has the full rules:
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
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
- Repairs are log messages. Like Ghostscript, the SDK works around malformed documents; it reports each problem as a log message whose
reasonnames the kind of problem, such as a document format issue. There is no switch like-dPDFSTOPONERRORto 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
-qto silence. Each message carrieslevel(LogLevel.Debug,Info,Warning,Error),reason, a staticmessage,data, andstack; filter bylevelin 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.
ViewportandPageCanvasare the exception: they throwDocumentNotLoadedErrorwhen rendered without a document. - Network errors are yours. Failures of the file request come from your own
fetch, as inDocumentLoaderabove.
Cleanup and lifecycle
Before: Ghostscript
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:
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/42After: @sdk-pdf/react
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 renders lazily and does no work off-screen. |
| Event listeners | 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. |
| Keyboard listeners | 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 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
- 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
Ghostscript extracts text with the txtwrite device, which by default writes UTF-8 text that approximates the page layout:
gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=txtwrite \
-sOutputFile=/srv/text/42.txt /srv/files/42.pdfIn @sdk-pdf/react, 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:
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
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:
# 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 lists them.
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 |