Migration

Migrating from Foxit PDF SDK for Web

This guide moves a web application built on Foxit PDF SDK for Web to @sdk-pdf/react. It covers both ways Foxit apps are built: the complete viewer of the UIExtension layer (PDFUI, with its ribbon, panels, and add-ons), and the PDFViewCtrl layer alone (PDFViewer) under a UI of your own. Both work on the same PDFDoc and PDFPage objects, so most @sdk-pdf/react replacements are the same for either setup. 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

Foxit PDF SDK for Web is built in three layers on a WebAssembly engine:

  • Core: PDFDoc and PDFPage read and change the document. Pages are zero-based indexes, page ranges are two-dimensional arrays such as [[0], [2, 4]], and rectangles are PDF coordinates (left, bottom, right, top).
  • PDFViewCtrl: PDFViewer renders the document into a DOM element you pass to init(). It owns the scrolling, zoom, view modes, and the active tool (a state handler), and it reports what happens through viewer events. getPDFDocRender() and getPDFPageRender(index) reach the rendered document and pages.
  • UIExtension: PDFUI wraps a PDFViewer and mounts a complete viewer into renderTo: a ribbon or mobile appearance, a layout template you adjust with fragments, and features delivered as add-ons from lib/uix-addons (thumbnails, undo and redo, print, search, form design, and more), often loaded together as allInOne.js.
  • You serve the package's lib folder (engine modules, workers, add-ons, locales, stamps) as static files and point libPath at it, optionally with fonts from external. A licenseSN and licenseKey go into the jr options, and the license decides which feature modules load; advanced editing is licensed separately.
  • Edits change the PDFDoc in the engine, and getFile() exports it as a PDF File. Annotations and form data can also travel separately as JSON, FDF, or XFDF.

@sdk-pdf/react is a React library: the SDK owns the document, and you own the UI.

  • There is no prebuilt UI to configure. Viewport is the main view, PageCanvas renders single pages, and every toolbar, panel, and dialog is your React code driven by hooks.
  • PdfProviders hosts the document state and the configuration inside your React tree; the SDK renders unstyled, and your design system supplies the look through the theme configuration.
  • Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers the history; useDocumentSaving builds the final PDF when you need it.
  • Pages and blocks are addressed by session-scoped ids, and positions are user-space units with the origin at the bottom-left corner of the page.
ConcernFoxit PDF SDK for Web@sdk-pdf/react
UIPDFUI with appearances, layout templates, fragments, and add-onsViewport and PageCanvas only; you build toolbars, panels, and dialogs
Mountingnew PDFUI({ renderTo }) or new PDFViewer() plus init() in an effectPdfProviders and components in your React tree
DeploymentThe lib folder under libPath, fonts, license key, Service-Worker-AllowedThe package from a private registry
State accessPDFViewer, PDFDocRender, and PDFDoc objects, viewer eventsState hooks that re-render, command hooks, and event hooks
PagesZero-based indexes and two-dimensional page rangesSession-scoped page ids
ToolsState handlers switched with StateHandlerManager.switchTo()An interaction tool paired with a content policy
FormsPDFForm, PDFFormField, and Widget objectsWidgets with a fieldName, values bound with useWidgetValue
Editing textStd Edit, or the separately licensed Adv EditBuilt in under the PDF_EDITOR content policy
UndoThe undo-redo add-on, for annotation, bookmark, and Adv Edit changesuseUndoRedo for the whole document
PersistencegetFile() for the PDF; JSON, FDF, or XFDF for annotations and form dataprepareDocument() for the PDF

What this means for your code:

  1. Build the UI. Every part of the Foxit UI your users rely on (ribbon groups, the thumbnail and comment panels, context menus, the print and password dialogs) becomes a React component built from the hooks in the API reference.
  2. Drop the mount effect. The PDFUI or PDFViewer instance, the getPDFViewer() promise, and any context that shares them are replaced by PdfProviders and hooks.
  3. Replace objects with hooks. PDFDocRender, PDFDoc, PDFForm, StateHandlerManager, and add-on calls become hook calls in the components that need them.
  4. Replace indexes with page ids. Every page index and page range in your state becomes a page id; convert at the edges (deep links, stored data) with usePages and usePageIndexGetter.
  5. Rethink annotations. @sdk-pdf/react edits page text, form widgets, image stamps, and pages, creates ink from code, and restyles the borders of existing shapes, lines, and ink; creating markup annotations, comments, and their JSON, FDF, or XFDF exchange have no equivalent.
  6. Map permissions such as customs.getDocPermissions to content policies, and decide whether to expose editing of existing text, which is built in.
  7. Remove deployment pieces: the copied lib folder, fonts, the license key, and the service worker header.
API map

Rows marked No equivalent are covered in Features without an equivalent. pdfui is a PDFUI, pdfViewer the PDFViewer from await pdfui.getPDFViewer(), docRender its getPDFDocRender(), and pdfDoc its getCurrentPDFDoc().

Foxit PDF SDK for Web@sdk-pdf/reactNotes
new PDFUI({ viewerOptions: { libPath, jr }, renderTo, addons })<PdfProviders>No libPath, license key, or add-ons.
new PDFViewer({ libPath, jr }), pdfViewer.init(element)<PdfProviders> and <Viewport />
openPDFByFile(file, { password, fileName })loadPdf(Uint8Array, { password }) from useLoadPdfRead a File or Blob into bytes first.
openPDFByHttpRangeRequest({ range: { url, headers } })No equivalentfetch the whole file, with your headers, then loadPdf.
Open options annotsJson, fdf, beforeRenderPDFDocNo equivalent
ViewerEvents.openFileSuccessloadState.status === 'ready'
pdfui.close(), pdfViewer.close()resetPdf() from useResetPdf
customs.closeDocBefore, customs.closeDocAfterYour own code around resetPdf()See Saving and downloading.
pdfDoc.getPageCount()usePageCount
docRender.getCurrentPageIndex(), ViewerEvents.pageNumberChangeuseCurrentPageIdRe-renders with the id of the page in the middle of the viewport.
docRender.goToPage(index)scrollToPage(pageId) from useScrollToPageNo offset within the page.
pdfDoc.insertPage(index, width, height), insertBlankPages(range, width, height)createPage(width, height, insertAfterPageId?) from usePageCreateInserts after a page, not at an index.
pdfDoc.removePage(index), removePages(range)deletePages(pageIdOrIds) from usePageDelete
pdfDoc.movePageTo(index, destIndex), movePagesTo(range, destIndex)moveBefore or moveAfter from usePageMove
pdfDoc.rotatePages(range, rotation), pdfPage.setRotation(rotation)setPageRotation(pageIdOrIds, degrees) from useSetPageRotationDegrees, not the Rotation enum.
extractPages() then insertPages() to copy a pageduplicatePage(sourcePageId, insertAfterPageId?) from usePageDuplicate
extractPages() to a new PDFprepareDocument({ pageIds }) from useDocumentSavingSaves only the listed pages, in the listed order.
insertPages(), mergePDFDoc(), setPagesBox()No equivalent
pdfViewer.rotateTo(degree) (view only)No equivalentuseSetPageRotation edits the document.
pdfViewer.zoomTo(scale), docRender.getScale()useViewportScale
pdfViewer.zoomTo('fitWidth'), zoomTo('fitHeight')useViewportAutoFitScale, useViewportFitScaleA one-time fit, not a fixed mode.
ViewerEvents.zoomToSuccessscale from useViewportScale in a useEffectUpdates once a zoom gesture or animation ends.
defaultViewMode, view mode add-onsNo equivalentOne scrollable layout.
Thumbnail add-on, pdfDoc.loadThumbnail()PageCanvas
pdfDoc.getPDFForm(), await form.getField(name)useWidgets filtered by fieldNameWidgets, not fields.
await field.getValue(), field.setValue(value)useWidgetValueInside a component.
FormFillerService.onDataChanged()widgetInput from useViewportEventReports values the user changes in the main view.
The other FormFillerService listeners and interceptorsNo equivalent
form.createTextField({ pdfRect, pageIndex }) and the other create* methodscreateWidgets(props) from useWidgetsCreate
Form designer add-on, STATE_HANDLER_CREATE_FIELD_* state handlersThe { type: 'addWidget', widgetProps } interaction tool
page.removeAnnotByObjectNumber() on a widgetuseBlockDelete with widget ids
form.exportToFile(), importFromFile(), resetForm()No equivalent
page.addAnnot(json) for inkuseInkAnnotationCreateSee Annotations.
page.addAnnot(json) for other types, annotation state handlersNo equivalentSee Annotations.
pdfDoc.exportAnnotsToJSON(), exportAnnotsToFDF(), importAnnotsFromJSON()No equivalent
getStateHandlerManager().switchTo(name)useSetInteractionModeSee Editing and interaction tools.
customs.getDocPermissions: () => 0PDF_VIEWER or PDF_PRINTER from CONTENT_POLICY_PRESET
Appearances, layout templates, fragments, getComponentByName()Your own components and the theme configuration, see Theming and customization
UndoRedoAddon undo(), redo()useUndoRedo
UndoRedoAddon undoAll(), invoke()No equivalent
pdfDoc.isDocModified()useDocumentRevision, useDocumentRevisionGetterCompares against the revision you saved.
pdfDoc.getFile({ fileName })prepareDocument() from useDocumentSavingResolves with a Uint8Array, not a File.
Download button of the built-in UIdownloadDocument()No file name option.
getFile() option flags, getStream()No equivalent
Rejected openPDFByFile, ViewerEvents.openFileFailedThe rejected loadPdf promiseSee Errors and logging.
Log.LoggerFactory.toggleLogger(), setLogLevel()logMessagesThe SDK never prints to the console.
pdfui.destroy()Nothing to callSee Cleanup and lifecycle.
pdfPage.getText(), pdfDoc.getText(range)getParagraphsByPage(pageId) from useProgrammaticEditorParagraph text and bounds.
pdfDoc.getTextSearch(), searchText()useDocumentSearchHighlights the matches and scrolls to the active one.
pdfui.getSelectedTextInfo(), pdfPage.getCharInfoByIndex()No equivalentuseSelectionTextStyle reads the selection's style.
pdfDoc.applyRedaction(), pdfDoc.sign(), pdfui.registerSignHandler(), the stamp state handlerNo equivalent
Form JavaScript, XFA forms, pdfDoc.autoRecognitionForm()No equivalent
pdfViewer.print(), printCurrentView(), takeSnapshot()No equivalent
pdfDoc.addHeaderFooter(), addWatermark(), getPageLabels(), getLayerNodesJson()No equivalent
pdfViewer.getBookmarkDataService(), compareDocuments(), getReadAloudService(), 3D contentNo equivalent
pdfViewer.zoomTo(scale, position)setScale(scale, pivot) from useViewportScaleThe pivot is a viewport point, not a point on the page.

