Migrating from pdf-lib
This guide moves the browser side of a pdf-lib integration to @sdk-pdf/react. pdf-lib loads, creates, modifies, and saves PDF bytes in any JavaScript runtime, and has no rendering or UI: apps show its output in an <iframe> or another viewer. @sdk-pdf/react is a React library for the browser that shows the document, lets users edit it, and builds the final PDF when you save. The guide covers the parts users see and change; pdf-lib code that runs without a browser, such as document generation on a server, has no equivalent and can stay where it runs. Work through the checklist below; each step links to the section that covers it.
Beta:
@sdk-pdf/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 pdf-lib call you use.
- Sort your pdf-lib code by where it runs: the browser code that shows and changes documents for users moves; server jobs stay (see Features without an equivalent).
- Install the package and set up
PdfProviderswith the content policy your users need. - Load and reset documents with
loadPdfandresetPdf, including the password flow for encrypted files. - Replace the Blob URL and
<iframe>preview withViewport. - Move page operations from indices to page ids.
- Move splitting to
prepareDocumentand plan for merging. - Move form filling and field creation to widgets.
- Replace
drawTextanddrawImagewith paragraphs and image stamps. - Replace
save()and Blob downloads withuseDocumentSaving. - Move error handling and logging, and plan for features without an equivalent.
- Remove
pdf-liband@pdf-lib/fontkitfrom the browser bundle, unless you keep merging there, along with the Blob URL and<iframe>code. Test encrypted, malformed, large, and form-heavy documents, page operations, and the save round-trip.
Overview
How the two integrations differ
pdf-lib creates and modifies PDF files in any JavaScript runtime (browsers, Node, Deno, and React Native) and has no rendering or UI. An integration built on it works on bytes, step by step:
PDFDocument.load()parses aUint8Array, anArrayBuffer, or a base64 string into an in-memory document;PDFDocument.create()starts an empty one.- Pages are
PDFPageobjects at zero-based indices:getPage(index),insertPage(index),removePage(index). - New content is drawn onto pages with
drawText,drawImage, and vector drawing calls. Text already on a page can't be read or edited; only form field values can. - Forms are reached through
getForm(), with fields looked up by fully qualified name. save()serializes the document to aUint8Array. To show the result, apps hand the bytes to an<iframe>or a viewer library, and save again after every change.- Encrypted documents aren't supported:
load()throwsEncryptedPDFError.
@sdk-pdf/react owns the document, and you own the UI:
PdfProvidershosts the document and the configuration; every hook and component renders inside it.useLoadPdfloads a document from bytes, encrypted documents included.Viewportshows the pages and handles selection, text editing, form filling, and zoom on its own; changes appear as they happen.- Pages, paragraphs, and widgets are addressed by session-scoped ids.
- Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers undo/redo;
useDocumentSavingbuilds the final PDF when you need the bytes.
| Concern | pdf-lib | @sdk-pdf/react |
|---|---|---|
| Runtime | Any JavaScript runtime: browsers, Node, Deno, React Native | The browser, in a React app |
| Style | PDFDocument objects and promises | A React provider, state hooks, and command hooks |
| Display | None: saved bytes go to an <iframe> or a viewer library | Viewport for the main view, PageCanvas for thumbnails |
| Page addressing | Zero-based indices | Session-scoped page ids from usePages |
| Page text | Drawn onto pages; existing text can't be read or edited | Paragraphs that users edit in place and code reads and rewrites |
| Forms | Fields by fully qualified name | Widgets with a fieldName |
| Change history | None | Undo/redo and document revisions |
| Encrypted files | Not supported (EncryptedPDFError) | Opened with the password |
| New documents | PDFDocument.create() | Load an existing PDF |
| Output | save(), saveAsBase64() | prepareDocument(), downloadDocument() |
| Diagnostics | Thrown errors | Typed errors thrown before any change; logMessages |
What this means for your code:
- Split by runtime. Code that runs in Node, Deno, or React Native, such as generating or merging documents on a server, has no equivalent and stays where it is. The browser code that shows documents to users and changes them moves to
@sdk-pdf/react. - Drop the save-to-preview loop. Calling
save(), building a Blob URL, and reloading an<iframe>after each change become oneViewportthat shows edits live. - Replace indices with ids. Every page reference in your state becomes a page id; convert at the edges (deep links, stored page numbers) with
usePages. - Let users do the editing. Hard-coded
drawTextanddrawImagecalls often become user actions: theaddTextandaddImagetools, typing in existing paragraphs, and filling fields in place. Code that still writes text uses the programmatic editor, and code that still places images creates stamp annotations. - Rethink forms as widgets. Field lookups by name become widget lists, and values are read and written through
useWidgetValuein components. - Choose the content policy. It decides what users may change: everything by default (
PDF_EDITOR), or form fields only (PDF_VIEWER). - Move into React. Async functions that drive a
PDFDocumentbecome hooks and components underPdfProviders.
pdf-lib has no viewer, so zoom, thumbnails, and undo have no pdf-lib counterpart and no section in this guide. @sdk-pdf/react gives your users all three: zoom gestures and useViewportScale, thumbnails with PageCanvas, and undo and redo with useUndoRedo. Building a PDF application shows each part. There is no cleanup and lifecycle section either: pdf-lib has nothing to destroy, and in @sdk-pdf/react, resetPdf() from useResetPdf closes a document.
API map
Rows marked No equivalent are covered in Features without an equivalent.
| pdf-lib | @sdk-pdf/react | Notes |
|---|---|---|
npm install pdf-lib, @pdf-lib/fontkit, registerFontkit() | The package and PdfProviders | No font setup. |
PDFDocument.load(bytes) | loadPdf(data) from useLoadPdf | loadPdf takes a Uint8Array. |
PDFDocument.load() on an encrypted file, EncryptedPDFError, ignoreEncryption | loadPdf(data, { password }), catch InvalidPasswordError | Encrypted documents open with the password. |
PDFDocument.create() | No equivalent | Load a PDF file, such as a blank template. |
| Loading another document | resetPdf() from useResetPdf, then loadPdf | |
save() into a Blob URL or saveAsBase64({ dataUri: true }) for an <iframe> | Viewport | Shows changes live, with no save. |
pdfDoc.getPageCount() | usePageCount | 0 while no document is loaded. |
pdfDoc.getPages(), getPage(index) | usePages, usePages()[index].id | Entries carry id, width, height, and rotation. |
page.getSize(), page.getRotation() | width, height, and rotation of a usePages entry | Sizes have the rotation applied. |
addPage([width, height]), addPage(PageSizes.A4) | createPage(width, height) from usePageCreate | Appends; the size is required. |
insertPage(index, size) | createPage(width, height, insertAfterPageId) | Inserts after a page id. |
removePage(index) | deletePages(pageIdOrIds) from usePageDelete | |
page.setRotation(degrees(angle)) | setPageRotation(pageIdOrIds, angle) from useSetPageRotation | A plain number of degrees. |
| — | moveBefore, moveAfter from usePageMove | |
| — | duplicatePage(sourcePageId, insertAfterPageId?) from usePageDuplicate | Copies a page within the loaded document. |
copyPages(srcDoc, indices) into a new document, to split | prepareDocument({ pageIds }) from useDocumentSaving | Saves only the listed pages, in the listed order. |
copyPages(srcDoc, indices) from another document, to merge | No equivalent | See Merge and split documents. |
embedPdf(), embedPage(), page.drawPage() | No equivalent | |
setSize(), setMediaBox(), setCropBox(), scale(), scaleContent() | No equivalent | |
form.getFields() | useWidgets, useWidgetsGetter | Widgets in reading order, not fields. |
getTextField(name).setText(), getText() | useWidgetValue on a text widget | One widget per hook call. |
getCheckBox(name).check(), uncheck(), isChecked() | useWidgetValue on a checkbox widget | A boolean. |
getRadioGroup(name).select(option), getDropdown(), getOptionList() | useWidgetValue on radio, choice, and multiChoice widgets | See Forms. |
createTextField(name) and the other create methods, addToPage(), addOptionToPage() | createWidgets(props) from useWidgetsCreate | One widget per props object. |
field.enableReadOnly() | setWidgetProps(ids, { isReadOnly: true }, { excludeFromHistory: true }) from useSetWidgetProps | |
setMaxLength(), enableMultiline(), enableCombing(), setAlignment(), setFontSize() | maxLength, isMultiline, isMonospaced, horizontalAlign, fontSize widget props | |
form.removeField(field) | deleteBlocks(widgetIds) from useBlockDelete | The stored field value stays. |
form.flatten() | No equivalent | Lock widgets with isReadOnly. |
setImage() on text fields and buttons | No equivalent | |
page.drawText(text, { x, y }) | createParagraph(pageId, x, y) from useParagraphCreate, then insertText(text) | An editable paragraph. |
drawText options maxWidth, lineHeight | setParagraphProps([paragraphId], { width, lineHeight }) from useSetParagraphProps | Also sets alignment, indent, and rotation. |
| — | getParagraphsByPage(pageId) from useProgrammaticEditor | Reads the text already on a page. |
embedFont(), widthOfTextAtSize(), heightAtSize() | No equivalent | Fonts come from useSelectionTextStyle. |
embedPng(), embedJpg(), page.drawImage() | stageImage, then useStampAnnotationCreate | A stamp annotation; or the user places it with the addImage tool. |
drawRectangle(), drawLine(), drawCircle(), drawEllipse(), drawSvgPath(), pushOperators() | No equivalent | |
pdfDoc.save() | prepareDocument() from useDocumentSaving | Resolves with a Uint8Array. |
| A Blob and a temporary download link | downloadDocument() | No file name option. |
saveAsBase64(), SaveOptions | No equivalent | The save options are returnOriginalIfUnchanged and pageIds. |
setTitle(), getTitle(), and the other metadata methods | No equivalent | |
catalog.getOrCreateViewerPreferences(), attach(), addJavaScript() | No equivalent | |
EncryptedPDFError and other thrown errors | InvalidPasswordError and the other SDK error classes, logMessages | See Errors and logging. |
Install and set up
Before: pdf-lib
pdf-lib installs as pdf-lib. Custom fonts need a second package, @pdf-lib/fontkit, registered on each document before a font is embedded:
npm install --save pdf-lib @pdf-lib/fontkitimport { PDFDocument } from 'pdf-lib'
import fontkit from '@pdf-lib/fontkit'
const pdfDoc = await PDFDocument.create()
pdfDoc.registerFontkit(fontkit)Without a bundler, the UMD build exposes the same API on a global PDFLib.
After: @sdk-pdf/react
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. The configuration decides what users may do: an editor starts with everything editable, and a form-filling app with read-only text and fillable forms. Keep its identity stable: define it outside the component and read the prop once:
import { useState } from 'react'
import { CONTENT_POLICY_PRESET, InteractionTool, PdfProviders } from '@sdk-pdf/react'
import '@sdk-pdf/react/style.css'
const selectTool: InteractionTool = { type: 'select' }
const textSelectTool: InteractionTool = { type: 'textSelect' }
const editorConfig = {
initialInteractionTool: selectTool,
initialContentPolicy: CONTENT_POLICY_PRESET.PDF_EDITOR,
}
const formFillerConfig = {
initialInteractionTool: textSelectTool,
initialContentPolicy: CONTENT_POLICY_PRESET.PDF_VIEWER,
}
export function App({ canEditContent }: { canEditContent: boolean }) {
// Read the prop once: the initial values apply when the viewport first mounts, and a
// prop that changes identity re-renders the PDF application. Switch later with
// useSetInteractionMode.
const [config] = useState(() => (canEditContent ? editorConfig : formFillerConfig))
return (
<PdfProviders {...config}>
<DocumentLoader />
</PdfProviders>
)
}What to know
- Browser and React only. The SDK renders React components in the browser. Node, Deno, and React Native code that uses pdf-lib has no counterpart here.
- No font setup. There is no fontkit to register and no font file to embed. The fonts users can pick are the
fontFamily.optionsofuseSelectionTextStyle:regularfamilies, always available, andextractedones, taken from the document. - Pick the content policy.
PdfProvidersdefaults to theselecttool andCONTENT_POLICY_PRESET.PDF_EDITOR, where text, annotations, links, and widgets are all editable.PDF_VIEWERkeeps text and annotations read-only and lets users fill forms;PDF_PRINTERmakes everything read-only, with links still clickable. The initial values apply when the viewport first mounts; switch later withuseSetInteractionMode.
Load and reset a document
Before: pdf-lib
PDFDocument.load() takes a Uint8Array, an ArrayBuffer, or a base64 string or data URI. pdf-lib can't decrypt documents: an encrypted file makes load() throw EncryptedPDFError:
import { EncryptedPDFError, PDFDocument } from 'pdf-lib'
async function openFile(file) {
try {
return await PDFDocument.load(await file.arrayBuffer())
} catch (error) {
if (error instanceof EncryptedPDFError) {
showError('Encrypted documents are not supported')
return null
}
throw error
}
}The ignoreEncryption: true load option opens an encrypted file without decrypting it, so later changes may fail or behave unexpectedly. To work on another document, you load another PDFDocument.
After: @sdk-pdf/react
loadPdf(pdfFileContent: Uint8Array, options?: { password?: string }): Promise<void> from useLoadPdf loads the document and drives the load state. An encrypted file opens once the user enters its password:
import { useRef, useState } from 'react'
import { InvalidPasswordError, useLoadPdf, useResetPdf } from '@sdk-pdf/react'
function DocumentLoader() {
const [loadState, loadPdf] = useLoadPdf()
const bytesRef = useRef<Uint8Array | null>(null)
const [isPasswordNeeded, setIsPasswordNeeded] = useState(false)
async function open(password?: string) {
if (!bytesRef.current) return
try {
// The document reads from the buffer it receives for its whole lifetime,
// so keep the original for a password retry and pass a copy.
await loadPdf(bytesRef.current.slice(), { password })
setIsPasswordNeeded(false)
} catch (error) {
if (error instanceof InvalidPasswordError) {
// Where pdf-lib stops at EncryptedPDFError, ask for the password and open again.
setIsPasswordNeeded(true)
} else {
showError(error)
}
}
}
async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.currentTarget.files?.[0]
if (!file) return
// loadPdf takes a Uint8Array: wrap the ArrayBuffer.
bytesRef.current = new Uint8Array(await file.arrayBuffer())
await open()
}
if (isPasswordNeeded)
return <PasswordPrompt onSubmit={(password) => void open(password)} />
if (loadState.status === 'initial')
return <input type="file" accept=".pdf" onChange={handleFileChange} />
if (loadState.status === 'loading') return <p>Loading the document…</p>
return <PdfApplication />
}
function CloseButton() {
const resetPdf = useResetPdf()
// The load state returns to 'initial', and the file input shows again.
return <button onClick={() => resetPdf()}>Close</button>
}What to know
- Bytes as a Uint8Array. Wrap an
ArrayBufferinnew Uint8Array(buffer), and decode base64 strings and data URIs to bytes before callingloadPdf. A URL becomes afetchof your own. - Encrypted files open. Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with
InvalidPasswordError.ignoreEncryptionhas no counterpart: an encrypted document opens with its password or not at all. - One load option.
passwordis the only option ofloadPdf;parseSpeed,updateMetadata,throwOnInvalidObject, andcapNumbershave no equivalent. - Keep a copy for retries. Anything else touching the buffer passed to
loadPdf, such as anotherloadPdfcall, can detach it, and every later document read throws. To reuse the bytes, passdata.slice(). - No empty documents.
PDFDocument.create()has no equivalent: load an existing PDF, such as a blank template your app ships. - Reset invalidates ids. After
resetPdf()fromuseResetPdf, page and block ids, widget snapshots, and document revisions from the old document are no longer valid.
Display the document
Before: pdf-lib
pdf-lib can't render pages. To show a document, apps save it and hand the bytes to an <iframe>, as a Blob URL or as a data URI from saveAsBase64({ dataUri: true }), and repeat that after every change:
const pdfBytes = await pdfDoc.save()
const blob = new Blob([pdfBytes], { type: 'application/pdf' })
document.getElementById('preview').src = URL.createObjectURL(blob)Apps that need more control render the saved bytes with a separate library, such as pdf.js.
After: @sdk-pdf/react
Viewport renders the loaded document and every change made to it, by users or by your code, with nothing to save first. It takes no props: PdfProviders configures it and hooks control it:
import { useViewportAutoFitScale, Viewport } from '@sdk-pdf/react'
function PdfApplication() {
// Opens at the page width, once per loaded document.
useViewportAutoFitScale('width')
return (
<div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: '100vh' }}>
{/* Your controls: the page, form, and save actions from this guide. */}
<Toolbar />
{/* The viewport fills its parent, so the parent needs an explicit size. */}
<div style={{ minHeight: 0 }}>
<Viewport />
</div>
</div>
)
}What to know
- No save-and-reload loop. The Blob URL, the data URI, and the
<iframe>go away.prepareDocument()is needed only when you want the file, as in Saving and downloading. - Render only when ready.
ViewportthrowsDocumentNotLoadedErrorwhen no document is loaded, so render it when the load state is'ready'. - Interaction is built in. Scrolling, zoom gestures, selection, typing, and form filling work inside
Viewport, limited by the content policy. Toolbars, page navigation withuseScrollToPage, and zoom controls withuseViewportScaleare your components. - Styling is yours. The SDK renders unstyled; class names, inline styles, and CSS variables go through the
themeconfiguration, as Theming and customization describes.
Pages
Before: pdf-lib
pdf-lib addresses pages by zero-based index and takes sizes as [width, height] pairs, often from PageSizes:
import { degrees, PageSizes } from 'pdf-lib'
const pageCount = pdfDoc.getPageCount()
const { width, height } = pdfDoc.getPage(0).getSize()
// Append an A4 page, and insert a Letter page as the second page.
pdfDoc.addPage(PageSizes.A4)
pdfDoc.insertPage(1, PageSizes.Letter)
// Remove the last page.
pdfDoc.removePage(pdfDoc.getPageCount() - 1)
// Turn the first page by another quarter turn.
const firstPage = pdfDoc.getPage(0)
firstPage.setRotation(degrees(firstPage.getRotation().angle + 90))After: @sdk-pdf/react
usePages returns the pages in document order, so an index is a position in that list. The page commands take page ids: usePageCreate, usePageDelete, usePageMove, and useSetPageRotation:
import {
usePageCreate,
usePageDelete,
usePageMove,
usePages,
useSetPageRotation,
} from '@sdk-pdf/react'
// PageSizes.A4 and PageSizes.Letter, in user-space units.
const A4 = { width: 595.28, height: 841.89 }
const LETTER = { width: 612, height: 792 }
function PageActions() {
const pages = usePages()
const createPage = usePageCreate()
const deletePages = usePageDelete()
const { moveBefore } = usePageMove()
const setPageRotation = useSetPageRotation()
if (pages.length === 0) return null
const firstPage = pages[0]
const lastPage = pages[pages.length - 1]
function insertAtStart() {
// insertPage(0, PageSizes.Letter): create the page, then move it to the start.
const pageId = createPage(LETTER.width, LETTER.height)
moveBefore(pageId)
}
return (
<div role="toolbar">
{/* addPage(PageSizes.A4): without a target page, the new page is appended. */}
<button onClick={() => createPage(A4.width, A4.height)}>Add page</button>
{/* insertPage(1, PageSizes.Letter): the new page goes after the first one. */}
<button onClick={() => createPage(LETTER.width, LETTER.height, firstPage.id)}>
Insert as page 2
</button>
<button onClick={insertAtStart}>Insert at start</button>
<button disabled={pages.length <= 1} onClick={() => deletePages(lastPage.id)}>
Delete last page
</button>
{/* Rotation is absolute: add to the current angle yourself. */}
<button onClick={() => setPageRotation(firstPage.id, firstPage.rotation + 90)}>
Rotate first page
</button>
</div>
)
}From indices to ids
| pdf-lib | @sdk-pdf/react |
|---|---|
pdfDoc.getPage(index) | usePages()[index]: an entry with id, width, height, and rotation |
pdfDoc.getPageCount() | usePageCount, which re-renders only when the count changes |
| The index of a page you hold | getPageIndex(pageId) from usePageIndexGetter: a zero-based index, or undefined for an unknown id |
page.getSize(), getWidth(), getHeight() | width and height of the entry, with the rotation applied |
page.getRotation().angle | rotation of the entry: 0, 90, 180, or 270 |
What to know
- Ids, not indices. An id keeps pointing at the same page while pages are added, removed, and moved by the user or your code, where an index would shift to another page. Ids are session-scoped: store page numbers, and resolve them to ids once the document is ready.
- Sizes are required.
createPage(width, height, insertAfterPageId?)always takes the size in user-space units, one unit being 1/72 of an inch; thePageSizespairs carry over as numbers. pdf-lib'sgetSize()reads the MediaBox, whileusePagessizes have the rotation applied. - Positions are page ids. A new page goes after
insertAfterPageId, or at the end without it. For the first position, create the page and callmoveBefore(pageId), which moves pages to the start when no target page is given. - Rotation is a number.
setPageRotation(pageIdOrPageIds, rotation)takes degrees instead ofdegrees(...). It sets an absolute angle;-90and450are normalized, and an angle off a quarter turn throwsInvalidInputError. - Undo. Deleting, moving, or rotating several pages in one call is a single undo step, and edits made within 500 ms of each other merge into one step, even across hook calls. To keep an operation a separate step, call
markUndoBoundary()fromuseUndoBoundarybefore it starts and after its edits finish. An undo can restore a deleted page with its content. - New operations.
moveBeforeandmoveAfterreorder pages, andduplicatePagefromusePageDuplicatecopies one. Page boxes and resizing (setSize,setMediaBox,setCropBox,scale,scaleContent) have no equivalent.
Merge and split documents
Before: pdf-lib
pdf-lib merges documents by loading each source and copying its pages into the target with copyPages, followed by addPage or insertPage. It splits a document by copying some of its pages into a document from PDFDocument.create(), and draws pages of other documents onto a page with embedPdf, embedPage, and drawPage:
import { PDFDocument } from 'pdf-lib'
// Merge: copy every page of each source into a new document.
const mergedPdf = await PDFDocument.create()
for (const sourceBytes of [firstPdfBytes, secondPdfBytes]) {
const sourcePdf = await PDFDocument.load(sourceBytes)
const copiedPages = await mergedPdf.copyPages(sourcePdf, sourcePdf.getPageIndices())
for (const page of copiedPages) mergedPdf.addPage(page)
}
// Split: copy the first two pages into a document of their own.
const firstPartPdf = await PDFDocument.create()
const [firstPage, secondPage] = await firstPartPdf.copyPages(mergedPdf, [0, 1])
firstPartPdf.addPage(firstPage)
firstPartPdf.addPage(secondPage)
const mergedBytes = await mergedPdf.save()
const firstPartBytes = await firstPartPdf.save()After: @sdk-pdf/react
Splitting becomes a save option: prepareDocument({ pageIds }) from useDocumentSaving builds a PDF of the listed pages only, in the listed order, with the session's edits:
import { useDocumentSaving, usePages } from '@sdk-pdf/react'
// Replaces copying pages into a new document: the first two pages become a file of their
// own.
function useSaveFirstPart() {
const pages = usePages()
const { prepareDocument } = useDocumentSaving()
return () => prepareDocument({ pageIds: pages.slice(0, 2).map((page) => page.id) })
}Merging has no equivalent: the page hooks work on the loaded document, and there is no API to import pages from another file. Keep merging where it runs today, on your server or in the browser with pdf-lib, and pass the resulting bytes to loadPdf. Inside the loaded document:
duplicatePage(sourcePageId, insertAfterPageId?)fromusePageDuplicatecopies a page with all its content and resolves with the new page's id;moveBeforeandmoveAfterfromusePageMovereorder pages, several at once;deletePagesfromusePageDeleteremoves them.
Each page goes into a saved file once: to repeat a page, duplicate it first. Saving some of the pages doesn't change the loaded document.
Forms
Before: pdf-lib
getForm() returns the document's form. Fields are looked up by fully qualified name and typed by getter; a missing name, or a field of another type, throws:
const form = pdfDoc.getForm()
form.getTextField('customer.name').setText('Jane Doe')
form.getCheckBox('terms.accepted').check()
form.getRadioGroup('delivery').select('Express')
const customerName = form.getTextField('customer.name').getText()
// A new radio group: each addOptionToPage call adds one widget.
const page = pdfDoc.getPage(0)
const contactField = form.createRadioGroup('contact')
contactField.addOptionToPage('Email', page, { x: 72, y: 540, width: 16, height: 16 })
contactField.addOptionToPage('Phone', page, { x: 72, y: 510, width: 16, height: 16 })
// Lock every field before the document goes out.
for (const field of form.getFields()) field.enableReadOnly()
const pdfBytes = await pdfDoc.save()After: @sdk-pdf/react
Users fill fields in Viewport when the content policy lets them interact with widgets. For values your code sets or reads, the unit is the widget: useWidgets lists the widgets in reading order, each with its fieldName, and useWidgetValue binds one widget's value like a useState backed by the document. Working with forms covers the model:
import { useEffect, useRef } from 'react'
import {
TextWidget,
useWidgets,
useWidgetsCreate,
useWidgetValue,
Widget,
} from '@sdk-pdf/react'
function isTextWidget(widget: Widget): widget is TextWidget {
return widget.widgetType === 'text'
}
// Replaces form.getTextField(name).setText(text) for each entry of `values`.
function PrefillTextFields({ values }: { values: Record<string, string> }) {
const { widgets, isLoaded } = useWidgets()
// Wait for the complete list: it's partial until every page with widgets has loaded.
if (!isLoaded) return null
return (
<>
{widgets
.filter(isTextWidget)
.filter((widget) => widget.fieldName in values)
.map((widget) => (
<PrefillTextField
key={widget.id}
widget={widget}
text={values[widget.fieldName]}
/>
))}
</>
)
}
function PrefillTextField({ widget, text }: { widget: TextWidget; text: string }) {
const [, setValue] = useWidgetValue(widget)
const isPrefilled = useRef(false)
useEffect(() => {
// Write once, so what the user types afterwards stays.
if (isPrefilled.current) return
isPrefilled.current = true
setValue(text)
}, [setValue, text])
return null
}
// Replaces createRadioGroup(name) and one addOptionToPage call per option.
function useAddContactField() {
const createWidgets = useWidgetsCreate()
return (pageId: string) => {
// Radio widgets that share a field name form one group.
const option = {
pageId,
widgetType: 'radio',
x: 72,
width: 16,
height: 16,
fieldName: 'contact',
} as const
return createWidgets([
{ ...option, y: 540, checkedValue: 'Email' },
{ ...option, y: 510, checkedValue: 'Phone' },
])
}
}Render PrefillTextFields once inside PdfApplication, with your values keyed by field name.
Mapping the field API
| pdf-lib | @sdk-pdf/react |
|---|---|
PDFTextField: setText(), getText() | A text widget; its value is a string |
PDFCheckBox: check(), uncheck(), isChecked() | A checkbox widget; its value is a boolean |
PDFRadioGroup: select(option), getSelected() | Several radio widgets sharing a field name, each with a checkedValue; each widget's value is a boolean, whether it's checked |
PDFDropdown: select(), addOptions(), enableEditing() | A choice widget with isCombo; its value is a string. Options are the options prop, and allowsCustomValue accepts typed text |
PDFOptionList, with or without enableMultiselect() | A choice widget without isCombo, a list box; with multiple selection, a multiChoice widget, whose value is the selected option indices |
PDFButton: addToPage(text, page) | A pushButton widget with a caption; it has no value |
PDFRadioGroup: enableOffToggling(); mutual exclusion (isMutuallyExclusive() and its setters) | canToggleToOff: true; isInUnison, the inverse of mutual exclusion: options sharing a checkedValue check together |
PDFTextField: setMaxLength(), enableMultiline(), enableCombing() | The maxLength, isMultiline, and isMonospaced props |
setAlignment(), setFontSize() | The horizontalAlign and fontSize props; a fontSize of 0 sizes the text automatically |
addToPage() and addOptionToPage() options, such as backgroundColor | createWidgets(props): pageId, widgetType, x, y, width, and height are required; fillColor, borderColor, and the other widget props are optional |
What to know
- Lookups find nothing instead of throwing. Where
getTextField(name)throws for a missing name or another field type, filteringuseWidgets()byfieldNameandwidgetTypereturns no widget. For a one-time read inside a handler,await getWidgets()fromuseWidgetsGetterloads every page with widgets first. - Values are read and written per widget.
useWidgetValuebinds one widget per hook call, inside a component, so a fill by name becomes a component per widget, likePrefillTextFieldabove. Its value follows the document: filling in the main view and undo/redo show up as they happen. - Field names are shared, not unique.
createTextField(name)throws when the name exists.createWidgetsfromuseWidgetsCreatewith an existingfieldNameadds a widget to that field, and every widget with the same name shows and synchronizes the same value; without afieldName, a unique one is generated. - Boxes are required.
addToPageoptions are optional, whilecreateWidgetsneeds the widget box: user-space units with the origin at the bottom-left corner of the page. Check placement on a test document when you port coordinates. - Locking, not flattening.
enableReadOnly()becomessetWidgetProps(widgetIds, { isReadOnly: true }, { excludeFromHistory: true })fromuseSetWidgetProps, whereexcludeFromHistorykeeps an undo from unlocking the form. The change applies at once, and the returned promise resolves when the widgets show it.form.flatten(), which turns fields into page content, has no equivalent. - Removing fields.
form.removeField(field)becomesdeleteBlocks(widgetIds)fromuseBlockDeletewith the ids of every widget of that field. The stored field value stays. - No images in fields.
setImageon text fields and buttons has no equivalent.
Draw text and images
Before: pdf-lib
pdf-lib embeds a font or an image into the document, then draws it at page coordinates:
import { rgb, StandardFonts } from 'pdf-lib'
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
const pngImage = await pdfDoc.embedPng(pngImageBytes)
const page = pdfDoc.getPage(0)
page.drawText('Approved for release', {
x: 72,
y: 720,
size: 14,
font: helveticaFont,
color: rgb(0.8, 0.1, 0.1),
maxWidth: 300,
})
const { width, height } = pngImage.scale(0.5)
page.drawImage(pngImage, { x: 72, y: 600, width, height })The drawn text becomes part of the page. pdf-lib has no API to read or edit text on a page afterwards, its own included.
After: @sdk-pdf/react
Text from code becomes a paragraph: useParagraphCreate creates an empty one, useSetParagraphProps sets its wrap width and layout, and the editor mirror from useProgrammaticEditor types into it. An image staged with stageImage goes onto the page as a stamp annotation, from code with useStampAnnotationCreate or by the user with the addImage tool:
import {
CONTENT_POLICY_PRESET,
stageImage,
useParagraphCreate,
useProgrammaticEditor,
useSetInteractionMode,
useSetParagraphProps,
useStampAnnotationCreate,
} from '@sdk-pdf/react'
// Replaces drawText: the result is a paragraph users can edit later.
function useAddApprovalNote() {
const createParagraph = useParagraphCreate()
const setParagraphProps = useSetParagraphProps()
const { getParagraphEditor } = useProgrammaticEditor()
return async (pageId: string) => {
const paragraphId = await createParagraph(pageId, 72, 720)
// Like maxWidth: the text wraps at 300 units.
setParagraphProps([paragraphId], { width: 300 })
// undefined when the paragraph no longer exists.
const editor = getParagraphEditor(paragraphId)
editor?.selectAll()
editor?.insertText('Approved for release')
return paragraphId
}
}
// Replaces embedPng and drawImage at fixed coordinates.
function useAddLogo() {
const createStampAnnotation = useStampAnnotationCreate()
return async (pageId: string, logo: Blob) => {
const image = await stageImage(logo)
// x and y are the bottom-left corner; with only a width, the height follows the
// aspect ratio.
return createStampAnnotation({ pageId, image, x: 72, y: 600, width: 200 })
}
}
// Or let the user choose the spot: the next click on a page places the image.
function ImagePicker() {
const setInteractionMode = useSetInteractionMode()
async function placeImage(file: File) {
const image = await stageImage(file)
// Placing images is editing, so switch the policy with the tool.
setInteractionMode(
{ type: 'addImage', image, width: 200 },
CONTENT_POLICY_PRESET.PDF_EDITOR
)
}
return (
<input
type="file"
accept="image/*"
onChange={(event) => {
const file = event.currentTarget.files?.[0]
if (file) placeImage(file).catch(showError)
}}
/>
)
}What to know
- Text stays editable. A paragraph is text, not drawing: users edit it in
Viewportunder a policy with editable text, and code reads and rewrites any paragraph, those already in the document included, withgetParagraphsByPage(pageId)andgetParagraphEditor(paragraphId), as Programmatic editing shows. - Users can add text too. The
{ type: 'addText' }tool creates a paragraph where the user clicks and focuses it for typing; it requires editable text. - No font or color options.
insertText(text)takes the text only, and there are nofont,size, orcoloroptions. Users style text with a selection toolbar built onuseSelectionTextStyle. The paragraph-widelineHeight,textAlign,textIndent, and rotation throughmatrixare props ofsetParagraphProps. Embedding fonts and measuring text (embedFont,widthOfTextAtSize) have no equivalent. - Check placement.
createParagraph(pageId, x, y)takes the paragraph position in user-space units, andsetParagraphProps([paragraphId], { width })sets the width the text wraps at; the height follows the text. LikedrawText, a paragraph'sxandyare the start of its first baseline. Check the result on a test page when you portdrawTextcoordinates. - Images are stamp annotations.
drawImagepaints into the page content;@sdk-pdf/reactplaces the image as a stamp annotation that users can select, move, and resize.stageImage(blob)accepts any image the browser can decode, not only PNG and JPEG, and throwsInvalidInputErrorwhen the blob isn't a loadable image or has no intrinsic size; SVGs are rasterized when the PDF is saved. The size goes on the placement, in user-space units: a single dimension keeps the aspect ratio, and withoutwidthandheight, the natural size is used, fit down to the page. LikedrawImage,xandyare the bottom-left corner. - No vector drawing.
drawRectangle,drawLine,drawCircle,drawEllipse,drawSquare,drawSvgPath, andpushOperatorshave no equivalent. For freehand strokes, such as a handwritten mark collected in your UI,useInkAnnotationCreatecreates an ink annotation from the points. - One document. Programmatic edits go through the same document as user edits.
setEditorAwarenessPropertiesgives your code a presence cursor for real-time collaboration, which is coming soon.
Saving and downloading
Before: pdf-lib
save() serializes the document, with all changes, into a Uint8Array; saveAsBase64() returns a base64 string or a data URI. Saving also refreshes the appearance of form fields when the form was accessed through getForm():
const pdfBytes = await pdfDoc.save()
await fetch('/api/documents', { method: 'PUT', body: pdfBytes })For a download, apps wrap the bytes in a Blob and click a temporary link. pdf-lib keeps no change history, so apps that warn about unsaved changes track edits themselves.
After: @sdk-pdf/react
useDocumentSaving builds the final PDF from the loaded document and the recorded changes. useDocumentRevision tells whether anything changed since the last save: keep the revision you saved, and compare:
import { useRef, useState } from 'react'
import {
useDocumentRevision,
useDocumentRevisionGetter,
useDocumentSaving,
} from '@sdk-pdf/react'
function SaveControls() {
const revision = useDocumentRevision()
const getRevision = useDocumentRevisionGetter()
// One component owns the saved revision: the loaded document's revision until the first
// save.
const [savedRevision, setSavedRevision] = useState(revision)
// One save at a time, so an overlapping save can't mark a failed upload as saved.
const isSavingRef = useRef(false)
const { prepareDocument, downloadDocument } = useDocumentSaving()
// Every edit changes the revision; undoing back to the saved state clears the
// indicator.
const hasUnsavedChanges = revision !== savedRevision
async function save() {
if (isSavingRef.current) return
isSavingRef.current = true
try {
// Edits made after the save starts may be missing from the saved bytes, so only
// this revision becomes saved.
const startRevision = getRevision()
// Like pdfDoc.save(): the bytes go to fetch, so never hand out the loaded
// document's own buffer.
const pdfBytes = await prepareDocument({ returnOriginalIfUnchanged: false })
const response = await fetch('/api/documents', {
method: 'PUT',
body: pdfBytes as BodyInit,
})
// A failed upload keeps the unsaved-changes indicator.
if (!response.ok) return
setSavedRevision(startRevision)
} finally {
isSavingRef.current = false
}
}
return (
<div role="toolbar">
<button onClick={() => void save()}>Save</button>
{/* Replaces the Blob and the temporary link. */}
<button onClick={() => void downloadDocument()}>Download</button>
{hasUnsavedChanges && <span>Unsaved changes</span>}
</div>
)
}What to know
- returnOriginalIfUnchanged defaults to
true: with no changes,prepareDocument()returns the buffer passed toloadPdfwithout a copy. Set it tofalsewhen you transfer or mutate the bytes, or the loaded document's buffer is detached and later reads throw. - Two options. Besides
returnOriginalIfUnchanged,pageIdssaves only the listed pages, as in Merge and split documents.useObjectStreams,addDefaultPage,objectsPerTick,updateFieldAppearances, base64 output, and a download file name have no equivalent; encode the bytes yourself where you need base64. - Change tracking is new. The revision changes on every edit by the user or your code, and undo and redo bring back the revision of that point in the history, so equal revisions mean no unsaved changes. Render
SaveControlsonce the document is ready, so its first revision is the loaded document's. To read the revision in a handler without re-rendering, useuseDocumentRevisionGetter. - Both functions throw DocumentNotLoadedError when no document is loaded.
Errors and logging
Before: pdf-lib
pdf-lib reports problems by throwing. load() throws EncryptedPDFError for encrypted files, the PDFForm getters throw when a field is missing or has another type, and text a font can't encode, such as Ω in the standard Helvetica, throws an encoding error:
import { EncryptedPDFError, PDFDocument } from 'pdf-lib'
async function fillAndSave(existingPdfBytes, customerName) {
try {
const pdfDoc = await PDFDocument.load(existingPdfBytes)
pdfDoc.getForm().getTextField('customer.name').setText(customerName)
// An encoding error for field text surfaces on save.
return await pdfDoc.save()
} catch (error) {
if (error instanceof EncryptedPDFError)
showError('Encrypted documents are not supported')
else showError(error)
return null
}
}After: @sdk-pdf/react
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 useSdkLogReporting() {
useEffect(() => {
// The SDK never prints to the console: subscribe, and unsubscribe on unmount.
const unsubscribe = logMessages.on('message', (logMessage) => {
if (logMessage.level >= LogLevel.Warning)
reportToMonitoring(logMessage.message, logMessage.data)
})
return () => {
unsubscribe()
}
}, [])
}
function useOpenBytes() {
const [, loadPdf] = useLoadPdf()
return async (data: Uint8Array, password?: string) => {
try {
// Pass a copy and keep `data` for a password retry.
await loadPdf(data.slice(), { password })
} catch (error) {
if (error instanceof InvalidPasswordError) askForPassword()
// Narrow the other SDK error classes with instanceof in the same way.
else showError(error)
}
}
}What to know
- Error classes.
InvalidPasswordError(the password is missing or wrong),DocumentFormatError(the PDF is malformed beyond what the SDK can work around),NotSupportedError(the document uses a PDF feature the SDK doesn't support yet),DocumentNotLoadedError,InvalidInputError, andUninitializedError. All extendGenericErrorand carry a staticmessageand an optionaldataobject. - Nothing half-applied. Functions that change the document validate their input and throw before touching anything; functions that only read return empty values instead of throwing; hooks never throw during render.
- Logs are opt-in. Each message carries
level(LogLevel.Debug,Info,Warning,Error),reason, a staticmessage,data, andstack. There is no verbosity setting: filter bylevelin your subscriber.
Features without an equivalent
Plan around these before you migrate. Jobs that run outside the browser, such as generating, merging, or flattening documents on a server, can keep running where they are and hand their output to loadPdf:
| pdf-lib feature | In @sdk-pdf/react |
|---|---|
| Running in Node, Deno, or React Native | No equivalent; the SDK runs in React in the browser |
Creating a document from scratch (PDFDocument.create()) | Load an existing PDF, such as a blank template |
Copying pages from other documents and merging (copyPages) | No equivalent; usePageDuplicate copies pages within the loaded document |
Drawing pages of other documents (embedPdf, embedPage, drawPage) | No equivalent |
Page boxes and resizing (setMediaBox, setCropBox, setSize, scale, scaleContent) | No equivalent |
Vector drawing (drawRectangle, drawLine, drawCircle, drawEllipse, drawSvgPath, pushOperators) | No equivalent; SVG images placed as stamps are rasterized on save |
Embedding fonts and measuring text (embedFont, @pdf-lib/fontkit, widthOfTextAtSize) | No equivalent; fonts come from the fontFamily options of useSelectionTextStyle |
Text style options on drawn text (font, size, color) | Users style paragraphs through the selection |
Flattening forms (form.flatten()) | No equivalent; lock widgets with isReadOnly |
Images in fields (setImage) | No equivalent |
Document metadata (setTitle, getAuthor, and the other metadata methods) | No equivalent |
Viewer preferences, attachments (attach), document JavaScript (addJavaScript) | No equivalent |
Save options (useObjectStreams, addDefaultPage, objectsPerTick) and saveAsBase64() | No equivalent; encode the bytes yourself |