Migration

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/react is in beta, and its exports and APIs may change between releases. Pin the exact package version and review the changes when you update.

Migration checklist

Overview

How the two integrations differ

iText Core is a server-side library:

  • It ships as Maven artifacts under com.itextpdf. The itext-core POM 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 PdfDocument opens in one of three modes: reading (a PdfReader), writing (a PdfWriter and 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.

  • PdfProviders hosts the document and its configuration in your React tree, and useLoadPdf loads the bytes your server returns.
  • Viewport renders 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 useDocumentSaving builds 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.
ConcerniText Core@sdk-pdf/react
Where it runsJava (or .NET) on your serverThe browser, inside your React app
What users seeThe output file, in a viewer you don't controlViewport, with your own toolbars and panels
Document lifetimeOpened and closed within a requestLoaded once per session; built into a file when you save
ChangesWritten to a new file when the document closesRecorded as undoable operations
Pages1-based numbersSession-scoped page ids
CoordinatesUser space, origin set by the media box (usually the bottom left)User space; widgets measured from the bottom-left corner
FormsFields by full name, string values, flatteningWidgets with a fieldName, typed values, no flattening
Adding contentPdfCanvas operators and the layout engineParagraphs and image stamps by hook or tool, ink by hook
DiagnosticsExceptions and SLF4J logsTyped errors and logMessages
LicensingAGPL, or a commercial license key loaded at startupThe package from a private registry

What this means for your code:

  1. 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.
  2. 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().
  3. 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.
  4. Replace numbers with ids in the browser, and convert at the edges with usePages.
  5. Rethink forms as widgets. Users fill fields in the viewport, and each widget's value is typed: a string, a boolean, or option indices.
  6. Choose the content policy. The default PDF_EDITOR makes everything editable; a form-filling app starts with PDF_VIEWER.
  7. 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/reactNotes
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 callPdfProviders takes no key.
new PdfDocument(new PdfReader(...))loadPdf(Uint8Array) from useLoadPdffetch the bytes from your server first.
new ReaderProperties().setPassword(bytes)loadPdf(data, { password })
BadPasswordExceptionInvalidPasswordErrorA rejected loadPdf promise.
pdfDoc.close() on a document opened for readingresetPdf() from useResetPdf
pdfDoc.getNumberOfPages()usePageCount0 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 entriesWith the rotation applied.
page.getRotation(), page.setRotation(degrees)rotation, and setPageRotation(pageIdOrPageIds, rotation) from useSetPageRotationAbsolute angles in both.
pdfDoc.addNewPage(index, pageSize)createPage(width, height, insertAfterPageId?) from usePageCreateInserts 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, useWidgetsGetterWidgets in reading order, not a map of fields.
form.getField(name).setValue(value), getValueAsString()useWidgetValueOne widget per hook call.
TextFormFieldBuilder, CheckBoxFormFieldBuilder, and the other builders, addFieldcreateWidgets(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 useBlockDeleteDeleting a widget keeps its stored field value.
form.flattenFields(), partialFormFlattening(name)No equivalentFlatten on the server after the upload.
PdfCanvas beginText(), moveText(), showText()createParagraph(pageId, x, y) from useParagraphCreate, getParagraphEditorOr the addText tool.
PdfCanvas.addImageFittedIntoRectangle(), addImageAt(), layout ImageuseStampAnnotationCreate with stageImagePlaces the image as a stamp annotation at given coordinates; the { type: 'addImage', image } tool places it where the user clicks.
PdfCanvas paths, PdfExtGStateNo equivalentFreehand strokes can become an ink annotation with useInkAnnotationCreate.
Creating text and link annotations (PdfTextAnnotation, PdfLinkAnnotation)No equivalentStamps 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 documentNo equivalentMerge on the server and load the result.
copyPagesTo() for one page rangeprepareDocument({ pageIds }) from useDocumentSavingSaves only the listed pages, in the listed order. See Merge and split pages.
PdfSplitter (several files)One prepareDocument({ pageIds }) call per partSplitting by size has no equivalent.
PdfTextExtractor.getTextFromPage(page)getParagraphsByPage(pageId) from useProgrammaticEditorParagraph text and bounds.
RegexBasedLocationExtractionStrategyuseDocumentSearchPlain-text queries, no regular expressions; matches carry page and paragraph ids, not rectangles.
new PdfWriter(outputStream) and close()prepareDocument() from useDocumentSavingResolves with a Uint8Array.
An application/pdf response with Content-Disposition: attachmentdownloadDocument()No file name option.
StampingProperties.useAppendMode(), WriterProperties.setStandardEncryption()No equivalent
PdfException, IOExceptionThe SDK error classesSee Errors and logging.
SLF4J logginglogMessagesThe SDK never prints to the console.
close(), try-with-resourcesNothing to closeSee Cleanup and lifecycle.
PdfSigner, PdfADocument, PdfUADocument, pdfHTML, pdfSweep, pdfOCRNo equivalentKeep 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/react

Set 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.loadLicenseFile has no counterpart: the registry token in .npmrc installs the package, and PdfProviders takes no key.
  • Pick the content policy deliberately. PdfProviders defaults to CONTENT_POLICY_PRESET.PDF_EDITOR, where text, annotations, links, and widgets are all editable. An app that replaces server-side form filling starts with PDF_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 as widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive'), and 'hidden' doesn't render that kind of content at all; useSetInteractionMode lists 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. loadPdf takes a Uint8Array. Authentication, headers, and error responses belong to your own fetch.
  • Keep a copy for retries. The document reads from the given buffer for its whole lifetime, and another loadPdf call 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 of BadPasswordException. Without a password, the empty password is tried, as the PDF specification prescribes. The password now goes from the user to loadPdf in 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 with resetPdf() 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 inside Viewport, with no request to the server. The content policy decides what is allowed.
  • Render only when ready. Viewport throws DocumentNotLoadedError when no document is loaded, so render it when the load state is 'ready'.
  • Previews without server rendering. PageCanvas renders 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-color style 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

OperationiText@sdk-pdf/react
CountgetNumberOfPages()usePageCount, which re-renders when the count changes
SizegetPageSize() without the rotation, getPageSizeWithRotation() with itwidth and height of each usePages() entry, with the rotation applied
InsertaddNewPage(index, pageSize) at a 1-based positioncreatePage(width, height, insertAfterPageId?) after the given page, or at the end; no default size
MovemovePage(pageNumber, insertBefore)moveBefore(ids, beforePageId?) (the start when omitted) or moveAfter(ids, afterPageId?) (the end when omitted)
RotatesetRotation(degrees), absolute; a multiple of 90, not checkedsetPageRotation(ids, rotation), absolute; -90 and 450 are normalized, and angles off a quarter turn throw InvalidInputError
RemoveremovePage(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, and getPageIndex(pageId) from usePageIndexGetter goes the other way with a zero-based index (add 1 for the page number; undefined when the page is gone). Ids are session-scoped: persist numbers, not ids.
  • No cropping or resizing. setCropBox() and setMediaBox() 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

ConceptiText@sdk-pdf/react
LookupgetField(name), getAllFormFields()useWidgets() (re-renders), or await getWidgets() from useWidgetsGetter (one-time, loads every page with widgets first); filter by fieldName
Shared valuesA field with several widgetsEvery widget with the same field name shows and synchronizes the same value
ValuessetValue(String), getValueAsString()useWidgetValue(widget): string for text and choice, boolean for checkbox and radio, option indices for multiChoice
Checkbox statesAppearance state names, such as Yes and Offtrue or false; the stored value when checked is the widget's checkedValue
CreationTextFormFieldBuilder, CheckBoxFormFieldBuilder, ChoiceFormFieldBuilder, RadioFormFieldBuilder, PushButtonFormFieldBuilder, SignatureFormFieldBuildercreateWidgets(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-onlysetReadOnly(true)setWidgetProps(ids, { isReadOnly: true }, options?)
Rename and removerenameField(oldName, newName), removeField(name)setWidgetProps(ids, { fieldName }), and deleteBlocks(widgetIds) from useBlockDelete, which keeps the stored field value
FlatteningflattenFields(), partialFormFlattening(name)No equivalent

What to know

  • Coordinates. A widget's x, y, width, and height are user-space units measured from the bottom-left corner of the page. iText's Rectangle matches 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 through useWidgetValue; 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() and getValueAsString(), 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. isReadOnly restricts filling the field, not authoring the widget; with { excludeFromHistory: true }, an undo can't unlock a submitted form.
  • Date fields. A text widget's format exposes Acrobat date formats as FormatType.DateTime, recognized without running scripts; see FormatType.
  • Signatures. signature widgets carry a read-only isCryptoSigned, and the SDK never signs: keep PdfSigner on 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 createParagraph or the addText tool 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, unlike setFontAndSize(). Users style text with a toolbar built on useSelectionTextStyle: font family, size, color, bold, italic, alignment, and spacing.
  • Editing existing text is new. Under PDF_EDITOR, users edit page text in place, and getParagraphEditor(paragraphId) rewrites a paragraph from code.
  • Images are stamp annotations. stageImage(blob) validates and measures the image. The size goes on the placement, width and height in user-space units, for both createStamp and the addImage tool: 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. useInkAnnotationCreate creates 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, PdfExtGState opacity, 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 a createParagraph call 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. PdfMerger and copyPagesTo into another document have no equivalent, and neither has inserting pages from another file. Merge with iText on the server, then load the result with loadPdf.
  • Splitting is one save per part. For PdfSplitter's several output documents, call prepareDocument({ pageIds }) once per part. The saved file has the pages in the order of pageIds, each page at most once. Splitting by file size has no equivalent.
  • Within the document. duplicatePage(sourcePageId, insertAfterPageId?) from usePageDuplicate copies a page with all its content, and usePageMove reorders pages, as in Page manipulation.

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-based pageNumber, the user-space box bounds ({ x, y, width, height }), and the paragraph layout, such as textAlign and lineHeight. 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. useDocumentSearch finds a plain-text query, with isCaseSensitive and isWholeWord options, highlights the matches on the pages, and scrolls to the active one. It takes no regular expressions, and a match carries its pageId, paragraphId, and text rather than the rectangles of RegexBasedLocationExtractionStrategy. For a pattern, match content yourself and scroll to the paragraph with scrollToBlock(paragraphId) from useScrollToBlock.
  • 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 a Uint8Array. The Java endpoint stores them, or opens them with PdfReader for server-side steps such as flattening or signing.
  • returnOriginalIfUnchanged defaults to true, returning the buffer passed to loadPdf when nothing changed; set it to false when the bytes will be transferred or mutated, or the loaded document's buffer is detached and later reads throw.
  • Save options. Besides returnOriginalIfUnchanged, pageIds saves 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
BadPasswordExceptionInvalidPasswordError: the document is encrypted, and the password is missing or wrong
PdfException for a file that can't be processedDocumentFormatError: 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 PdfReaderErrors from your own fetch
Invalid arguments, such as IndexOutOfBoundsException for a page number out of rangeInvalidInputError
—DocumentNotLoadedError: an operation needs a document, and none is loaded
A cross-reference table iText rebuilds, logged at error levelA 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 GenericError and carries a static message and an optional data object; UninitializedError covers objects used before their asynchronous initialization completes.
  • Logs are opt-in. Each message carries level (LogLevel.Debug, Info, Warning, or Error), reason, a static message, data, and stack. There are no loggers to configure: filter by level in 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 PdfProviders for the session; there is no close() and no stream to manage.
  • Subscriptions follow components. useViewportEvent subscribes for the component's lifetime, with callbacks that don't need memoizing. useHotkeys installs 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() from useResetPdf, 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 featureIn @sdk-pdf/react
Generating documents with the layout engine (Document, Paragraph, Table), barcodes, and vector SVGNo 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 validationNo 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, pdfOptimizerNo 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 watermarksNo 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 pipelinesKeep them on your server

On this page