Install and set up

Before: Foxit PDF SDK for Web

Foxit PDF SDK for Web installs as @foxitsoftware/foxit-pdf-sdk-for-web-library. The build copies the package's lib folder into the app's static files, and the server sends a Service-Worker-Allowed header, which the SDK needs since version 10.0:

npm install @foxitsoftware/foxit-pdf-sdk-for-web-library
// webpack.config.js
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  plugins: [
    new CopyWebpackPlugin({
      patterns: [
        {
          // Engine modules, workers, add-ons, locales, and stamps, served at /foxit-lib.
          from: path.resolve(
            'node_modules/@foxitsoftware/foxit-pdf-sdk-for-web-library/lib'
          ),
          to: 'foxit-lib',
          force: true,
        },
      ],
    }),
  ],
  devServer: {
    headers: { 'Service-Worker-Allowed': '/' },
  },
}

A React component creates the PDFUI in an effect, with the license from license-key.js, and destroys it on unmount:

import { useEffect, useRef } from 'react'
import * as UIExtension from '@foxitsoftware/foxit-pdf-sdk-for-web-library/lib/UIExtension.full.js'
import '@foxitsoftware/foxit-pdf-sdk-for-web-library/lib/UIExtension.css'
import { licenseKey, licenseSN } from './license-key'

function PDFViewer() {
  const elementRef = useRef(null)

  useEffect(() => {
    const pdfui = new UIExtension.PDFUI({
      viewerOptions: {
        libPath: '/foxit-lib',
        jr: { licenseSN, licenseKey },
      },
      renderTo: elementRef.current,
      appearance: UIExtension.appearances.adaptive,
      addons: UIExtension.PDFViewCtrl.DeviceInfo.isMobile
        ? '/foxit-lib/uix-addons/allInOne.mobile.js'
        : '/foxit-lib/uix-addons/allInOne.js',
    })

    return () => {
      // Asynchronous: closes the document and removes the UI's DOM.
      pdfui.destroy()
    }
  }, [])

  return <div ref={elementRef} style={{ height: '100vh' }} />
}

Integrations that bring their own UI create the viewer layer alone: new PDFViewCtrl.PDFViewer({ libPath, jr }), pdfViewer.init(element), and PDFViewCtrl.css. Under React Strict Mode the effect runs twice in development, and because destroy() returns a promise, the second PDFUI can mount before the first is gone; Foxit integrations keep the instance in a ref or turn Strict Mode off.

After: @sdk-pdf/react

Add the @sdk-pdf scope to an .npmrc next to your package.json, keep the token in an environment variable, and install; Getting started has the details:

@sdk-pdf:registry=https://registry.sdk.pdf.net
//registry.sdk.pdf.net/:_authToken=${PDF_NET_REGISTRY_TOKEN}
npm install @sdk-pdf/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.

Mount PdfProviders where the viewer element was. Its props must keep their identity between renders, so define static values outside the component and memoize the rest. CanEditContext carries your app's own permission to the editing controls in the rest of this guide:

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

// Your app's permission, in place of customs.getDocPermissions: editing controls render
// only when it's true.
export const CanEditContext = createContext(false)

const readerTool: InteractionTool = { type: 'textSelect' }
const editorTool: InteractionTool = { type: 'select' }

export function App({ canEdit }: { canEdit: boolean }) {
  // The PDF application re-renders whenever a PdfProviders prop changes identity.
  const config = useMemo(
    () => ({
      theme: { viewportPadding: 16 },
      // Applied when the viewport first mounts.
      initialInteractionTool: canEdit ? editorTool : readerTool,
      initialContentPolicy: canEdit
        ? CONTENT_POLICY_PRESET.PDF_EDITOR
        : CONTENT_POLICY_PRESET.PDF_VIEWER,
    }),
    [canEdit]
  )

  return (
    <CanEditContext.Provider value={canEdit}>
      <PdfProviders {...config}>
        <DocumentLoader url="/api/files/contract.pdf" />
      </PdfProviders>
    </CanEditContext.Provider>
  )
}

What to know

  • Nothing to serve or license in code. The install is the registry scope and the package: there is no lib folder to copy, no libPath, jr engine paths, or fontPath, no licenseSN or licenseKey, and no add-on list.
  • No mount effect. The SDK renders as components, so the element ref, the effect that creates PDFUI or PDFViewer, the Strict Mode guard, and the destroy() call go away. Any component under PdfProviders reaches the document through hooks instead of getPDFViewer().
  • Choose the starting permissions. customs.getDocPermissions defaults to -1, all permissions, and PdfProviders defaults to the select tool and PDF_EDITOR, where text, annotations, links, and widgets are all editable. For users who may not edit, where your app returned 0, start with PDF_VIEWER (forms stay fillable) or PDF_PRINTER (forms read-only too) and the textSelect tool, as above, and leave the editing controls out: the content policy is a mode your UI can switch with useSetInteractionMode, as in Editing and interaction tools, not a permission.

Load and reset a document

Before: Foxit PDF SDK for Web

openPDFByFile(file, options) opens a File, Blob, ArrayBuffer, TypedArray, or DataView; openPDFByHttpRangeRequest loads a remote file on demand through HTTP range requests. Both exist on PDFUI and PDFViewer, and both close the current document first:

await pdfui.openPDFByFile(file, {
  fileName: file.name,
  password: enteredPassword,
})

// Large remote files, read in ranges; the server must support Range requests and CORS.
await pdfui.openPDFByHttpRangeRequest(
  {
    range: {
      url: '/api/files/contract.pdf',
      headers: { Authorization: `Bearer ${token}` },
    },
  },
  { fileName: 'contract.pdf' }
)

