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/reactis in beta, and its exports and APIs may change between releases. Pin the exact package version and review the changes when you update.
Migration checklist
- Read the overview: how the two integrations differ, and the API map for every
PDFUI,PDFViewer, andPDFDoccall you use. - List the built-in UI your users rely on (ribbon tabs, the thumbnail and comment panels, context menus, dialogs, add-ons): each becomes a React component.
- Install the package and replace the
new PDFUI(...)mount withPdfProviders. - Load and close documents with
loadPdfandresetPdf, including the password flow. - Replace the appearance, layout template, and fragments with your own layout around
Viewport. - Move page navigation from zero-based indexes to page ids.
- Move page operations from page ranges to the page hooks.
- Move zoom and fit modes.
- Rebuild the thumbnail panel with
PageCanvas. - Move form fields from
PDFFormandPDFFormFieldto widgets. - Check which annotation workflows carry over.
- Map state handlers, read-only permissions, and text editing to the interaction mode.
- Replace the undo-redo add-on and
isDocModified(). - Replace
getFile()withprepareDocument. - Move error handling, cleanup, and text extraction and search.
- Plan for features without an equivalent.
- Remove
@foxitsoftware/foxit-pdf-sdk-for-web-library, the copiedlibandexternalfolders,license-key.js, and theService-Worker-Allowedheader you added for Foxit. Test encrypted, malformed, large, and form-heavy PDFs, page operations, and the save round-trip.
Overview
How the two integrations differ
Foxit PDF SDK for Web is built in three layers on a WebAssembly engine:
- Core:
PDFDocandPDFPageread 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:
PDFViewerrenders the document into a DOM element you pass toinit(). It owns the scrolling, zoom, view modes, and the active tool (a state handler), and it reports what happens through viewer events.getPDFDocRender()andgetPDFPageRender(index)reach the rendered document and pages. - UIExtension:
PDFUIwraps aPDFViewerand mounts a complete viewer intorenderTo: a ribbon or mobile appearance, a layout template you adjust with fragments, and features delivered as add-ons fromlib/uix-addons(thumbnails, undo and redo, print, search, form design, and more), often loaded together asallInOne.js. - You serve the package's
libfolder (engine modules, workers, add-ons, locales, stamps) as static files and pointlibPathat it, optionally with fonts fromexternal. AlicenseSNandlicenseKeygo into thejroptions, and the license decides which feature modules load; advanced editing is licensed separately. - Edits change the
PDFDocin the engine, andgetFile()exports it as a PDFFile. 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.
Viewportis the main view,PageCanvasrenders single pages, and every toolbar, panel, and dialog is your React code driven by hooks. PdfProvidershosts the document state and the configuration inside your React tree; the SDK renders unstyled, and your design system supplies the look through thethemeconfiguration.- Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers the history;
useDocumentSavingbuilds 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.
| Concern | Foxit PDF SDK for Web | @sdk-pdf/react |
|---|---|---|
| UI | PDFUI with appearances, layout templates, fragments, and add-ons | Viewport and PageCanvas only; you build toolbars, panels, and dialogs |
| Mounting | new PDFUI({ renderTo }) or new PDFViewer() plus init() in an effect | PdfProviders and components in your React tree |
| Deployment | The lib folder under libPath, fonts, license key, Service-Worker-Allowed | The package from a private registry |
| State access | PDFViewer, PDFDocRender, and PDFDoc objects, viewer events | State hooks that re-render, command hooks, and event hooks |
| Pages | Zero-based indexes and two-dimensional page ranges | Session-scoped page ids |
| Tools | State handlers switched with StateHandlerManager.switchTo() | An interaction tool paired with a content policy |
| Forms | PDFForm, PDFFormField, and Widget objects | Widgets with a fieldName, values bound with useWidgetValue |
| Editing text | Std Edit, or the separately licensed Adv Edit | Built in under the PDF_EDITOR content policy |
| Undo | The undo-redo add-on, for annotation, bookmark, and Adv Edit changes | useUndoRedo for the whole document |
| Persistence | getFile() for the PDF; JSON, FDF, or XFDF for annotations and form data | prepareDocument() for the PDF |
What this means for your code:
- 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.
- Drop the mount effect. The
PDFUIorPDFViewerinstance, thegetPDFViewer()promise, and any context that shares them are replaced byPdfProvidersand hooks. - Replace objects with hooks.
PDFDocRender,PDFDoc,PDFForm,StateHandlerManager, and add-on calls become hook calls in the components that need them. - 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
usePagesandusePageIndexGetter. - Rethink annotations.
@sdk-pdf/reactedits 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. - Map permissions such as
customs.getDocPermissionsto content policies, and decide whether to expose editing of existing text, which is built in. - Remove deployment pieces: the copied
libfolder, 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/react | Notes |
|---|---|---|
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 useLoadPdf | Read a File or Blob into bytes first. |
openPDFByHttpRangeRequest({ range: { url, headers } }) | No equivalent | fetch the whole file, with your headers, then loadPdf. |
Open options annotsJson, fdf, beforeRenderPDFDoc | No equivalent | |
ViewerEvents.openFileSuccess | loadState.status === 'ready' | |
pdfui.close(), pdfViewer.close() | resetPdf() from useResetPdf | |
customs.closeDocBefore, customs.closeDocAfter | Your own code around resetPdf() | See Saving and downloading. |
pdfDoc.getPageCount() | usePageCount | |
docRender.getCurrentPageIndex(), ViewerEvents.pageNumberChange | useCurrentPageId | Re-renders with the id of the page in the middle of the viewport. |
docRender.goToPage(index) | scrollToPage(pageId) from useScrollToPage | No offset within the page. |
pdfDoc.insertPage(index, width, height), insertBlankPages(range, width, height) | createPage(width, height, insertAfterPageId?) from usePageCreate | Inserts 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 useSetPageRotation | Degrees, not the Rotation enum. |
extractPages() then insertPages() to copy a page | duplicatePage(sourcePageId, insertAfterPageId?) from usePageDuplicate | |
extractPages() to a new PDF | prepareDocument({ pageIds }) from useDocumentSaving | Saves only the listed pages, in the listed order. |
insertPages(), mergePDFDoc(), setPagesBox() | No equivalent | |
pdfViewer.rotateTo(degree) (view only) | No equivalent | useSetPageRotation edits the document. |
pdfViewer.zoomTo(scale), docRender.getScale() | useViewportScale | |
pdfViewer.zoomTo('fitWidth'), zoomTo('fitHeight') | useViewportAutoFitScale, useViewportFitScale | A one-time fit, not a fixed mode. |
ViewerEvents.zoomToSuccess | scale from useViewportScale in a useEffect | Updates once a zoom gesture or animation ends. |
defaultViewMode, view mode add-ons | No equivalent | One scrollable layout. |
Thumbnail add-on, pdfDoc.loadThumbnail() | PageCanvas | |
pdfDoc.getPDFForm(), await form.getField(name) | useWidgets filtered by fieldName | Widgets, not fields. |
await field.getValue(), field.setValue(value) | useWidgetValue | Inside a component. |
FormFillerService.onDataChanged() | widgetInput from useViewportEvent | Reports values the user changes in the main view. |
The other FormFillerService listeners and interceptors | No equivalent | |
form.createTextField({ pdfRect, pageIndex }) and the other create* methods | createWidgets(props) from useWidgetsCreate | |
Form designer add-on, STATE_HANDLER_CREATE_FIELD_* state handlers | The { type: 'addWidget', widgetProps } interaction tool | |
page.removeAnnotByObjectNumber() on a widget | useBlockDelete with widget ids | |
form.exportToFile(), importFromFile(), resetForm() | No equivalent | |
page.addAnnot(json) for ink | useInkAnnotationCreate | See Annotations. |
page.addAnnot(json) for other types, annotation state handlers | No equivalent | See Annotations. |
pdfDoc.exportAnnotsToJSON(), exportAnnotsToFDF(), importAnnotsFromJSON() | No equivalent | |
getStateHandlerManager().switchTo(name) | useSetInteractionMode | See Editing and interaction tools. |
customs.getDocPermissions: () => 0 | PDF_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, useDocumentRevisionGetter | Compares against the revision you saved. |
pdfDoc.getFile({ fileName }) | prepareDocument() from useDocumentSaving | Resolves with a Uint8Array, not a File. |
| Download button of the built-in UI | downloadDocument() | No file name option. |
getFile() option flags, getStream() | No equivalent | |
Rejected openPDFByFile, ViewerEvents.openFileFailed | The rejected loadPdf promise | See Errors and logging. |
Log.LoggerFactory.toggleLogger(), setLogLevel() | logMessages | The SDK never prints to the console. |
pdfui.destroy() | Nothing to call | See Cleanup and lifecycle. |
pdfPage.getText(), pdfDoc.getText(range) | getParagraphsByPage(pageId) from useProgrammaticEditor | Paragraph text and bounds. |
pdfDoc.getTextSearch(), searchText() | useDocumentSearch | Highlights the matches and scrolls to the active one. |
pdfui.getSelectedTextInfo(), pdfPage.getCharInfoByIndex() | No equivalent | useSelectionTextStyle reads the selection's style. |
pdfDoc.applyRedaction(), pdfDoc.sign(), pdfui.registerSignHandler(), the stamp state handler | No 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 content | No equivalent | |
pdfViewer.zoomTo(scale, position) | setScale(scale, pivot) from useViewportScale | The 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/reactSet up the project as Getting started describes: import the stylesheet, render the SDK on the client only, alias canvas in the bundler, and turn off React Strict Mode.
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
libfolder to copy, nolibPath,jrengine paths, orfontPath, nolicenseSNorlicenseKey, and no add-on list. - No mount effect. The SDK renders as components, so the element ref, the effect that creates
PDFUIorPDFViewer, the Strict Mode guard, and thedestroy()call go away. Any component underPdfProvidersreaches the document through hooks instead ofgetPDFViewer(). - Choose the starting permissions.
customs.getDocPermissionsdefaults to-1, all permissions, andPdfProvidersdefaults to theselecttool andPDF_EDITOR, where text, annotations, links, and widgets are all editable. For users who may not edit, where your app returned0, start withPDF_VIEWER(forms stay fillable) orPDF_PRINTER(forms read-only too) and thetextSelecttool, as above, and leave the editing controls out: the content policy is a mode your UI can switch withuseSetInteractionMode, 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.
loadPdftakes aUint8Array.openPDFByHttpRangeRequestand 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
loadPdfcall 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, andbeforeRenderPDFDochave no equivalent, and neither does importing annotation or form data later. - No close hooks.
customs.closeDocBeforeandcloseDocAfterhave 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 callresetPdf()andloadPdf. - 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, andkeepStatehave no counterpart. Selection, text editing, form filling, and links work insideViewportwithin 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.classNamesandtheme.stylestarget the SDK's elements (scrollViewport,page,pageCanvas,resizable,widgetOverlay, and more), and CSS variables such as--selection-colorand--widget-overlay-backgroundtune the selection and widget visuals; Theming and customization lists them.UIExtension.dark-variable.cssand the theme colors of the complete viewer have no counterpart: dark mode is your stylesheet. - Dialogs are yours. The pieces a custom
viewerUI(extendingXViewerUIorTinyViewerUI) 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.
ViewportthrowsDocumentNotLoadedErrorwhen no document is loaded. - One layout.
defaultViewMode('continuous-view-mode'or'single-page-view-mode'), the facing and horizontal modes of theh-continuousadd-on, customViewmodes, andcustoms.ScrollWraphave no equivalent:Viewportshows 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
usePagesonce the document is ready. - Scroll options.
scrollToPagealigns the page withvertical('top'by default) andhorizontal('nearest'by default), andbehaviorsets the animation. TheoffsetandisPDFPointarguments ofgoToPage, which jump to a point on the page, have no equivalent;scrollToBlock(blockId, options?)fromuseScrollToBlockscrolls to a block, such as a form widget. - The page count. Where you read
getPageCount()once afteropenFileSuccess,usePageCountre-renders when pages are added or removed, and returns0while 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
| Operation | Foxit PDF SDK for Web | @sdk-pdf/react |
|---|---|---|
| Insert | At a zero-based index (insertPage) or at page ranges, in points | After insertAfterPageId, or appended; width and height in user-space units (1/72 of an inch), no defaults |
| Move | To a destination index (movePageTo, movePagesTo) | moveBefore(ids, beforePageId?) (start when omitted) or moveAfter(ids, afterPageId?) (end when omitted) |
| Rotate | Sets a Rotation enum value, 0 to 3 | Sets an absolute angle in degrees; -90 and 450 are normalized; angles off a quarter turn throw InvalidInputError |
| Copy | extractPages and insertPages | duplicatePage is asynchronous and inserts after the source by default |
| Delete | A page index (removePage) or page ranges | One 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.
usePagesre-renders with the new page list, anduseDocumentRevisionchanges on any edit. - Extracting pages.
prepareDocument({ pageIds })fromuseDocumentSavingsaves only the listed pages, in the listed order, in place ofextractPagesto a new PDF. - Pages from other files, page boxes.
insertPagesfrom another PDF,mergePDFDoc,setPagesBoxcropping 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.
useViewportAutoFitScaleapplies when a document loads and doesn't re-apply on viewport resizes or manual zooming;useViewportFitScalereflects the viewport size at the moment of the call. There is no fit mode that stays applied as the viewport resizes, and no'fitWidth'value to read back. - What the fits measure.
fitWidthScalefits the widest page into the viewport width andfitHeightScalethe tallest page into its height, both accounting forviewportPadding. - Scale 1 is 100%: one user-space unit per CSS pixel.
setScaleandanimateScale, the third item ofuseViewportScale(), take a positive finite number and throwInvalidInputErrorotherwise. - Zoom around a point. In place of the
positionargument ofzoomTo,setScale(scale, pivot?)keeps the document still atpivot, a point{ x, y }given as fractions of the viewport area (0the top or left edge,1the bottom or right edge), the center by default.animateScale(scale, pivot?)animates the change and resolves when it ends. - The scale updates after a gesture. During a pinch or wheel zoom,
scalekeeps its value until the gesture ends;zoomGestureStartandzoomGestureEndfromuseViewportEventmark the gesture. - Gestures and limits. Pinch and wheel zoom are built in and tuned with
zoomGesturesonPdfProviders: itsminScaleandmaxScale(0.5and4by default) bound the gestures. - No redraw call. Foxit integrations call
redraw()after the window resizes;Viewportfills 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 ofloadThumbnail'sscale,width, andheight. In a virtualized list, passisVisible. - Annotations included.
PageCanvasdraws annotations, form widgets included, unlesshideAnnotationsis set. - Thumbnails that show session edits are coming soon.
PageCanvasdraws 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,
moveBeforethrowsInvalidInputErrorand 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
| Concept | Foxit PDF SDK for Web | @sdk-pdf/react |
|---|---|---|
| Lookup | getField(name), getFields(), getFieldsByNamePrefix() | useWidgets() (re-renders), or await getWidgets() from useWidgetsGetter (one-time, loads every page with widgets first) |
| Shared values | A field with several widgets | Every widget with the same field name shows and synchronizes the same value |
| Values | field.getValue(), setValue(), strings | useWidgetValue(widget): string for text and choice, boolean for checkbox and radio, option indices for multiChoice |
| Creation | createTextField, createCheckbox, createDatetime, and the other create* methods | createWidgets(props) from useWidgetsCreate; required: pageId, widgetType, x, y, width, height; a unique fieldName is generated when omitted |
| Interactive creation | Design mode of the form designer add-on, STATE_HANDLER_CREATE_FIELD_* | The { type: 'addWidget', widgetProps } interaction tool, with editable widgets in the content policy |
| Deletion | page.removeAnnotByObjectNumber(), which removes the field with its last widget | useBlockDelete with widget ids; same-named widgets act as one field, and deleting a widget doesn't erase the stored field value |
| Properties | PDFFormField setters and the form designer's properties dialog | setWidgetProps(ids, props, options?) from useSetWidgetProps: isReadOnly, borderColor, fillColor, fontSize, isMultiline, options, and more |
| Interaction events | FormFillerService listeners and interceptors | widgetInput from useViewportEvent for values the user changes, useFocusedWidgetId for the widget being filled; no interceptors |
| Form data | exportToFile, importFromFile, resetForm | No equivalent |
What to know
- Coordinates differ in shape. A Foxit
pdfRectgivesleft,bottom,right, andtopin PDF coordinates; a widget here takesx,y,width, andheightin 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.
useWidgetValuebinds one widget per hook call, and there is no setter by field name: to prefill a form, callsetValuefrom a component bound to one widget of each field, likeNamedTextFieldabove. Widgets with the same field name share the value, so one per field is enough. - Read-only fields.
isReadOnlyrestricts filling the field, not authoring the widget; pass{ excludeFromHistory: true }tosetWidgetPropsso an undo can't unlock a submitted form.setWidgetPropsapplies the change at once and returns a promise that resolves when the widgets show it. - Field events.
widgetInputreports only values the user changes in the main view: values set from code, undo, and redo emit nothing, anduseWidgetValuere-renders on all of them. - Date fields and scripts. Foxit runs form JavaScript for calculation, validation, and formatting.
@sdk-pdf/reactrecognizes Acrobat date formats without running scripts and exposes them as the text widget'sformat,FormatType.DateTime; seeFormatType. Date fields driven by hand-written scripts aren't recognized and read as plain text fields. Calculation and validation scripts have no equivalent. - Signatures.
signaturewidgets carry a read-onlyisCryptoSigned; 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 })fromuseInkAnnotationCreateadds an ink annotation from strokes your code collects; there is no drawing tool likeSTATE_HANDLER_CREATE_PENCIL. Each stroke ininkListis a flat[x1, y1, x2, y2, …]array of fractions of the box given byx,y,width, andheightin 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
addAnnotfor 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.
AnnotationAuthorityManagerandcustoms.getAnnotPermissionsset permissions per annotation; a content policy sets anannotationslevel,'hidden','readonly', or'editable', for all of them, or per annotation with a function of the block id, such asannotations: (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_HAND | No 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 editing | Text 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_BOX | The nearest tool is addText, which creates a text paragraph. |
STATE_HANDLER_CREATE_PENCIL | No drawing tool. useInkAnnotationCreate creates an ink annotation from strokes your code collects. |
Text markup, shapes, notes, links, measurements, STATE_HANDLER_ERASER, STATE_HANDLER_SNAPSHOT_TOOL | No equivalent. |
customs.getDocPermissions returning 0 | CONTENT_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_EDITORwith the editing tools, as above.textSelectneeds read-only text: with editable text, the combination is unsupported and reports a message throughlogMessages. - Per-kind permissions. A custom content policy is a plain object with
text,annotations,links, andwidgetslevels, in place ofUser_Permissionsbits.'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 aswidgets: (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_EDITORonly when your app's own permission allows it, ascanEditdoes 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 withuseSelectedBlocksand change them withsetWidgetProps(ids, { isMultiline, format })fromuseSetWidgetProps. Return the focus to the text withuseFocusSelectionafter a toolbar click. - Keyboard shortcuts are your code. Foxit's built-in shortcuts and
onShortcutKeymove to your app: undo, redo, and Delete become auseHotkeysmapping, memoized so the listener doesn't re-install on every render, as in the next section. Paste and print have no equivalent, and search isuseDocumentSearchbehind your own search bar. - Programmatic text edits.
useProgrammaticEditorreads 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()fromuseUndoBoundarybefore 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 theexcludeFromHistoryoption ofuseSetWidgetPropsnever enter it. - No undo all.
undoAll()has no equivalent; to return to the file as loaded, reload the original bytes:resetPdf(), thenloadPdf(original.slice()). - Events for user actions only. In place of Foxit's data events,
useViewportEventreports what users do in the main view, such astextEditorInput,addToolApplied,blocksDragEnd, andwidgetInput. Page operations, undo, and redo that your code calls emit no events: track them where you call them, and watchuseDocumentRevisionfor 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 aUint8Array, anddownloadDocument()saves the same bytes on the user's device; there is no file name option. - returnOriginalIfUnchanged defaults to
true, returning the buffer passed toloadPdfwhen nothing changed. Wrapping it in aBlobcopies it, as above; set the option tofalsewhen the bytes will be transferred or mutated, such as posted to a worker. - Save options. Besides
returnOriginalIfUnchanged,pageIdssaves only the listed pages, in the listed order. ThegetFile()flags(incremental, linearized, and the others),progressHandler, and the chunkedgetStream()have no equivalent, and neither does saving with a password throughsetPasswordAndPermission. - 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), andUninitializedError, all extendingGenericErrorwith a staticmessageand an optionaldataobject. Narrow them withinstanceof. - 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, beforeloadPdfruns. - Logs are opt-in. In place of
toggleLoggerandsetLogLevel, filter bylevelin yourlogMessagessubscriber; the SDK never prints to the console. Each message carrieslevel,reason, a staticmessage,data, andstack. 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.
useViewportEventsubscribes for the component's lifetime, with callbacks that don't need memoizing.useHotkeysinstalls window-level shortcuts, and passing a falsy value instead of the mapping disables them, 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()fromuseResetPdf, which returns the load state to'initial'. There is no instance to destroy.
Text extraction and search
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 feature | In @sdk-pdf/react |
|---|---|
| The complete viewer: appearances, layout templates, fragments, add-ons, localization | The UI is yours |
Range loading (openPDFByHttpRangeRequest), open options annotsJson, fdf, beforeRenderPDFDoc | Fetch the whole file; no import |
| Find and replace | No 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; comments | No 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, flattening | No equivalent |
| Redaction, digital signatures, stamps from the stamp library | No equivalent; images place as stamp annotations with addImage, and the SDK never signs |
| Form JavaScript, XFA forms, form recognition | No equivalent; Acrobat date formats are recognized without scripts |
FormFillerService interceptors | No 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 modes | Rotation is a document edit; one scrollable layout |
Printing (print, printCurrentView), snapshots | No 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 aloud | No equivalent |
undoAll() and the invoke wrapper of the undo-redo add-on | useUndoRedo covers the local user's edits |