Migrating from iText
This guide moves the part of an iText Core application that users see and change into a React app built on @sdk-pdf/react. iText Core is a Java library that runs on your server and has no viewer: the app fills, stamps, and rearranges a PDF during a request and sends the result to the browser. iText for .NET has the same classes with C# method names, such as GetNumberOfPages(), so the mapping applies to it as well. With @sdk-pdf/react, the browser shows the document, and users fill forms, edit pages, and save; your Java backend keeps fetching and storing the bytes, and keeps the work only a server does, such as generating documents, signing, and producing PDF/A. Work through the checklist below; each step links to the section that covers it.
Beta:
@sdk-pdf/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 iText call that serves the browser.
- Decide what moves to the browser (showing, filling, page edits, and stamps users ask for) and what stays on the server (generation, signing, compliance).
- Install the package and set up
PdfProviders, with a content policy that fits your users. - Load and reset documents from your Java endpoint with
loadPdfandresetPdf, including the password flow. - Replace the served PDF and the browser's viewer with
Viewport. - Move page operations from numbers to page ids.
- Move form filling from
PdfAcroFormto widgets. - Map stamped text and images to paragraphs and image stamps.
- Keep merging on the server, and move page ranges and splitting.
- Move text extraction and search.
- Replace the close-and-return round trip with
prepareDocument. - Move error handling and logging and cleanup.
- Plan for features without an equivalent.
- Remove the iText endpoints that only served the browser (per-request filling, stamping, and page operations) and the modules only they used; keep iText where the server still generates, signs, or archives. Test encrypted, malformed, large, and form-heavy PDFs, page operations, and the save round trip, including reading the saved file back with iText.
Overview
How the two integrations differ
iText Core is a server-side library:
- It ships as Maven artifacts under
com.itextpdf. Theitext-corePOM brings in the kernel (PDF objects, pages,PdfCanvas, text extraction), layout (Document,Paragraph,Table,Image), forms, sign, pdfa, and pdfua modules, among others. Add-ons cover HTML conversion (pdfHTML), redaction (pdfSweep), and OCR (pdfOCR), which are open source under the AGPL like iText Core, and optimization (pdfOptimizer) and XFA flattening (pdfXFA), which need a commercial license. iText is licensed under the AGPL or commercially; a commercial license is a key file loaded at startup. - A
PdfDocumentopens in one of three modes: reading (aPdfReader), writing (aPdfWriterand a new, empty document), or stamping (both), which reads the original and writes a new file when the document closes. Each request opens, changes, and closes its own document. - iText Core doesn't render pages and has no UI. Users see the output in a viewer you don't control, such as the browser's built-in PDF viewer.
- Pages are 1-based numbers. Positions are user-space units (72 per inch), with the origin usually at the lower-left corner, as set by the page's media box.
- Failures are exceptions (
PdfException,BadPasswordException,IOException), and problems iText works around, such as a damaged cross-reference table, are logged through SLF4J.
@sdk-pdf/react is a browser library: the SDK owns the document, and you own the UI.
PdfProvidershosts the document and its configuration in your React tree, anduseLoadPdfloads the bytes your server returns.Viewportrenders the pages, and users work on them directly: selecting and editing text, filling forms, and placing text and images. The toolbars and panels around it are your React components, driven by hooks.- A loaded document lives for the session. Edits don't rewrite the file: each change is an operation in a CRDT, which powers undo and redo, and
useDocumentSavingbuilds the final PDF when you save. - Pages and blocks are addressed by session-scoped ids. Positions are user-space units; widget positions have their origin at the bottom-left corner of the page.
| Concern | iText Core | @sdk-pdf/react |
|---|---|---|
| Where it runs | Java (or .NET) on your server | The browser, inside your React app |
| What users see | The output file, in a viewer you don't control | Viewport, with your own toolbars and panels |
| Document lifetime | Opened and closed within a request | Loaded once per session; built into a file when you save |
| Changes | Written to a new file when the document closes | Recorded as undoable operations |
| Pages | 1-based numbers | Session-scoped page ids |
| Coordinates | User space, origin set by the media box (usually the bottom left) | User space; widgets measured from the bottom-left corner |
| Forms | Fields by full name, string values, flattening | Widgets with a fieldName, typed values, no flattening |
| Adding content | PdfCanvas operators and the layout engine | Paragraphs and image stamps by hook or tool, ink by hook |
| Diagnostics | Exceptions and SLF4J logs | Typed errors and logMessages |
| Licensing | AGPL, or a commercial license key loaded at startup | The package from a private registry |
What this means for your code:
- Split the work. Decide per feature: what users see and change moves to React; generation, signing, compliance, and storage stay on the server. Features without an equivalent lists what stays.
- Replace the round trip. Instead of a request per change that returns a new PDF (fill these fields, rotate this page), the browser loads the bytes once, users make their changes, and the app uploads the result of
prepareDocument(). - Keep the endpoints that move bytes. The endpoint that returned iText output returns the stored file, and another one accepts the saved file. Server-side steps after the upload, such as flattening or signing, keep using iText.
- Replace numbers with ids in the browser, and convert at the edges with
usePages. - Rethink forms as widgets. Users fill fields in the viewport, and each widget's value is typed: a string, a boolean, or option indices.
- Choose the content policy. The default
PDF_EDITORmakes everything editable; a form-filling app starts withPDF_VIEWER. - Build the viewer UI. iText has no viewer, so navigation, zoom, thumbnails, selection, and undo and redo have no iText counterpart to migrate: Building a PDF application covers them.
API map
Rows marked No equivalent are covered in Features without an equivalent.
| iText | @sdk-pdf/react | Notes |
|---|---|---|
com.itextpdf:itext-core, bouncy-castle-adapter | @sdk-pdf/react from the private registry, <PdfProviders> | The server keeps its Maven setup. |
LicenseKey.loadLicenseFile(...) | Nothing to call | PdfProviders takes no key. |
new PdfDocument(new PdfReader(...)) | loadPdf(Uint8Array) from useLoadPdf | fetch the bytes from your server first. |
new ReaderProperties().setPassword(bytes) | loadPdf(data, { password }) | |
BadPasswordException | InvalidPasswordError | A rejected loadPdf promise. |
pdfDoc.close() on a document opened for reading | resetPdf() from useResetPdf | |
pdfDoc.getNumberOfPages() | usePageCount | 0 while no document is loaded. |
pdfDoc.getPage(pageNumber) | usePages()[pageNumber - 1] | Entries carry id, width, height, and rotation. |
page.getPageSizeWithRotation() | width and height of the usePages entries | With the rotation applied. |
page.getRotation(), page.setRotation(degrees) | rotation, and setPageRotation(pageIdOrPageIds, rotation) from useSetPageRotation | Absolute angles in both. |
pdfDoc.addNewPage(index, pageSize) | createPage(width, height, insertAfterPageId?) from usePageCreate | Inserts after a page, not at a position. |
pdfDoc.removePage(pageNumber) | deletePages(pageIdOrPageIds) from usePageDelete | |
pdfDoc.movePage(pageNumber, insertBefore) | moveBefore or moveAfter from usePageMove | |
| — | duplicatePage(sourcePageId, insertAfterPageId?) from usePageDuplicate | |
page.setCropBox(), page.setMediaBox() | No equivalent | |
PdfFormCreator.getAcroForm(pdfDoc, true), getAllFormFields() | useWidgets, useWidgetsGetter | Widgets in reading order, not a map of fields. |
form.getField(name).setValue(value), getValueAsString() | useWidgetValue | One widget per hook call. |
TextFormFieldBuilder, CheckBoxFormFieldBuilder, and the other builders, addField | createWidgets(props) from useWidgetsCreate | |
field.setReadOnly(true) | setWidgetProps(ids, { isReadOnly: true }) from useSetWidgetProps | |
form.renameField(oldName, newName) | setWidgetProps(ids, { fieldName }) | |
form.removeField(name) | deleteBlocks(widgetIds) from useBlockDelete | Deleting a widget keeps its stored field value. |
form.flattenFields(), partialFormFlattening(name) | No equivalent | Flatten on the server after the upload. |
PdfCanvas beginText(), moveText(), showText() | createParagraph(pageId, x, y) from useParagraphCreate, getParagraphEditor | Or the addText tool. |
PdfCanvas.addImageFittedIntoRectangle(), addImageAt(), layout Image | useStampAnnotationCreate with stageImage | Places the image as a stamp annotation at given coordinates; the { type: 'addImage', image } tool places it where the user clicks. |
PdfCanvas paths, PdfExtGState | No equivalent | Freehand strokes can become an ink annotation with useInkAnnotationCreate. |
Creating text and link annotations (PdfTextAnnotation, PdfLinkAnnotation) | No equivalent | Stamps and ink are the annotations the SDK creates. Existing annotations are drawn and, under PDF_EDITOR, editable; useSelectionAnnotationProps changes the border color and width of selected shapes, lines, and ink. |
PdfMerger.merge(), copyPagesTo() into another document | No equivalent | Merge on the server and load the result. |
copyPagesTo() for one page range | prepareDocument({ pageIds }) from useDocumentSaving | Saves only the listed pages, in the listed order. See Merge and split pages. |
PdfSplitter (several files) | One prepareDocument({ pageIds }) call per part | Splitting by size has no equivalent. |
PdfTextExtractor.getTextFromPage(page) | getParagraphsByPage(pageId) from useProgrammaticEditor | Paragraph text and bounds. |
RegexBasedLocationExtractionStrategy | useDocumentSearch | Plain-text queries, no regular expressions; matches carry page and paragraph ids, not rectangles. |
new PdfWriter(outputStream) and close() | prepareDocument() from useDocumentSaving | Resolves with a Uint8Array. |
An application/pdf response with Content-Disposition: attachment | downloadDocument() | No file name option. |
StampingProperties.useAppendMode(), WriterProperties.setStandardEncryption() | No equivalent | |
PdfException, IOException | The SDK error classes | See Errors and logging. |
| SLF4J logging | logMessages | The SDK never prints to the console. |
close(), try-with-resources | Nothing to close | See Cleanup and lifecycle. |
PdfSigner, PdfADocument, PdfUADocument, pdfHTML, pdfSweep, pdfOCR | No equivalent | Keep them on your server. |
Install and set up
Before: iText
iText Core ships as Maven artifacts under com.itextpdf: the itext-core POM brings in its modules, and the bouncy-castle-adapter artifact goes next to it:
<properties>
<itext.version>9.8.0</itext.version>
</properties>
<dependencies>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-core</artifactId>
<version>${itext.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>bouncy-castle-adapter</artifactId>
<version>${itext.version}</version>
</dependency>
</dependencies>Under the AGPL, that is the whole setup. A commercial license adds the com.itextpdf.licensing:licensing-base artifact from iText's own Maven repository and loads the key file before any other iText call:
// Before the first iText call.
LicenseKey.loadLicenseFile(new File("path/to/itextkey.json"));After: @sdk-pdf/react
The browser part installs from a private registry. Add the @sdk-pdf scope to an .npmrc next to your package.json, keep the token in an environment variable, and install; Getting started 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.
Wrap the page that shows documents in PdfProviders. The PDF application re-renders whenever a prop changes identity, so memoize the configuration:
import { useMemo } from 'react'
import { CONTENT_POLICY_PRESET, PdfProviders } from '@sdk-pdf/react'
import '@sdk-pdf/react/style.css'
export function ContractPage({
contractId,
canEdit,
}: {
contractId: string
canEdit: boolean
}) {
const config = useMemo(
() => ({
// PDF_VIEWER: fillable widgets and clickable links, read-only text and annotations.
// Applied when the viewport first mounts; switch later with useSetInteractionMode.
initialContentPolicy: canEdit
? CONTENT_POLICY_PRESET.PDF_EDITOR
: CONTENT_POLICY_PRESET.PDF_VIEWER,
}),
[canEdit]
)
return (
<PdfProviders {...config}>
<DocumentLoader url={`/api/contracts/${contractId}/file`} />
</PdfProviders>
)
}What to know
- The server keeps iText. The Maven setup stays for the work in Features without an equivalent; the React app needs no Java and no request to the server to change the document.
- No license key in the browser.
LicenseKey.loadLicenseFilehas no counterpart: the registry token in.npmrcinstalls the package, andPdfProviderstakes no key. - Pick the content policy deliberately.
PdfProvidersdefaults toCONTENT_POLICY_PRESET.PDF_EDITOR, where text, annotations, links, and widgets are all editable. An app that replaces server-side form filling starts withPDF_VIEWER, which keeps widgets fillable and links clickable, with read-only text and annotations. Each level can also be a function of the block id, such aswidgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive'), and'hidden'doesn't render that kind of content at all;useSetInteractionModelists the levels.
Load and reset a document
Before: iText
PdfReader reads a file name, a File, or an InputStream, and PdfDocument opens it. ReaderProperties carries the password of an encrypted file, which opens with the user or the owner password; stamping it needs the owner password, and with only the user password the PdfDocument constructor throws BadPasswordException:
byte[] stored = storage.read(documentId);
ReaderProperties properties = new ReaderProperties();
if (password != null) {
properties.setPassword(password.getBytes(StandardCharsets.UTF_8));
}
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(stored), properties))) {
int pageCount = pdfDoc.getNumberOfPages();
} catch (BadPasswordException e) {
// The password is missing or wrong: ask for it, then open the file again.
} catch (IOException e) {
// Reading the bytes failed.
}Each request opens its own document and closes it when the work is done; no document stays open between requests.
After: @sdk-pdf/react
loadPdf from useLoadPdf takes the file bytes, so the browser fetches them from your Java endpoint first. The document then stays loaded until resetPdf() from useResetPdf unloads it, which the loader does before opening a document from another URL:
import { useRef, useState } from 'react'
import { InvalidPasswordError, useLoadPdf, useResetPdf } from '@sdk-pdf/react'
function DocumentLoader({ url }: { url: string }) {
const [loadState, loadPdf] = useLoadPdf()
const resetPdf = useResetPdf()
// The bytes and the URL they came from, kept for a password retry.
const bytesRef = useRef<{ url: string; data: Uint8Array } | null>(null)
const [loadedUrl, setLoadedUrl] = useState<string>()
const [needsPassword, setNeedsPassword] = useState(false)
const [errorMessage, setErrorMessage] = useState<string>()
async function open(password?: string) {
setErrorMessage(undefined)
try {
let data = bytesRef.current?.url === url ? bytesRef.current.data : undefined
if (!data) {
// The endpoint that used to stream iText output now returns the stored file.
const response = await fetch(url, { credentials: 'include' })
if (!response.ok) {
setErrorMessage(`Loading the document failed with ${response.status}`)
return
}
data = new Uint8Array(await response.arrayBuffer())
bytesRef.current = { url, data }
}
// A document from another URL may still be loaded: unload it, which invalidates its
// ids.
if (loadState.status !== 'initial') resetPdf()
setLoadedUrl(undefined)
// loadPdf reads from the buffer it receives for the document's lifetime,
// so keep the original for a retry and pass a copy.
await loadPdf(data.slice(), { password })
setLoadedUrl(url)
setNeedsPassword(false)
} catch (error) {
// Replaces catching BadPasswordException.
if (error instanceof InvalidPasswordError) setNeedsPassword(true)
else setErrorMessage('The document could not be opened')
}
}
if (needsPassword)
return <PasswordPrompt onSubmit={(password) => void open(password)} />
if (loadState.status === 'loading') return <p>Loading the document…</p>
// Render the application only for the document loaded from the current URL.
if (loadState.status === 'ready' && loadedUrl === url) return <PdfApplication />
return (
<>
{errorMessage && <p role="alert">{errorMessage}</p>}
<button onClick={() => void open()}>Open</button>
</>
)
}To close the document without opening another one, call resetPdf() directly:
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
- Bytes, not files or streams.
loadPdftakes aUint8Array. Authentication, headers, and error responses belong to your ownfetch. - Keep a copy for retries. The document reads from the given buffer for its whole lifetime, and another
loadPdfcall can detach it. To reuse the bytes for a password retry, pass a copy:loadPdf(data.slice(), { password }). - Passwords are a rejection. A missing or wrong password rejects with
InvalidPasswordError, the counterpart ofBadPasswordException. Without a password, the empty password is tried, as the PDF specification prescribes. The password now goes from the user toloadPdfin the browser: the server sends the encrypted file as stored. - Another document. When the URL changes, the loader shows its Open button again and renders the application only after the new document has loaded;
open()unloads the previous one withresetPdf()first. - A session, not a request. The document stays loaded while users work on it. After
resetPdf(), page and block ids, widget snapshots, and document revisions from the old document are no longer valid.
Display the document
Before: iText
iText Core doesn't render pages. An iText backend writes the PDF into the HTTP response, buffered first so the response carries the content length, and the browser shows it in its built-in viewer:
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
byte[] stored = storage.read(request.getParameter("id"));
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output))) {
// Per-request changes go here: filled fields, stamps, page operations.
}
response.setContentType("application/pdf");
response.setContentLength(output.size());
output.writeTo(response.getOutputStream());
}<iframe src="/documents?id=42" title="Contract"></iframe>Every change users ask for, such as a filled field or a rotated page, is another request that runs iText and reloads the frame.
After: @sdk-pdf/react
Viewport renders the pages and takes the input; your components render everything around it:
import { useViewportAutoFitScale, Viewport } from '@sdk-pdf/react'
function PdfApplication() {
// Fit the page width once per loaded document.
useViewportAutoFitScale('width')
return (
<div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: '100vh' }}>
{/* Your toolbar: the page, form, and save controls from this guide. */}
<Toolbar />
{/* The viewport fills its parent, so the parent needs an explicit size. */}
<div style={{ minHeight: 0 }}>
<Viewport />
</div>
</div>
)
}What to know
- Changes happen in place. Users fill fields, select text, follow links, and, under
PDF_EDITOR, edit text and move blocks insideViewport, with no request to the server. The content policy decides what is allowed. - Render only when ready.
ViewportthrowsDocumentNotLoadedErrorwhen no document is loaded, so render it when the load state is'ready'. - Previews without server rendering.
PageCanvasrenders one page onto a canvas anywhere outside the main view, lazily and at the canvas's CSS size, for thumbnails and previews. It draws the page as loaded from the file; edits made during the session show in the main view. Previews that show those edits are coming soon. - Styling is yours. The SDK renders unstyled:
theme.classNames,theme.styles, and CSS variables such as--selection-colorstyle its elements, as Theming and customization describes.
Page manipulation
Before: iText
PdfDocument changes pages by 1-based number, and each PdfPage holds its size and rotation:
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output))) {
// getPageSize() ignores the rotation; getPageSizeWithRotation() applies it.
Rectangle firstPageSize = pdfDoc.getPage(1).getPageSizeWithRotation();
// Insert a blank A4 page at position 2: the pages after it shift by one.
pdfDoc.addNewPage(2, PageSize.A4);
// Move the last page to the front.
pdfDoc.movePage(pdfDoc.getNumberOfPages(), 1);
// The rotation is absolute: read the current angle to turn the page a quarter
// clockwise.
PdfPage firstPage = pdfDoc.getPage(1);
firstPage.setRotation((firstPage.getRotation() + 90) % 360);
// Remove page 3.
pdfDoc.removePage(3);
}After: @sdk-pdf/react
Each page operation is a hook that returns a command taking page ids: usePageCreate, usePageDelete, usePageMove, useSetPageRotation, and usePageDuplicate. usePages gives the order and the sizes:
import {
usePageCreate,
usePageDelete,
usePageMove,
usePages,
useSetPageRotation,
} from '@sdk-pdf/react'
function PageActions({ pageId }: { pageId: string }) {
const pages = usePages()
const createPage = usePageCreate()
const deletePages = usePageDelete()
const { moveBefore } = usePageMove()
const setPageRotation = useSetPageRotation()
const index = pages.findIndex((page) => page.id === pageId)
if (index === -1) return null
const page = pages[index]
return (
<div role="toolbar" aria-label={`Page ${index + 1} of ${pages.length}`}>
{/* User-space units with the rotation applied, like getPageSizeWithRotation(). */}
<span>
{Math.round(page.width)} × {Math.round(page.height)}
</span>
{/* A4 is roughly 595 × 842; the new page goes after this one. */}
<button onClick={() => createPage(595, 842, pageId)}>
Insert blank page after
</button>
{/* Without a target page, moveBefore inserts at the start of the document. */}
<button onClick={() => moveBefore(pageId)}>Move to start</button>
{/* Absolute, as with setRotation: add to the current angle yourself. */}
<button onClick={() => setPageRotation(pageId, page.rotation + 90)}>
Rotate clockwise
</button>
<button disabled={pages.length <= 1} onClick={() => deletePages(pageId)}>
Delete
</button>
</div>
)
}What to know
| Operation | iText | @sdk-pdf/react |
|---|---|---|
| Count | getNumberOfPages() | usePageCount, which re-renders when the count changes |
| Size | getPageSize() without the rotation, getPageSizeWithRotation() with it | width and height of each usePages() entry, with the rotation applied |
| Insert | addNewPage(index, pageSize) at a 1-based position | createPage(width, height, insertAfterPageId?) after the given page, or at the end; no default size |
| Move | movePage(pageNumber, insertBefore) | moveBefore(ids, beforePageId?) (the start when omitted) or moveAfter(ids, afterPageId?) (the end when omitted) |
| Rotate | setRotation(degrees), absolute; a multiple of 90, not checked | setPageRotation(ids, rotation), absolute; -90 and 450 are normalized, and angles off a quarter turn throw InvalidInputError |
| Remove | removePage(pageNumber) | deletePages(ids); an undo can restore the page |
| Duplicate | — | duplicatePage(sourcePageId, insertAfterPageId?), asynchronous, after the source by default |
- Edits, not a new file. Each call changes the loaded document at once and shows in the viewport; deleting, moving, or rotating several pages in one call is a single undo step. The PDF file is built when you save.
- Ids instead of numbers. Page numbers from your server, such as a stored page reference, become ids at the edge with
usePages()[n - 1].id, andgetPageIndex(pageId)fromusePageIndexGettergoes the other way with a zero-based index (add 1 for the page number;undefinedwhen the page is gone). Ids are session-scoped: persist numbers, not ids. - No cropping or resizing.
setCropBox()andsetMediaBox()have no equivalent.
Forms
Before: iText
PdfFormCreator.getAcroForm(pdfDoc, true) returns the document's PdfAcroForm, creating one when the file has none. Fields are looked up by full name, values are strings, and a checkbox takes the name of an appearance state, such as Yes or Off. New fields come from builders, with a Rectangle given by its lower-left corner, width, and height:
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output))) {
PdfAcroForm form = PdfFormCreator.getAcroForm(pdfDoc, true);
form.getField("customer.name").setValue("Jane Doe");
form.getField("terms").setValue("Yes");
PdfTextFormField notes = new TextFormFieldBuilder(pdfDoc, "notes")
.setWidgetRectangle(new Rectangle(72, 600, 300, 24))
.createText();
notes.setMultiline(true);
form.addField(notes, pdfDoc.getPage(1));
// Lock a field, or turn every field into page content with form.flattenFields().
form.getField("customer.name").setReadOnly(true);
}Reading a submitted form is getAllFormFields() and getValueAsString() on the uploaded file.
After: @sdk-pdf/react
When the content policy allows interacting with widgets, as PDF_VIEWER does, users fill the fields in Viewport, with nothing to build. The hooks are for the form UI around it, as Working with forms explains: useWidgets lists the widgets, useWidgetValue binds one widget's value, useWidgetsCreate adds fields, and useSetWidgetProps locks them:
import {
CheckboxWidget,
useDocumentSaving,
useSetWidgetProps,
useWidgets,
useWidgetsCreate,
useWidgetsGetter,
useWidgetValue,
Widget,
} from '@sdk-pdf/react'
function isCheckboxWidget(widget: Widget): widget is CheckboxWidget {
return widget.widgetType === 'checkbox'
}
// The counterpart of getField("terms"): the value is a boolean, not an appearance state
// name.
function TermsCheckbox() {
const { widgets } = useWidgets()
const widget = widgets
.filter(isCheckboxWidget)
.find((widget) => widget.fieldName === 'terms')
return widget ? <CheckboxField widget={widget} /> : null
}
function CheckboxField({ widget }: { widget: CheckboxWidget }) {
const [isChecked, setIsChecked] = useWidgetValue(widget)
return (
<label>
<input
type="checkbox"
checked={isChecked}
onChange={(event) => setIsChecked(event.currentTarget.checked)}
/>
I accept the terms
</label>
)
}
// The counterpart of TextFormFieldBuilder and addField(field, page).
function useAddNotesField() {
const createWidgets = useWidgetsCreate()
return async (pageId: string) => {
const [widget] = await createWidgets({
pageId,
widgetType: 'text',
// User-space units: the origin is at the bottom-left corner of the page.
x: 72,
y: 600,
width: 300,
height: 24,
fieldName: 'notes',
isMultiline: true,
})
return widget
}
}
// Upload the filled form, then lock it: the counterpart of setReadOnly(true) on every
// field.
function useSubmitForm() {
const getWidgets = useWidgetsGetter()
const setWidgetProps = useSetWidgetProps()
const { prepareDocument } = useDocumentSaving()
return async (url: string) => {
const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })
const response = await fetch(url, { method: 'POST', body: bytes as BodyInit })
// A failed upload leaves the form editable, so the user can try again.
if (!response.ok)
throw new Error(`Submitting the form failed with ${response.status}`)
const widgets = await getWidgets()
// excludeFromHistory keeps undo from unlocking the submitted form.
await setWidgetProps(
widgets.map((widget) => widget.id),
{ isReadOnly: true },
{ excludeFromHistory: true }
)
}
}From fields to widgets
| Concept | iText | @sdk-pdf/react |
|---|---|---|
| Lookup | getField(name), getAllFormFields() | useWidgets() (re-renders), or await getWidgets() from useWidgetsGetter (one-time, loads every page with widgets first); filter by fieldName |
| Shared values | A field with several widgets | Every widget with the same field name shows and synchronizes the same value |
| Values | setValue(String), getValueAsString() | useWidgetValue(widget): string for text and choice, boolean for checkbox and radio, option indices for multiChoice |
| Checkbox states | Appearance state names, such as Yes and Off | true or false; the stored value when checked is the widget's checkedValue |
| Creation | TextFormFieldBuilder, CheckBoxFormFieldBuilder, ChoiceFormFieldBuilder, RadioFormFieldBuilder, PushButtonFormFieldBuilder, SignatureFormFieldBuilder | createWidgets(props) with widgetType text, checkbox, choice or multiChoice, radio, pushButton, or signature; a unique fieldName is generated when omitted |
| Interactive creation | — | The { type: 'addWidget', widgetProps } interaction tool, with editable widgets |
| Read-only | setReadOnly(true) | setWidgetProps(ids, { isReadOnly: true }, options?) |
| Rename and remove | renameField(oldName, newName), removeField(name) | setWidgetProps(ids, { fieldName }), and deleteBlocks(widgetIds) from useBlockDelete, which keeps the stored field value |
| Flattening | flattenFields(), partialFormFlattening(name) | No equivalent |
What to know
- Coordinates. A widget's
x,y,width, andheightare user-space units measured from the bottom-left corner of the page. iText'sRectanglematches them when the page's media box starts at 0, 0. Check the placement on a test document before converting stored positions. - Prefilling can stay on the server. Filling fields with
setValue()before sending the file keeps working: users see the values in the viewport and can change them. In the browser, values are set per widget throughuseWidgetValue; there is no setter by field name. - Read submitted values on the server. The SDK reads values per widget, inside components. To process a submitted form, upload the file and read it with
getAllFormFields()andgetValueAsString(), as before. - No flattening.
flattenFields()has no equivalent: flatten on the server after the upload when the stored file must not stay fillable. - Read-only fields.
isReadOnlyrestricts filling the field, not authoring the widget; with{ excludeFromHistory: true }, an undo can't unlock a submitted form. - Date fields. A text widget's
formatexposes Acrobat date formats asFormatType.DateTime, recognized without running scripts; seeFormatType. - Signatures.
signaturewidgets carry a read-onlyisCryptoSigned, and the SDK never signs: keepPdfSigneron your server.
Draw text and images
Before: iText
iText adds content to a page in two ways: PdfCanvas writes low-level operators (text objects, images, and paths), and the layout engine positions elements such as Paragraph for you. Closing the layout Document also closes the PdfDocument:
PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output));
try (Document document = new Document(pdfDoc)) {
PdfPage page = pdfDoc.getPage(1);
Rectangle pageSize = page.getPageSize();
// A text object at an absolute position.
PdfCanvas canvas = new PdfCanvas(page);
canvas.beginText()
.setFontAndSize(PdfFontFactory.createFont(StandardFonts.HELVETICA), 12)
.moveText(72, pageSize.getTop() - 72)
.showText("Reviewed by the legal team")
.endText();
// An image fitted into a rectangle given by its lower-left corner, width, and height.
ImageData logo = ImageDataFactory.create(logoBytes);
canvas.addImageFittedIntoRectangle(logo, new Rectangle(72, 72, 150, 50), false);
// A layout paragraph, centered on page 1.
document.showTextAligned(new Paragraph("DRAFT").setFontSize(48),
pageSize.getX() + pageSize.getWidth() / 2,
pageSize.getY() + pageSize.getHeight() / 2, 1,
TextAlignment.CENTER, VerticalAlignment.MIDDLE, 0);
}After: @sdk-pdf/react
Text becomes a paragraph, created from code with useParagraphCreate and filled through useProgrammaticEditor, or placed by users with the addText tool. An image is staged with stageImage and becomes a stamp annotation, placed from code with useStampAnnotationCreate or by users with the addImage tool. The tools are editing, so the policy switches with the tool through useSetInteractionMode, here to PDF_EDITOR:
import {
CONTENT_POLICY_PRESET,
InteractionTool,
stageImage,
useInteractionTool,
useParagraphCreate,
useProgrammaticEditor,
useSetInteractionMode,
useStampAnnotationCreate,
} from '@sdk-pdf/react'
// The counterpart of beginText(), moveText(x, y), and showText(text).
function useStampText() {
const createParagraph = useParagraphCreate()
const { getParagraphEditor } = useProgrammaticEditor()
return async (pageId: string, x: number, y: number, text: string) => {
const paragraphId = await createParagraph(pageId, x, y)
const editor = getParagraphEditor(paragraphId)
// The paragraph may be gone already: deleted by an undo.
if (!editor) return
editor.selectAll()
editor.deleteAll()
editor.insertText(text)
}
}
// The counterpart of addImageFittedIntoRectangle(logo, new Rectangle(72, 72, 150, 50),
// false).
function useStampLogo() {
const createStamp = useStampAnnotationCreate()
return async (pageId: string, logo: Blob) => {
const image = await stageImage(logo)
// x and y are the bottom-left corner in user space; with only a width, the height
// follows the aspect ratio.
return createStamp({ pageId, image, x: 72, y: 72, width: 150 })
}
}
const addTextTool: InteractionTool = { type: 'addText' }
function StampToolbar({ logo }: { logo: Blob }) {
const interactionTool = useInteractionTool()
const setInteractionMode = useSetInteractionMode()
async function placeLogo() {
const image = await stageImage(logo)
// The next click on a page places the image, 150 user-space units wide.
setInteractionMode(
{ type: 'addImage', image, width: 150 },
CONTENT_POLICY_PRESET.PDF_EDITOR
)
}
return (
<div role="toolbar">
{/* The next click creates a paragraph and focuses it. */}
<button
aria-pressed={interactionTool.type === 'addText'}
onClick={() => setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
>
Add text
</button>
<button
aria-pressed={interactionTool.type === 'addImage'}
onClick={() => placeLogo().catch(showError)}
>
Place logo
</button>
</div>
)
}What to know
- Text is a paragraph, not operators. Text from
createParagraphor theaddTexttool is a block that users can select, edit, and move. Programmatic edits flow through the same document as user edits, as Programmatic editing shows. - Styling goes through the selection.
createParagraph(pageId, x, y)takes no font or size, unlikesetFontAndSize(). Users style text with a toolbar built onuseSelectionTextStyle: font family, size, color, bold, italic, alignment, and spacing. - Editing existing text is new. Under
PDF_EDITOR, users edit page text in place, andgetParagraphEditor(paragraphId)rewrites a paragraph from code. - Images are stamp annotations.
stageImage(blob)validates and measures the image. The size goes on the placement,widthandheightin user-space units, for bothcreateStampand theaddImagetool: a single dimension keeps the aspect ratio, and with neither the image keeps its natural size, fit to the page. SVGs are accepted and rasterized when the PDF is saved. - Freehand ink.
useInkAnnotationCreatecreates an ink annotation from strokes your code collects; there is no drawing tool. - Other drawing and every-page stamps stay on the server. Paths and shapes,
PdfExtGStateopacity, text and link annotations, and watermarks have no equivalent: add them with iText after the upload. A plain-text header, footer, or page number is acreateParagraphcall per page.
Merge and split pages
Before: iText
PdfMerger appends page ranges from source documents to a target, and copyPagesTo copies an inclusive, 1-based range into another document, which is how a range becomes its own file. Source documents stay open unless setCloseSourceDocuments(true):
ByteArrayOutputStream merged = new ByteArrayOutputStream();
try (PdfDocument result = new PdfDocument(new PdfWriter(merged));
PdfDocument cover = new PdfDocument(
new PdfReader(new ByteArrayInputStream(coverBytes)));
PdfDocument contract = new PdfDocument(
new PdfReader(new ByteArrayInputStream(contractBytes)))) {
PdfMerger merger = new PdfMerger(result);
merger.merge(cover, 1, 1);
merger.merge(contract, 1, contract.getNumberOfPages());
}
ByteArrayOutputStream excerpt = new ByteArrayOutputStream();
try (PdfDocument source = new PdfDocument(
new PdfReader(new ByteArrayInputStream(contractBytes)));
PdfDocument part = new PdfDocument(new PdfWriter(excerpt))) {
// Pages 2 to 4, inclusive.
source.copyPagesTo(2, 4, part);
}PdfSplitter cuts a document into several by page numbers, by page count, or by size.
After: @sdk-pdf/react
@sdk-pdf/react works on one loaded document, so merging stays on the server. A page range becomes its own file in the browser: prepareDocument({ pageIds }) from useDocumentSaving saves only the listed pages, and leaves the loaded document as it is:
import { useDocumentSaving, usePages } from '@sdk-pdf/react'
// The browser-side counterpart of copyPagesTo(from, to, part): 1-based and inclusive.
function useSavePageRange() {
const pages = usePages()
const { prepareDocument } = useDocumentSaving()
return async (from: number, to: number) => {
if (from < 1 || to > pages.length || from > to)
throw new RangeError('The page range is outside the document')
const pageIds = pages.slice(from - 1, to).map((page) => page.id)
return prepareDocument({ pageIds })
}
}What to know
- No merging.
PdfMergerandcopyPagesTointo another document have no equivalent, and neither has inserting pages from another file. Merge with iText on the server, then load the result withloadPdf. - Splitting is one save per part. For
PdfSplitter's several output documents, callprepareDocument({ pageIds })once per part. The saved file has the pages in the order ofpageIds, each page at most once. Splitting by file size has no equivalent. - Within the document.
duplicatePage(sourcePageId, insertAfterPageId?)fromusePageDuplicatecopies a page with all its content, andusePageMovereorders pages, as in Page manipulation.
Text extraction and search
Before: iText
PdfTextExtractor reads a page's text through an extraction strategy, a new one per page; LocationTextExtractionStrategy is the default:
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(stored)))) {
StringBuilder text = new StringBuilder();
for (int i = 1; i <= pdfDoc.getNumberOfPages(); i++) {
text.append(PdfTextExtractor.getTextFromPage(
pdfDoc.getPage(i), new LocationTextExtractionStrategy()));
text.append('\n');
}
}RegexBasedLocationExtractionStrategy finds text by pattern, with its locations.
After: @sdk-pdf/react
useProgrammaticEditor reads the text as paragraphs: getParagraphsByPage(pageId) for one page, or getAllParagraphIds() and getPlainTextParagraphs(ids) for the whole document:
import { useProgrammaticEditor } from '@sdk-pdf/react'
function useReadDocumentText() {
const { getAllParagraphIds, getPlainTextParagraphs } = useProgrammaticEditor()
return async () => {
// Loads every page in turn, which takes a while on a large document.
const paragraphIds = await getAllParagraphIds()
const paragraphs = await getPlainTextParagraphs(paragraphIds)
return paragraphs
.map((paragraph) => `[page ${paragraph.pageNumber}] ${paragraph.content}`)
.join('\n\n')
}
}What to know
- Paragraphs, not strategies. Each entry carries
id,content, the 1-basedpageNumber, the user-space boxbounds({ x, y, width, height }), and the paragraph layout, such astextAlignandlineHeight. There are no extraction strategies or text chunks to choose. - One page when you can.
getAllParagraphIds()loads every page sequentially; when the page is known,getParagraphsByPage(pageId)is faster. - Search for users.
useDocumentSearchfinds a plain-text query, withisCaseSensitiveandisWholeWordoptions, highlights the matches on the pages, and scrolls to the active one. It takes no regular expressions, and a match carries itspageId,paragraphId, andtextrather than the rectangles ofRegexBasedLocationExtractionStrategy. For a pattern, matchcontentyourself and scroll to the paragraph withscrollToBlock(paragraphId)fromuseScrollToBlock. - Indexing stays on the server. Extraction for search indexes, and other jobs without a user, keeps running with iText on the stored files.
Saving and downloading
Before: iText
Closing a stamped PdfDocument writes the new file into the PdfWriter's stream. With StampingProperties.useAppendMode(), the changes follow the original bytes as an incremental update. A download is the same bytes with an attachment header:
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(stored)), new PdfWriter(output),
new StampingProperties().useAppendMode())) {
PdfFormCreator.getAcroForm(pdfDoc, true).getField("status").setValue("Approved");
}
storage.save(documentId, output.toByteArray());
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "attachment; filename=\"contract.pdf\"");
output.writeTo(response.getOutputStream());After: @sdk-pdf/react
useDocumentSaving builds the PDF from the loaded document and the recorded changes. useDocumentRevision changes with every edit, and undo and redo bring back the revision of that point in the history: keep the revision the server has, and a different current revision means unsaved changes. The save reads the revision it starts from with useDocumentRevisionGetter and records it after the server accepts the file:
import { useRef, useState } from 'react'
import {
useDocumentRevision,
useDocumentRevisionGetter,
useDocumentSaving,
} from '@sdk-pdf/react'
function SaveControls({ url }: { url: string }) {
const revision = useDocumentRevision()
const getRevision = useDocumentRevisionGetter()
// The revision on the server: the loaded document's revision until the first save.
const [savedRevision, setSavedRevision] = useState(revision)
// One save at a time, so an overlapping save can't mark a failed upload as saved.
const isSavingRef = useRef(false)
const { prepareDocument, downloadDocument } = useDocumentSaving()
// Undoing back to the saved state clears it too.
const hasUnsavedChanges = revision !== savedRevision
async function save() {
if (isSavingRef.current) return
isSavingRef.current = true
try {
// Edits made after the save starts may be missing from the saved bytes, so only
// this revision becomes saved.
const startRevision = getRevision()
// The buffer goes to fetch, so don't hand out the loaded document's original
// buffer.
const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })
const response = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/pdf' },
body: bytes as BodyInit,
})
// Keep the unsaved state when the server rejects the file.
if (!response.ok) throw new Error(`Saving failed with ${response.status}`)
setSavedRevision(startRevision)
} finally {
isSavingRef.current = false
}
}
return (
<div role="toolbar">
<button disabled={!hasUnsavedChanges} onClick={() => save().catch(showError)}>
Save
</button>
<button onClick={() => void downloadDocument()}>Download</button>
{hasUnsavedChanges && <span>Unsaved changes</span>}
</div>
)
}One component owns the saved revision, so the save and the indicator compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's.
What to know
- The server receives a complete PDF.
prepareDocument()resolves with the file's bytes as aUint8Array. The Java endpoint stores them, or opens them withPdfReaderfor server-side steps such as flattening or signing. - returnOriginalIfUnchanged defaults to
true, returning the buffer passed toloadPdfwhen nothing changed; set it tofalsewhen the bytes will be transferred or mutated, or the loaded document's buffer is detached and later reads throw. - Save options. Besides
returnOriginalIfUnchanged,pageIdssaves only the listed pages, as in Merge and split pages. There is no append mode, encryption, compression setting, or download file name. Encrypt or compress on the server when the stored file needs it. - Downloads stay in the browser.
downloadDocument()builds the same bytes and saves them on the user's device, with no request to the server.
Errors and logging
Before: iText
iText throws BadPasswordException for a missing or wrong password, and other problems as PdfException; both are unchecked, while the PdfReader constructors throw the checked IOException. Problems it works around, such as a damaged cross-reference table it rebuilds, go to SLF4J, so the app supplies the binding, such as Logback:
try (PdfDocument pdfDoc = new PdfDocument(
new PdfReader(new ByteArrayInputStream(stored)))) {
if (pdfDoc.getReader().hasRebuiltXref()) {
metrics.increment("pdf.repaired");
}
} catch (BadPasswordException e) {
// A missing or wrong password.
} catch (PdfException e) {
// A file iText can't process.
} catch (IOException e) {
// Reading the bytes failed.
}<!-- logback.xml: iText's loggers are named after its classes, under com.itextpdf. -->
<logger name="com.itextpdf" level="WARN" />After: @sdk-pdf/react
Failures are typed errors from the call that failed, and problems the SDK works around are log messages you subscribe to with logMessages. Error handling and logging has the full rules. Forwarding the browser's messages to your server keeps them next to the iText logs:
import { useEffect } from 'react'
import { logMessages, LogLevel } from '@sdk-pdf/react'
function useForwardSdkLogs(url: string) {
useEffect(() => {
// The SDK never prints to the console: subscribe, and unsubscribe on unmount.
const unsubscribe = logMessages.on('message', (logMessage) => {
if (logMessage.level < LogLevel.Warning) return
// The message is static, so it groups cleanly in the server's logs.
navigator.sendBeacon(
url,
JSON.stringify({
level: logMessage.level,
reason: logMessage.reason,
message: logMessage.message,
})
)
})
return () => {
unsubscribe()
}
}, [url])
}Catch load failures where you call loadPdf, as DocumentLoader does in Load and reset a document.
What to know
| iText | @sdk-pdf/react |
|---|---|
BadPasswordException | InvalidPasswordError: the document is encrypted, and the password is missing or wrong |
PdfException for a file that can't be processed | DocumentFormatError: the PDF is malformed beyond what the SDK can work around; NotSupportedError: it uses a PDF feature the SDK doesn't support yet |
IOException from PdfReader | Errors from your own fetch |
Invalid arguments, such as IndexOutOfBoundsException for a page number out of range | InvalidInputError |
| — | DocumentNotLoadedError: an operation needs a document, and none is loaded |
| A cross-reference table iText rebuilds, logged at error level | A log message whose reason is a document format issue |
- Nothing half-applied. Commands validate first and throw before touching anything; functions that only read return empty values instead of throwing; hooks never throw during render.
- Every error class extends
GenericErrorand carries a staticmessageand an optionaldataobject;UninitializedErrorcovers objects used before their asynchronous initialization completes. - Logs are opt-in. Each message carries
level(LogLevel.Debug,Info,Warning, orError),reason, a staticmessage,data, andstack. There are no loggers to configure: filter bylevelin your subscriber, where Logback filtered by logger.
Cleanup and lifecycle
Before: iText
PdfDocument is AutoCloseable. close() writes the output and closes the reader and the writer, unless setCloseReader(false) or setCloseWriter(false) keeps them open; after it, calls on the document throw PdfException. PdfMerger.close() closes the target document:
PdfDocument pdfDoc = new PdfDocument(new PdfReader(input), new PdfWriter(output));
// Keep the caller's output stream open after close().
pdfDoc.setCloseWriter(false);
try {
// Work with the document.
} finally {
// Writes the file; later calls on pdfDoc throw PdfException.
pdfDoc.close();
}After: @sdk-pdf/react
- Nothing to close. The document lives in
PdfProvidersfor the session; there is noclose()and no stream to manage. - Subscriptions follow components.
useViewportEventsubscribes for the component's lifetime, with callbacks that don't need memoizing.useHotkeysinstalls window-level shortcuts, and passing a falsy value instead of the mapping disables them; memoize the mapping, or the listener re-installs on every render. - Log subscriptions return an unsubscribe function: call it in the effect cleanup, as in Errors and logging.
- Unloading a document is
resetPdf()fromuseResetPdf, which returns the load state to'initial'and invalidates the old document's ids, snapshots, and revisions. - Buffers. Don't reuse a buffer passed to
loadPdf; pass a copy instead.
Features without an equivalent
Plan around these before you migrate. Most of them keep working where they are: the server opens the file that prepareDocument() produced and continues with iText.
| iText feature | In @sdk-pdf/react |
|---|---|
Generating documents with the layout engine (Document, Paragraph, Table), barcodes, and vector SVG | No equivalent: generate on the server and load the result; SVG images placed with addImage are rasterized on save |
HTML to PDF (pdfHTML, HtmlConverter) | No equivalent: keep it on your server |
Digital signatures (PdfSigner) and signature validation | No equivalent; signature widgets show isCryptoSigned, and the SDK never signs |
PDF/A, PDF/UA, and Well-Tagged PDF documents (PdfADocument, PdfUADocument, WellTaggedPdfDocument) | No equivalent: keep them on your server |
Encryption (setStandardEncryption, setPublicKeyEncryption), compression settings, pdfOptimizer | No equivalent: apply them on the server after the upload |
Append mode (useAppendMode()) | No save option for it |
| Redaction (pdfSweep) and OCR (pdfOCR) | No equivalent |
Form flattening (flattenFields) and XFA flattening (pdfXFA) | No equivalent: flatten on the server |
Merging and copying pages between documents (PdfMerger, copyPagesTo) | No equivalent: merge on the server and load the result |
Paths, shapes, opacity (PdfExtGState), and watermarks | No equivalent; freehand strokes can become ink with useInkAnnotationCreate |
Creating text and link annotations (PdfTextAnnotation, PdfLinkAnnotation) | No equivalent; the border color and width of selected shapes, lines, and ink can be changed |
Outlines (PdfOutline) and document metadata (PdfDocumentInfo) | No equivalent |
Regular-expression search and match rectangles (RegexBasedLocationExtractionStrategy) | useDocumentSearch finds plain text; match a pattern in paragraph content yourself |
Cropping and resizing pages (setCropBox, setMediaBox) | No equivalent |
| Batch jobs without a user, such as indexing or conversion pipelines | Keep them on your server |