// A closeDocBefore callback that returns false cancels both an explicit close and opening
// another file.
await pdfui.close()

The open options also take annotsJson to import annotations while opening, fdf to import an FDF file of form data (fdf.type 1) or annotations (2), beforeRenderPDFDoc to prepare the document before the first render, and isRenderOnDocLoaded. With the complete viewer, an encrypted document brings up the built-in password dialog.

After: @sdk-pdf/react

loadPdf from useLoadPdf takes the file bytes, and the load state decides what to render. A URL becomes your own fetch, which also takes the headers you passed in range.headers:

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

function DocumentLoader({ url }: { url: string }) {
  const [loadState, loadPdf] = useLoadPdf()
  const resetPdf = useResetPdf()
  const bytesRef = useRef<Uint8Array | null>(null)
  const openedUrlRef = useRef(url)
  const [isAskingForPassword, setIsAskingForPassword] = useState(false)
  const [loadError, setLoadError] = useState<unknown>()

  // Like opening another file in Foxit: a new URL closes the current document. Skips the
  // first mount.
  useEffect(() => {
    if (openedUrlRef.current === url) return
    openedUrlRef.current = url
    bytesRef.current = null
    setIsAskingForPassword(false)
    setLoadError(undefined)
    resetPdf()
    // resetPdf stays out of the dependencies: only a URL change closes the document.
  }, [url])

  async function open(password?: string) {
    setLoadError(undefined)
    try {
      if (!bytesRef.current) {
        // The whole file: there is no range loading. Add your request headers here.
        const response = await fetch(url, { credentials: 'include' })
        if (!response.ok)
          throw new Error(`Download failed with status ${response.status}`)
        const bytes = new Uint8Array(await response.arrayBuffer())
        // The URL changed during the download: drop this response.
        if (openedUrlRef.current !== url) return
        bytesRef.current = bytes
      }

      // loadPdf reads from the buffer it receives, so keep the original for a retry and
      // pass a copy.
      await loadPdf(bytesRef.current.slice(), { password })
      setIsAskingForPassword(false)
    } catch (error) {
      // Replaces the built-in password dialog.
      if (error instanceof InvalidPasswordError) setIsAskingForPassword(true)
      else setLoadError(error)
    }
  }

  if (loadError !== undefined)
    return <LoadErrorMessage error={loadError} onRetry={() => void open()} />
  if (isAskingForPassword)
    return <PasswordDialog onSubmit={(password: string) => void open(password)} />
  if (loadState.status === 'initial')
    return <button onClick={() => void open()}>Open</button>
  if (loadState.status === 'loading') return <p>Loading the document…</p>

  return <PdfApplication />
}

For a file input, pass new Uint8Array(await file.arrayBuffer()). resetPdf() from useResetPdf closes the document, and the load state returns to 'initial'.

What to know

  • Bytes, not files or URLs. loadPdf takes a Uint8Array. openPDFByHttpRangeRequest and its on-demand loading have no equivalent: fetch the whole file, with the headers and credentials your server needs, and pass the bytes.
  • 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, not a dialog. Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with InvalidPasswordError. The prompt is your component.
  • No import on open. annotsJson, fdf, and beforeRenderPDFDoc have no equivalent, and neither does importing annotation or form data later.
  • No close hooks. customs.closeDocBefore and closeDocAfter have no counterpart: resetPdf() closes at once, so check for unsaved changes before calling it, as the Saving and downloading example does. To open another document, check the same way, then call resetPdf() and loadPdf.
  • Closing invalidates ids. After resetPdf(), page and block ids, widget snapshots, and document revisions from the old document are no longer valid.

Display the document

Before: Foxit PDF SDK for Web

The complete viewer renders the document inside its appearance: RibbonAppearance on desktop, MobileAppearance on phones, or AdaptiveAppearance, which picks one by device. Integrations reshape it with fragments and hide components at runtime:

const FRAGMENT_ACTION = UIExtension.UIConsts.FRAGMENT_ACTION

const pdfui = new UIExtension.PDFUI({
  viewerOptions: { libPath: '/foxit-lib', jr: { licenseSN, licenseKey } },
  renderTo: elementRef.current,
  appearance: UIExtension.appearances.adaptive.extend({
    getDefaultFragments() {
      // Remove the Delete item from the thumbnail context menu.
      return [
        { target: 'contextmenu-item-thumbnail-delete', action: FRAGMENT_ACTION.REMOVE },
      ]
    },
  }),
  addons: '/foxit-lib/uix-addons/allInOne.js',
})

// Hide a component and keep the SDK from showing it again.
const applyRedactionItem = await pdfui.getComponentByName('fv--contextmenu-item-apply')
applyRedactionItem.hide()
applyRedactionItem.keepState()

With PDFViewCtrl alone, pdfViewer.init(element) renders the pages into your element, styled by PDFViewCtrl.css.

After: @sdk-pdf/react

Viewport renders the pages, and your components render everything else. A typical shell puts a toolbar above the viewport and a thumbnail sidebar beside it, as Building a PDF application walks through:

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

// A fuller theme than the one in App: pass it as PdfProviders' `theme` prop, defined
// outside components so its identity is stable.
export const theme: Config['theme'] = {
  viewportPadding: [24, 16],
  classNames: { scrollViewport: 'pdf-viewport', page: 'pdf-page' },
  styles: {
    scrollViewport: {
      '--widget-overlay-background': 'rgba(37, 99, 235, 0.08)',
    } as React.CSSProperties,
  },
}

function PdfApplication() {
  // Foxit opens documents at fit width on desktop; this fits once per loaded document.
  useViewportAutoFitScale('width')

  return (
    <div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: '100vh' }}>
      {/* Your ribbon: the navigation, zoom, tool, and history controls from this
         guide. */}
      <Toolbar />
      <div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', minHeight: 0 }}>
        <ThumbnailsSidebar />
        {/* The viewport fills its parent, so the parent needs an explicit size. */}
        <div style={{ minHeight: 0 }}>
          <Viewport />
        </div>
      </div>
    </div>
  )
}

What to know

  • There is nothing to hide. Appearances, layout templates, fragments, getComponentByName, and keepState have no counterpart. Selection, text editing, form filling, and links work inside Viewport within the content policy, and zoom gestures are built in; every control around it exists only if you build it.
  • Theming is CSS you own. theme.classNames and theme.styles target the SDK's elements (scrollViewport, page, pageCanvas, resizable, widgetOverlay, and more), and CSS variables such as --selection-color and --widget-overlay-background tune the selection and widget visuals; Theming and customization lists them. UIExtension.dark-variable.css and the theme colors of the complete viewer have no counterpart: dark mode is your stylesheet.
  • Dialogs are yours. The pieces a custom viewerUI (extending XViewerUI or TinyViewerUI) overrides in Foxit, such as alerts, confirmations, the password prompt, context menus, and the loading overlay, are ordinary React UI in your app.
  • Render only when ready. Viewport throws DocumentNotLoadedError when no document is loaded.
  • One layout. defaultViewMode ('continuous-view-mode' or 'single-page-view-mode'), the facing and horizontal modes of the h-continuous add-on, custom Viewmodes, and customs.ScrollWrap have no equivalent: Viewport shows the pages in one scrollable area.

Pages and navigation

Before: Foxit PDF SDK for Web

PDFDocRender navigates by zero-based page index, and the pageNumberChange viewer event reports the new current page as a one-based number:

const pdfViewer = await pdfui.getPDFViewer()
const { ViewerEvents } = UIExtension.PDFViewCtrl.constants

pdfui.addViewerEventListener(ViewerEvents.openFileSuccess, (pdfDoc) => {
  pageCountLabel.textContent = pdfDoc.getPageCount()
})

pdfui.addViewerEventListener(ViewerEvents.pageNumberChange, (pageNumber) => {
  pageInput.value = pageNumber
})

pageInput.onchange = () => {
  // Page indexes start at 0.
  pdfViewer.getPDFDocRender().goToPage(Number(pageInput.value) - 1)
}

After: @sdk-pdf/react

usePages bridges positions and ids: a position is an index in the list, and navigation with useScrollToPage takes an id. useCurrentPageId re-renders with the page in the middle of the viewport:

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

function PageNumberInput() {
  const pages = usePages()
  // Replaces getCurrentPageIndex() and the pageNumberChange listener.
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()
  // What the user is typing; undefined shows the current page.
  const [draft, setDraft] = useState<string>()

  // While there is no current page yet, show the first page.
  const currentIndex = Math.max(
    0,
    pages.findIndex((page) => page.id === currentPageId)
  )

  function goToDraft() {
    const page = pages[Number(draft) - 1]
    if (page) scrollToPage(page.id, { behavior: 'smooth' })
    setDraft(undefined)
  }

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        goToDraft()
      }}
    >
      <label>
        Page
        <input
          inputMode="numeric"
          value={draft ?? String(currentIndex + 1)}
          onChange={(event) => setDraft(event.currentTarget.value)}
          onBlur={goToDraft}
        />
        of {pages.length}
      </label>
    </form>
  )
}

When a handler needs a page's position, such as for analytics, getPageIndex(pageId) from usePageIndexGetter resolves an id to its zero-based index without re-rendering on order changes.

What to know

  • Ids survive reordering; indexes don't. An id keeps pointing at the same page while pages are added, removed, and rearranged by the user, where an index would drift to another page. Ids are session-scoped: persist page numbers, not ids, and resolve them with usePages once the document is ready.
  • Scroll options. scrollToPage aligns the page with vertical ('top' by default) and horizontal ('nearest' by default), and behavior sets the animation. The offset and isPDFPoint arguments of goToPage, which jump to a point on the page, have no equivalent; scrollToBlock(blockId, options?) from useScrollToBlock scrolls to a block, such as a form widget.
  • The page count. Where you read getPageCount() once after openFileSuccess, usePageCount re-renders when pages are added or removed, and returns 0 while no document is loaded.

Page manipulation

Before: Foxit PDF SDK for Web

PDFDoc changes pages by index. Most methods take two-dimensional page ranges, where [[0], [2, 4]] means page 1 and pages 3 to 5, and sizes are PDF points:

const pdfDoc = pdfViewer.getCurrentPDFDoc()

// An A4 page before page 1.
await pdfDoc.insertPage(0, 595, 842)
await pdfDoc.removePages([[1, 3]])
await pdfDoc.movePagesTo([[0]], 2)
// Sets the rotation; the Rotation enum runs from 0 to 3 for 0° to 270°.
await pdfDoc.rotatePages([[0, 2]], 1)

// Copying a page means extracting it and inserting the extracted PDF.
const extracted = await pdfDoc.extractPages([[0]])
await pdfDoc.insertPages({
  destIndex: 1,
  file: new Blob(extracted, { type: 'application/pdf' }),
  password: '',
  startIndex: 0,
  endIndex: 0,
})

After: @sdk-pdf/react

Each page operation is a hook that returns a command taking page ids: usePageCreate, usePageDelete, usePageMove, useSetPageRotation, and usePageDuplicate. These buttons replace the thumbnail context menu:

import { useContext } from 'react'
import {
  usePageCreate,
  usePageDelete,
  usePageDuplicate,
  usePageMove,
  usePages,
  useSetPageRotation,
} from '@sdk-pdf/react'

function PageContextMenu({ pageId }: { pageId: string }) {
  const canEdit = useContext(CanEditContext)
  const pages = usePages()
  const createPage = usePageCreate()
  const deletePages = usePageDelete()
  const duplicatePage = usePageDuplicate()
  const { moveAfter } = usePageMove()
  const setPageRotation = useSetPageRotation()

  const page = pages.find((page) => page.id === pageId)
  // Page operations are editing: users who may not edit don't get the menu.
  if (!page || !canEdit) return null

  return (
    <div role="menu">
      {/* The rotation is absolute: add to the current angle. -90 is normalized to
         270. */}
      <button role="menuitem" onClick={() => setPageRotation(pageId, page.rotation - 90)}>
        Rotate left
      </button>
      <button role="menuitem" onClick={() => setPageRotation(pageId, page.rotation + 90)}>
        Rotate right
      </button>
      {/* A4 is roughly 595 × 842 user-space units; the new page goes after this one. */}
      <button role="menuitem" onClick={() => createPage(595, 842, pageId)}>
        Insert blank page
      </button>
      <button role="menuitem" onClick={() => void duplicatePage(pageId)}>
        Duplicate
      </button>
      {/* Without a target page, moveAfter moves the page to the end. */}
      <button role="menuitem" onClick={() => moveAfter(pageId)}>
        Move to end
      </button>
      <button
        role="menuitem"
        disabled={pages.length <= 1}
        onClick={() => deletePages(pageId)}
      >
        Delete page
      </button>
    </div>
  )
}

What to know

OperationFoxit PDF SDK for Web@sdk-pdf/react
InsertAt a zero-based index (insertPage) or at page ranges, in pointsAfter insertAfterPageId, or appended; width and height in user-space units (1/72 of an inch), no defaults
MoveTo a destination index (movePageTo, movePagesTo)moveBefore(ids, beforePageId?) (start when omitted) or moveAfter(ids, afterPageId?) (end when omitted)
RotateSets a Rotation enum value, 0 to 3Sets an absolute angle in degrees; -90 and 450 are normalized; angles off a quarter turn throw InvalidInputError
CopyextractPages and insertPagesduplicatePage is asynchronous and inserts after the source by default
DeleteA page index (removePage) or page rangesOne id or an array of ids
  • Undo. Deleting, moving, or rotating several pages in one call is a single undo step, and an undo can restore a deleted page; separate edits within 500 ms merge into one step.
  • No page events. Page operations your code calls emit no events: track them where you call the commands. usePages re-renders with the new page list, and useDocumentRevision changes on any edit.
  • Extracting pages. prepareDocument({ pageIds }) from useDocumentSaving saves only the listed pages, in the listed order, in place of extractPages to a new PDF.
  • Pages from other files, page boxes. insertPages from another PDF, mergePDFDoc, setPagesBox cropping and resizing, and dropping a PDF on the thumbnails to merge it have no equivalent.

Zoom and fit modes

Before: Foxit PDF SDK for Web

zoomTo takes a number or a fit mode, 'fitWidth' or 'fitHeight' (which fits the whole page), within the minScale and maxScale constructor options (0.01 and 10 by default). Documents open at defaultScale, 'fitWidth' by default:

const pdfViewer = await pdfui.getPDFViewer()

await pdfViewer.zoomTo('fitWidth')
await pdfViewer.zoomTo(1.25)

// A number, or the active fit mode.
const scale = await pdfViewer.getPDFDocRender().getScale()

pdfui.addViewerEventListener(
  UIExtension.PDFViewCtrl.constants.ViewerEvents.zoomToSuccess,
  (newScale) => {
    saveZoomPreference(newScale)
  }
)

After: @sdk-pdf/react

useViewportScale reads and sets the scale, useViewportFitScale calculates fit scales on demand, and useViewportAutoFitScale (in PdfApplication above) fits once per loaded document:

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

const zoomSteps = [0.5, 0.75, 1, 1.25, 1.5, 2, 4]

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

  // Every scale change, whatever the source: controls, gestures, or an auto fit.
  useEffect(() => saveZoomPreference(scale), [scale])

  function fitPage() {
    const { fitWidthScale, fitHeightScale } = getFitScale()
    // The smaller scale fits whole pages, like zoomTo('fitHeight').
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

  return (
    <div role="toolbar">
      <select
        value={scale}
        onChange={(event) => setScale(Number(event.currentTarget.value))}
      >
        {!zoomSteps.includes(scale) && (
          <option value={scale}>{Math.round(scale * 100)}%</option>
        )}
        {zoomSteps.map((step) => (
          <option key={step} value={step}>
            {step * 100}%
          </option>
        ))}
      </select>
      {/* Call the getter inside the handler: the result reflects the viewport size at
         that moment. */}
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={fitPage}>Fit page</button>
    </div>
  )
}

What to know

  • Fits are one-time calculations. useViewportAutoFitScale applies when a document loads and doesn't re-apply on viewport resizes or manual zooming; useViewportFitScale reflects the viewport size at the moment of the call. There is no fit mode that stays applied as the viewport resizes, and no 'fitWidth' value to read back.
  • What the fits measure. fitWidthScale fits the widest page into the viewport width and fitHeightScale the tallest page into its height, both accounting for viewportPadding.
  • Scale 1 is 100%: one user-space unit per CSS pixel. setScale and animateScale, the third item of useViewportScale(), take a positive finite number and throw InvalidInputError otherwise.
  • Zoom around a point. In place of the position argument of zoomTo, setScale(scale, pivot?) keeps the document still at pivot, a point { x, y } given as fractions of the viewport area (0 the top or left edge, 1 the bottom or right edge), the center by default. animateScale(scale, pivot?) animates the change and resolves when it ends.
  • The scale updates after a gesture. During a pinch or wheel zoom, scale keeps its value until the gesture ends; zoomGestureStart and zoomGestureEnd from useViewportEvent mark the gesture.
  • Gestures and limits. Pinch and wheel zoom are built in and tuned with zoomGestures on PdfProviders: its minScale and maxScale (0.5 and 4 by default) bound the gestures.
  • No redraw call. Foxit integrations call redraw() after the window resizes; Viewport fills its parent element and has no redraw API, so give the parent an explicit size.

Thumbnails

Before: Foxit PDF SDK for Web

The thumbnail panel comes from the thumbnail add-on: thumbnails reorder by dragging, a PDF dropped on the list is merged in, and a context menu rotates, inserts, and deletes pages. Thumbnails outside the panel come from loadThumbnail:

const pdfViewer = await pdfui.getPDFViewer()
const pdfDoc = pdfViewer.getCurrentPDFDoc()

// Fits the page into a 160 × 200 box and resolves with a canvas.
const canvas = await pdfDoc.loadThumbnail({
  pageIndex: 0,
  width: 160,
  height: 200,
  type: 'canvas',
})
thumbnailContainer.appendChild(canvas)

After: @sdk-pdf/react

PageCanvas renders a page lazily at the canvas's CSS size; the sidebar, its selection, and its page operations are yours. Drag-to-reorder becomes moveBefore from usePageMove:

import { useContext, useState } from 'react'
import { PageCanvas, usePageMove, usePages, useScrollToPage } from '@sdk-pdf/react'

function ThumbnailsSidebar() {
  const canEdit = useContext(CanEditContext)
  const pages = usePages()
  const scrollToPage = useScrollToPage()
  const { moveBefore } = usePageMove()
  const [draggedPageId, setDraggedPageId] = useState<string>()

  return (
    <nav aria-label="Pages">
      {pages.map((page, index) => (
        <button
          key={page.id}
          // Reordering is editing: only users who may edit can drag.
          draggable={canEdit}
          onDragStart={(event) => {
            event.dataTransfer.setData('text/plain', page.id)
            setDraggedPageId(page.id)
          }}
          onDragOver={(event) => {
            // Accept only thumbnails from this list, not files or text.
            if (draggedPageId) event.preventDefault()
          }}
          onDrop={() => {
            // Drop a page before this one, like rearranging in the thumbnail panel.
            if (draggedPageId && draggedPageId !== page.id) {
              try {
                moveBefore(draggedPageId, page.id)
              } catch {
                // One of the pages no longer exists: InvalidInputError, and nothing
                // moved.
              }
            }
            setDraggedPageId(undefined)
          }}
          onClick={() => scrollToPage(page.id)}
        >
          {/* The render resolution follows the canvas CSS size: style .thumbnail to the
             width you need. */}
          <PageCanvas
            pageId={page.id}
            className="thumbnail"
            role="img"
            aria-label={`Page ${index + 1}`}
          />
        </button>
      ))}
    </nav>
  )
}

What to know

  • Lazy and sized with CSS. An off-screen canvas does no work until it becomes visible, and the render resolution follows the displayed CSS size or an explicit scale, in place of loadThumbnail's scale, width, and height. In a virtualized list, pass isVisible.
  • Annotations included. PageCanvas draws annotations, form widgets included, unless hideAnnotations is set.
  • Thumbnails that show session edits are coming soon. PageCanvas draws the page as loaded from the file; the main view shows the edits. Rotation is followed live.
  • Stale ids. If the dragged page no longer exists, moveBefore throws InvalidInputError and changes nothing, so the drop handler catches it.
  • No merging. Dropping another PDF on the thumbnails has no equivalent.

Forms

Before: Foxit PDF SDK for Web

Since version 11.0, forms are PDFForm, PDFFormField, and Widget objects with asynchronous accessors. Fields are looked up by name, values are strings, and widgets are created in PDF coordinates on a zero-based page:

const pdfViewer = await pdfui.getPDFViewer()
const form = pdfViewer.getCurrentPDFDoc().getPDFForm()

const field = await form.getField('customer.name')
if (field) {
  const currentValue = await field.getValue()
  await field.setValue('Jane Doe')
}

// A date field: the rectangle is left, bottom, right, and top in PDF coordinates.
await form.createDatetime({
  pdfRect: { left: 72, bottom: 700, right: 192, top: 724 },
  pageIndex: 0,
  rotate: 0,
  timeformat: 'yyyy-mm-dd',
})

// Form data as a file: xfdf, fdf, csv, json, text, or xml.
const formData = await form.exportToFile(
  UIExtension.PDFViewCtrl.commons.FileFormat.json,
  {}
)

After: @sdk-pdf/react

Forms are widget-centric: you work with the widgets on the pages, and each value is shaped like the value of the matching HTML form control, as Working with forms explains. The counterpart of getField(name) is a widget lookup by fieldName, bound with useWidgetValue:

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

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

// The counterpart of getField(fieldName) for a text field, as a bound input.
function NamedTextField({ fieldName }: { fieldName: string }) {
  const { widgets } = useWidgets()
  const widget = widgets
    .filter(isTextWidget)
    .find((widget) => widget.fieldName === fieldName)

  return widget ? <BoundTextInput widget={widget} /> : null
}

function BoundTextInput({ widget }: { widget: TextWidget }) {
  // Replaces getValue and setValue: filling in the main view and undo update it.
  const [value, setValue] = useWidgetValue(widget)

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

function useAddDateField() {
  const createWidgets = useWidgetsCreate()

  // Replaces form.createDatetime.
  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: 700,
      width: 120,
      height: 24,
      fieldName: 'contract.date',
      format: { type: FormatType.DateTime, format: 'yyyy-mm-dd' },
    })
    return widget
  }
}

From fields to widgets

ConceptFoxit PDF SDK for Web@sdk-pdf/react
LookupgetField(name), getFields(), getFieldsByNamePrefix()useWidgets() (re-renders), or await getWidgets() from useWidgetsGetter (one-time, loads every page with widgets first)
Shared valuesA field with several widgetsEvery widget with the same field name shows and synchronizes the same value
Valuesfield.getValue(), setValue(), stringsuseWidgetValue(widget): string for text and choice, boolean for checkbox and radio, option indices for multiChoice
CreationcreateTextField, createCheckbox, createDatetime, and the other create* methodscreateWidgets(props) from useWidgetsCreate; required: pageId, widgetType, x, y, width, height; a unique fieldName is generated when omitted
Interactive creationDesign mode of the form designer add-on, STATE_HANDLER_CREATE_FIELD_*The { type: 'addWidget', widgetProps } interaction tool, with editable widgets in the content policy
Deletionpage.removeAnnotByObjectNumber(), which removes the field with its last widgetuseBlockDelete with widget ids; same-named widgets act as one field, and deleting a widget doesn't erase the stored field value
PropertiesPDFFormField setters and the form designer's properties dialogsetWidgetProps(ids, props, options?) from useSetWidgetProps: isReadOnly, borderColor, fillColor, fontSize, isMultiline, options, and more
Interaction eventsFormFillerService listeners and interceptorswidgetInput from useViewportEvent for values the user changes, useFocusedWidgetId for the widget being filled; no interceptors
Form dataexportToFile, importFromFile, resetFormNo equivalent

What to know

  • Coordinates differ in shape. A Foxit pdfRect gives left, bottom, right, and top in PDF coordinates; a widget here takes x, y, width, and height in user space, with the origin at the bottom-left corner of the page. Check placement on a test document before converting stored positions.
  • Values are read and written per widget. useWidgetValue binds one widget per hook call, and there is no setter by field name: to prefill a form, call setValue from a component bound to one widget of each field, like NamedTextField above. Widgets with the same field name share the value, so one per field is enough.
  • Read-only fields. isReadOnly restricts filling the field, not authoring the widget; pass { excludeFromHistory: true } to setWidgetProps so an undo can't unlock a submitted form. setWidgetProps applies the change at once and returns a promise that resolves when the widgets show it.
  • Field events. widgetInput reports only values the user changes in the main view: values set from code, undo, and redo emit nothing, and useWidgetValue re-renders on all of them.
  • Date fields and scripts. Foxit runs form JavaScript for calculation, validation, and formatting. @sdk-pdf/react recognizes Acrobat date formats without running scripts and exposes them as the text widget's format, FormatType.DateTime; see FormatType. Date fields driven by hand-written scripts aren't recognized and read as plain text fields. Calculation and validation scripts have no equivalent.
  • Signatures. signature widgets carry a read-only isCryptoSigned; signing and verification (registerSignHandler, PDFDoc.sign, SignatureWorkflowService) have no equivalent, and the SDK never signs.

Annotations

Before: Foxit PDF SDK for Web

Annotations are first-class in Foxit: the complete viewer creates them with its tools, and PDFPage reads, adds, and removes them in PDF coordinates. Annotation data travels as JSON, FDF, or XFDF:

const pdfDoc = pdfViewer.getCurrentPDFDoc()
const page = await pdfDoc.getPageByIndex(0)

const annots = await page.getAnnots()

// addAnnot may also add an associated popup, so it resolves with an array.
const [square] = await page.addAnnot({
  type: 'square',
  rect: { left: 100, bottom: 500, right: 200, top: 550 },
  color: 0xff0000,
})
await page.removeAnnotById(square.getUniqueID())

// null exports every annotation.
const annotsJson = await pdfDoc.exportAnnotsToJSON(null)

After: @sdk-pdf/react

Existing annotations display in Viewport and PageCanvas, and the border color, width, and opacity (the color's alpha channel) of selected shapes, lines, ink, and widgets can be changed with useSelectionAnnotationProps. Images are placed as stamp annotations with the addImage tool, as in Editing and interaction tools, or from code with useStampAnnotationCreate, and ink annotations are created from code with useInkAnnotationCreate:

import { useSelectionAnnotationProps } from '@sdk-pdf/react'

function AnnotationBorderControls() {
  const { isApplicable, borderColor, borderWidth } = useSelectionAnnotationProps()

  // The selection holds no annotation with a border.
  if (!isApplicable) return null

  return (
    <div role="toolbar">
      <input
        type="color"
        value={borderColor.value ?? '#000000'}
        onChange={(event) => borderColor.set(event.currentTarget.value)}
      />
      <input
        type="number"
        min={0}
        // An empty value means the selected annotations have different widths.
        value={borderWidth.value ?? ''}
        onChange={(event) => borderWidth.set(Number(event.currentTarget.value))}
      />
    </div>
  )
}

What to know

  • Ink from code only. createInkAnnotation({ pageId, x, y, width, height, inkList, borderColor, borderWidth, lineCap, blendMode }) from useInkAnnotationCreate adds an ink annotation from strokes your code collects; there is no drawing tool like STATE_HANDLER_CREATE_PENCIL. Each stroke in inkList is a flat [x1, y1, x2, y2, …] array of fractions of the box given by x, y, width, and height in user space.
  • No other annotation authoring. Creating notes, free text, text markup, shapes, links, file attachments, measurements, and redaction marks has no equivalent, and neither does addAnnot for them, setting an annotation's content, or the comment panel with replies and review states.
  • No annotation data exchange. exportAnnotsToJSON, exportAnnotsToFDF, the import counterparts, and flattening have no equivalent.
  • Per-kind permissions. AnnotationAuthorityManager and customs.getAnnotPermissions set permissions per annotation; a content policy sets an annotations level, 'hidden', 'readonly', or 'editable', for all of them, or per annotation with a function of the block id, such as annotations: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'editable').

Editing and interaction tools

Before: Foxit PDF SDK for Web

The active tool is a state handler, switched by name through the StateHandlerManager. Read-only viewing comes from the permission callback, and content editing is Std Edit or the separately licensed Adv Edit, both driven from the complete viewer's Edit tab:

const { STATE_HANDLER_NAMES } = UIExtension.PDFViewCtrl.constants
const stateHandlerManager = await pdfui.getStateHandlerManager()

stateHandlerManager.switchTo(STATE_HANDLER_NAMES.STATE_HANDLER_HAND)
stateHandlerManager.switchTo(STATE_HANDLER_NAMES.STATE_HANDLER_SELECT_ANNOTATION)
stateHandlerManager.switchTo(STATE_HANDLER_NAMES.STATE_HANDLER_CREATE_STAMP, {
  category: 'Static',
  name: 'Void',
})

// Read-only viewing: 0 means read-only, and the default -1 grants all permissions.
const readOnlyViewer = new UIExtension.PDFUI({
  viewerOptions: {
    libPath: '/foxit-lib',
    jr: { licenseSN, licenseKey },
    customs: { getDocPermissions: () => 0 },
  },
  renderTo: elementRef.current,
})

After: @sdk-pdf/react

The interaction mode combines a content policy (what may change) and an interaction tool (what a gesture does); useSetInteractionMode switches both at once, and useInteractionTool reads the tool back:

import { useContext } from 'react'
import {
  CONTENT_POLICY_PRESET,
  InteractionTool,
  stageImage,
  useInteractionTool,
  useSetInteractionMode,
} from '@sdk-pdf/react'

const textSelectTool: InteractionTool = { type: 'textSelect' }
const selectTool: InteractionTool = { type: 'select' }
const addTextTool: InteractionTool = { type: 'addText' }

function ToolGroup() {
  const canEdit = useContext(CanEditContext)
  const interactionTool = useInteractionTool()
  const setInteractionMode = useSetInteractionMode()

  async function placeImage(file: File) {
    const image = await stageImage(file)
    // The next click on a page places the image as a stamp annotation.
    // Placing images is editing, so switch the policy with the tool.
    setInteractionMode({ type: 'addImage', image }, CONTENT_POLICY_PRESET.PDF_EDITOR)
  }

  return (
    <div role="toolbar">
      {/* Like getDocPermissions returning 0, with fillable forms and links. */}
      <button
        aria-pressed={interactionTool.type === 'textSelect'}
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_VIEWER)
        }
      >
        Select text
      </button>
      {/* The editing tools switch to PDF_EDITOR, so only users who may edit get them. */}
      {canEdit && (
        <>
          <button
            aria-pressed={interactionTool.type === 'select'}
            onClick={() =>
              setInteractionMode(selectTool, CONTENT_POLICY_PRESET.PDF_EDITOR)
            }
          >
            Edit
          </button>
          <button
            aria-pressed={interactionTool.type === 'addText'}
            onClick={() =>
              setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)
            }
          >
            Add text
          </button>
          <input
            type="file"
            accept="image/*"
            aria-label="Add image"
            onChange={(event) => {
              const file = event.currentTarget.files?.[0]
              if (file) placeImage(file).catch(showError)
            }}
          />
        </>
      )}
    </div>
  )
}

Mapping the tools

STATE_HANDLER_NAMES and modes@sdk-pdf/react
STATE_HANDLER_HANDNo tool: Viewport scrolls and zooms on its own.
STATE_HANDLER_SELECT_TEXT_IMAGE{ type: 'textSelect' }: native text selection for reading and copying, with read-only text.
STATE_HANDLER_SELECT_ANNOTATION{ type: 'select' }: select, move, and resize blocks.
Std Edit and Adv Edit text editingText is editable in place when the policy's text level is 'editable' (PDF_EDITOR); there is no separate mode or add-on.
Add text in the Edit tab{ type: 'addText' }: creates a paragraph at the click position and focuses it for typing, with PDF_EDITOR.
STATE_HANDLER_CREATE_IMAGE, STATE_HANDLER_CREATE_STAMP with an image{ type: 'addImage', image, width?, height? } with await stageImage(blob) places a stamp annotation; see stageImage. The size is in user-space units: a single dimension keeps the aspect ratio, and with none the image keeps its natural size, fit to the page.
STATE_HANDLER_CREATE_FIELD_TEXT and the other field state handlers{ type: 'addWidget', widgetProps }, with PDF_EDITOR.
STATE_HANDLER_CREATE_FREETEXT_TYPEWRITER, STATE_HANDLER_CREATE_FREETEXT_BOXThe nearest tool is addText, which creates a text paragraph.
STATE_HANDLER_CREATE_PENCILNo drawing tool. useInkAnnotationCreate creates an ink annotation from strokes your code collects.
Text markup, shapes, notes, links, measurements, STATE_HANDLER_ERASER, STATE_HANDLER_SNAPSHOT_TOOLNo equivalent.
customs.getDocPermissions returning 0CONTENT_POLICY_PRESET.PDF_VIEWER (forms stay fillable) or PDF_PRINTER (forms read-only too).

What to know

  • Switch the policy with the tool. Pass PDF_EDITOR with the editing tools, as above. textSelect needs read-only text: with editable text, the combination is unsupported and reports a message through logMessages.
  • Per-kind permissions. A custom content policy is a plain object with text, annotations, links, and widgets levels, in place of User_Permissions bits. 'hidden' doesn't render that kind of content at all, and a level can be a function of the block id to set it per block, such as widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive'). There is no hook that reads the policy back: keep the policy you set in your own state to show or hide editing UI.
  • Gate the mode switcher. The content policy is the current mode, not what the user may do: render controls that switch to PDF_EDITOR only when your app's own permission allows it, as canEdit does in these examples.
  • Local mode. Switching the mode affects this client only.
  • Selection toolbars. useSelectionTextStyle (font, size, color, bold, italic, alignment, spacing) replaces the font and paragraph controls of the Edit tab, and for the form properties dialog, read the selected widgets with useSelectedBlocks and change them with setWidgetProps(ids, { isMultiline, format }) from useSetWidgetProps. Return the focus to the text with useFocusSelection after a toolbar click.
  • Keyboard shortcuts are your code. Foxit's built-in shortcuts and onShortcutKey move to your app: undo, redo, and Delete become a useHotkeys mapping, memoized so the listener doesn't re-install on every render, as in the next section. Paste and print have no equivalent, and search is useDocumentSearch behind your own search bar.
  • Programmatic text edits. useProgrammaticEditor reads paragraphs and rewrites them without UI.

Undo, redo, and change tracking

Before: Foxit PDF SDK for Web

The undo-redo add-on undoes annotation, bookmark, and Adv Edit changes, binds Ctrl+Z and Ctrl+Y, and records changes made through the wrapped document of invoke. isDocModified() reports unsaved changes:

const undoRedoAddon = await pdfui.getAddonInstance('UndoRedoAddon')

undoButton.onclick = () => undoRedoAddon.undo()
redoButton.onclick = () => undoRedoAddon.redo()
revertButton.onclick = () => undoRedoAddon.undoAll()

// Changes made through the wrapped pdfDoc enter the undo stack.
await undoRedoAddon.invoke(async (pdfDoc) => {
  const page = await pdfDoc.getPageByIndex(0)
  await page.addAnnot({
    type: 'square',
    rect: { left: 0, bottom: 500, right: 100, top: 550 },
  })
})

const hasUnsavedChanges = pdfViewer.getCurrentPDFDoc().isDocModified()

After: @sdk-pdf/react

useUndoRedo returns the commands and their availability, and useHotkeys binds the shortcuts the add-on bound for you:

import { useContext, useMemo } from 'react'
import {
  createUnfocusedHandler,
  useDeleteSelection,
  useHotkeys,
  useUndoRedo,
} from '@sdk-pdf/react'

function HistoryControls() {
  const canEdit = useContext(CanEditContext)
  // canUndo and canRedo re-render the component as the history changes.
  const { undo, redo, canUndo, canRedo } = useUndoRedo()
  const deleteSelection = useDeleteSelection()

  // Foxit's Ctrl+Z, Ctrl+Y, and Delete; Mod is Cmd on macOS. The mapping must be
  // memoized.
  useHotkeys(
    useMemo(
      () => ({
        'Mod-z': undo,
        'Mod-y': redo,
        'Shift-Mod-z': redo,
        // Deleting is editing: bind Delete only for users who may edit.
        // createUnfocusedHandler keeps Delete for text while the user types in a
        // paragraph or a field.
        ...(canEdit ? { Delete: createUnfocusedHandler(deleteSelection) } : {}),
      }),
      [undo, redo, deleteSelection, canEdit]
    )
  )

  return (
    <div role="toolbar">
      <button disabled={!canUndo} onClick={undo}>
        Undo
      </button>
      <button disabled={!canRedo} onClick={redo}>
        Redo
      </button>
    </div>
  )
}

isDocModified() becomes a comparison of document revisions: useDocumentRevision re-renders with a new revision on every edit, and undo and redo bring back the revision of that point in the history. Keep the revision you last saved and compare: a different revision means unsaved changes. A save records the revision it saved, so the unsaved-changes indicator lives with the save button in Saving and downloading.

What to know

  • Beyond annotations. The history covers the document: page operations are undo steps, widget property changes are undo steps unless excluded, and form values follow undo and redo.
  • Local and merged. Undo reverts the local user's changes, and edits within 500 ms merge into one step, even across hook calls. To keep an operation a separate step, call markUndoBoundary() from useUndoBoundary before it starts and after its edits finish.
  • No wrapper. There is no invoke: the history tracks the local user's edits without a wrapped document. Changes applied with the excludeFromHistory option of useSetWidgetProps never enter it.
  • No undo all. undoAll() has no equivalent; to return to the file as loaded, reload the original bytes: resetPdf(), then loadPdf(original.slice()).
  • Events for user actions only. In place of Foxit's data events, useViewportEvent reports what users do in the main view, such as textEditorInput, addToolApplied, blocksDragEnd, and widgetInput. Page operations, undo, and redo that your code calls emit no events: track them where you call them, and watch useDocumentRevision for any change.

Saving and downloading

Before: Foxit PDF SDK for Web

getFile() exports the document, with its annotations, form data, and page edits, as a File. Uploading and downloading are your code:

const pdfViewer = await pdfui.getPDFViewer()
const pdfDoc = pdfViewer.getCurrentPDFDoc()

const file = await pdfDoc.getFile({ fileName: 'contract.pdf' })

const body = new FormData()
body.append('file', file, file.name)
await fetch('/api/pdf/save', { method: 'POST', body })

// A download goes through an object URL.
const url = URL.createObjectURL(file)
const link = document.createElement('a')
link.href = url
link.download = file.name
link.click()
URL.revokeObjectURL(url)

After: @sdk-pdf/react

useDocumentSaving applies the recorded changes to the original document. One component owns the saved revision, so the save, the unsaved-changes indicator, and the close confirmation that replaces customs.closeDocBefore compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's:

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

function DocumentActions() {
  const canEdit = useContext(CanEditContext)
  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()
  const resetPdf = useResetPdf()

  // Replaces isDocModified(); 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()
      const bytes = await prepareDocument()

      const body = new FormData()
      body.append(
        'file',
        new Blob([bytes as BlobPart], { type: 'application/pdf' }),
        'contract.pdf'
      )
      const response = await fetch('/api/pdf/save', { method: 'POST', body })
      if (!response.ok) throw new Error(`Save failed with status ${response.status}`)

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

  function close() {
    // Replaces customs.closeDocBefore.
    if (hasUnsavedChanges && !window.confirm('Close without saving?')) return
    resetPdf()
  }

  return (
    <div role="toolbar">
      {canEdit && <button onClick={() => save().catch(showError)}>Save</button>}
      <button onClick={() => void downloadDocument()}>Download</button>
      <button onClick={close}>Close</button>
      {hasUnsavedChanges && <span>Unsaved changes</span>}
    </div>
  )
}

What to know

  • Bytes, not a File. prepareDocument() resolves with the complete PDF as a Uint8Array, and downloadDocument() saves the same bytes on the user's device; there is no file name option.
  • returnOriginalIfUnchanged defaults to true, returning the buffer passed to loadPdf when nothing changed. Wrapping it in a Blob copies it, as above; set the option to false when the bytes will be transferred or mutated, such as posted to a worker.
  • Save options. Besides returnOriginalIfUnchanged, pageIds saves only the listed pages, in the listed order. The getFile() flags (incremental, linearized, and the others), progressHandler, and the chunked getStream() have no equivalent, and neither does saving with a password through setPasswordAndPermission.
  • Record the saved revision. Saving doesn't reset any modified state: store the revision each successful save started from, as above.

Errors and logging

Before: Foxit PDF SDK for Web

A failed open rejects with an object whose error code tells failures apart (3 is a wrong password), and the viewer also emits the openFileFailed event. For a password failure, the complete viewer asks for the password itself:

const { ViewerEvents } = UIExtension.PDFViewCtrl.constants
const passwordErrorCode = UIExtension.PDFViewCtrl.PDF.constant.Error_Code.password

pdfui.addViewerEventListener(ViewerEvents.openFileFailed, (failure) => {
  // The password dialog takes over.
  if (failure && failure.error === passwordErrorCode) return
  hideLoadingOverlay()
})

try {
  await pdfui.openPDFByFile(file, { fileName: file.name })
} catch (failure) {
  // Reported once: a password failure goes to the password dialog.
  if (!failure || failure.error !== passwordErrorCode) showError(failure)
}

// Logging: true hides the SDK's log output; setLogLevel takes a Log_Levels value.
UIExtension.PDFViewCtrl.Log.LoggerFactory.toggleLogger(true)

After: @sdk-pdf/react

Failures are typed errors from the call that failed, and recoverable problems are log messages you subscribe to with logMessages. Error handling and logging has the full rules:

import { useEffect } from 'react'
import { InvalidPasswordError, logMessages, LogLevel, useLoadPdf } from '@sdk-pdf/react'

function useSdkDiagnostics() {
  // The SDK never prints to the console: subscribe, and unsubscribe on unmount.
  useEffect(() => {
    const unsubscribe = logMessages.on('message', (logMessage) => {
      if (logMessage.level >= LogLevel.Warning)
        reportToMonitoring(logMessage.message, logMessage.data)
    })
    return () => {
      unsubscribe()
    }
  }, [])
}

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

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

What to know

  • Error classes, not codes. DocumentNotLoadedError, InvalidInputError, InvalidPasswordError, DocumentFormatError (malformed beyond what the SDK can work around), NotSupportedError (a PDF feature the SDK doesn't support), and UninitializedError, all extending GenericError with a static message and an optional data object. Narrow them with instanceof.
  • Nothing half-applied. Commands validate first and throw before touching anything; readers return empty values instead of throwing; hooks never throw during render.
  • Network errors are yours. Without range loading, a failed download is an error from your own fetch, before loadPdf runs.
  • Logs are opt-in. In place of toggleLogger and setLogLevel, filter by level in your logMessages subscriber; the SDK never prints to the console. Each message carries level, reason, a static message, data, and stack. Error UI, such as the alerts of the complete viewer, is yours.

Cleanup and lifecycle

Before: Foxit PDF SDK for Web

destroy() closes the open document and removes the viewer's DOM, and returns a promise; viewer event listeners return a function that removes them. A React effect undoes both:

useEffect(() => {
  const pdfui = new UIExtension.PDFUI({
    viewerOptions: { libPath: '/foxit-lib', jr: { licenseSN, licenseKey } },
    renderTo: elementRef.current,
  })
  const removeOpenListener = pdfui.addViewerEventListener(
    UIExtension.PDFViewCtrl.constants.ViewerEvents.openFileSuccess,
    () => setIsDocumentOpen(true)
  )

  return () => {
    removeOpenListener()
    pdfui.destroy()
  }
}, [])

Search objects from getTextSearch() are released with their own destroy().

After: @sdk-pdf/react

  • 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, such as while a modal is open.
  • Log subscriptions return an unsubscribe function; call it in the effect cleanup, as in Errors and logging.
  • Closing a document is resetPdf() from useResetPdf, which returns the load state to 'initial'. There is no instance to destroy.

Foxit reads text with PDFPage.getText(), PDFDoc.getText(range), and getTextInRect, and searches with getTextSearch() and searchText(). In @sdk-pdf/react, useProgrammaticEditor reads the text as paragraphs: getParagraphsByPage(pageId) for one page, or every paragraph in page and content order:

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

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

  return async () => {
    // Loads every page in turn: prefer getParagraphsByPage(pageId) when you know the
    // page.
    const paragraphIds = await getAllParagraphIds()
    const paragraphs = await getPlainTextParagraphs(paragraphIds)
    return paragraphs.map((paragraph) => ({
      pageNumber: paragraph.pageNumber,
      text: paragraph.content,
    }))
  }
}

Each entry also carries the paragraph box in user space as bounds ({ x, y, width, height }), and the paragraph layout, such as textAlign and lineHeight.

useDocumentSearch replaces getTextSearch() and searchText(): set the query and the options, and the SDK finds the matches, highlights them on the pages, and scrolls to the active one. There is no search object to destroy:

import { useDocumentSearch } from '@sdk-pdf/react'

function SearchBar() {
  const {
    criteria,
    matches,
    activeMatchIndex,
    status,
    setSearchCriteria,
    goToNextMatch,
    goToPreviousMatch,
  } = useDocumentSearch()

  return (
    <div role="search">
      <input
        value={criteria.query}
        onChange={(event) =>
          setSearchCriteria({ ...criteria, query: event.currentTarget.value })
        }
      />
      <label>
        <input
          type="checkbox"
          checked={criteria.isWholeWord}
          onChange={(event) =>
            setSearchCriteria({ ...criteria, isWholeWord: event.currentTarget.checked })
          }
        />
        Whole words
      </label>
      <span>
        {status === 'searching'
          ? 'Searching…'
          : `${activeMatchIndex + 1} of ${matches.length}`}
      </span>
      <button disabled={matches.length === 0} onClick={goToPreviousMatch}>
        Previous
      </button>
      <button disabled={matches.length === 0} onClick={goToNextMatch}>
        Next
      </button>
    </div>
  )
}

The options are isCaseSensitive and isWholeWord. Each match carries id, pageId, paragraphId, and the matched text, in document order, with no rectangles; goToMatch(match.id) activates one from a results list. A changed query is searched once typing pauses, and status is 'searching' until the whole document is searched. Replacing text has no equivalent.

There is no character-level geometry, and no API returns the selected text or its rectangles (getSelectedTextInfo, TextSelectionTool.getSelectionInfo()); useSelectionTextStyle reads the selection's style. To jump to a paragraph you located yourself, call scrollToBlock(blockId) from useScrollToBlock.

Features without an equivalent

Plan around these before you migrate:

Foxit PDF SDK for Web featureIn @sdk-pdf/react
The complete viewer: appearances, layout templates, fragments, add-ons, localizationThe UI is yours
Range loading (openPDFByHttpRangeRequest), open options annotsJson, fdf, beforeRenderPDFDocFetch the whole file; no import
Find and replaceNo equivalent; useDocumentSearch finds the text
Reading the selected text, character geometry (getCharInfoByIndex)No equivalent; useProgrammaticEditor reads paragraph text
Creating markup, shape, note, free text, link, and measurement annotations; drawing ink; commentsNo equivalent; existing annotations display, ink is created from code with useInkAnnotationCreate, and the border color, width, and opacity of shapes, lines, ink, and widgets can be changed
Annotation and form data exchange (JSON, FDF, XFDF, CSV), resetForm, flatteningNo equivalent
Redaction, digital signatures, stamps from the stamp libraryNo equivalent; images place as stamp annotations with addImage, and the SDK never signs
Form JavaScript, XFA forms, form recognitionNo equivalent; Acrobat date formats are recognized without scripts
FormFillerService interceptorsNo equivalent; widgetInput from useViewportEvent reports value changes after the fact
Inserting pages from another PDF, merging, cropping (setPagesBox)No equivalent; prepareDocument({ pageIds }) saves a subset of pages
View-only rotation (rotateTo), view modesRotation is a document edit; one scrollable layout
Printing (print, printCurrentView), snapshotsNo print API; PageCanvas can render pages for your own print layout, drawn as loaded from the file, without this session's edits
Save flags, getStream, passwords and permissions on save (setPasswordAndPermission)prepareDocument() takes returnOriginalIfUnchanged and pageIds only
Headers and footers, watermarks, page labels, layers, bookmarks, document comparison, 3D, read aloudNo equivalent
undoAll() and the invoke wrapper of the undo-redo add-onuseUndoRedo covers the local user's edits

On this page