Migrating from Syncfusion PDF Viewer
This guide moves a React application built on the Syncfusion React PDF Viewer (@syncfusion/ej2-react-pdfviewer) to @sdk-pdf/react. It covers both Syncfusion deployment modes: standalone, which renders in the browser with PDFium assets from resourceUrl, and server-backed, which sends the work to a web service at serviceUrl. PdfViewerComponent is a complete viewer with toolbars, panels, and dialogs, extended by the feature modules you inject; @sdk-pdf/react ships the document engine and the page view and leaves the rest of the UI to you. Most of the migration is rebuilding the built-in UI you rely on as React components, then moving your calls through the viewer ref to hooks. 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 viewer property, method, module call, and event you use.
- List the built-in UI your users rely on (primary toolbar items, the annotation and form designer toolbars, the thumbnails and bookmarks panels, the Organize Pages dialog, the context menu, and the dialogs): each becomes a React component.
- Install the package and replace
PdfViewerComponentand its modules withPdfProviders. - Load and close documents with
loadPdfandresetPdf, including the password flow. - Lay out
Viewportwith your own toolbar and sidebars. - Move page navigation from numbers to page ids.
- Move zoom percentages and fit modes.
- Rebuild the thumbnails panel with
PageCanvasand the Organize Pages dialog with the page hooks. - Move form filling to widgets and form designing to the widget hooks.
- Map interaction modes and read-only switches, and plan for the annotation tools.
- Replace
undo(),redo(), andisDocumentEdited. - Replace
saveAsBlob()anddownload(), and check collaboration. - Move error handling and logging and cleanup.
- Move text extraction and plan for features without an equivalent.
- Remove
@syncfusion/ej2-react-pdfviewer, the theme package and its CSS import, theregisterLicensecall, and the PDFium assets or the PDF Viewer web service. Test encrypted, malformed, large, and form-heavy PDFs, page operations, and the save round-trip.
Overview
How the two integrations differ
The Syncfusion PDF Viewer is a packaged viewer component:
PdfViewerComponentrenders the whole viewer: the primary toolbar, the navigation panel with thumbnails and bookmarks, the annotation and form designer toolbars, the Organize Pages dialog, the context menu, and dialogs such as the password prompt and error messages.- Features are modules. Each one is registered with
<Inject services={[...]} />and switched with anenable*prop (enableToolbar,enableAnnotation,enableFormDesigner,enablePageOrganizer, and more), most of them on by default. A module that isn't injected isn't available: withoutThumbnailView,thumbnailViewisundefined. - You reach the API through a
refto the component: instance members (load,saveAsBlob,download,undo,pageCount) and module objects (navigation,magnification,annotation,formDesigner,thumbnailView,pageOrganizer). Changes arrive as component events such asdocumentLoad,pageChange, andzoomChange. - There are two deployment modes. Standalone mode renders in the browser with PDFium (
pdfium.jsandpdfium.wasm) loaded fromresourceUrl, from the CDN or from your public folder. Server-backed mode renders and processes on a .NET web service atserviceUrl; since the 2026 Vol 2 release it gets no new features. - The app registers a license key with
registerLicensefrom@syncfusion/ej2-baseand imports its styles from a theme package, such as@syncfusion/ej2-tailwind3-theme. - Pages are 1-based numbers, zoom is a percentage (10 to 400 by default), and annotations and form fields are objects in collections (
annotationCollection,formFieldCollections) that you edit through module methods and can export as JSON, XFDF, or FDF.
@sdk-pdf/react is a library: the SDK owns the document, and you own the UI.
- There are no prebuilt toolbars, panels, or dialogs.
Viewportis the main view andPageCanvasrenders single pages; everything around them is your React code driven by hooks. PdfProvidershosts the state and configuration inside your React tree. There are no modules to inject and no instance to hold: components under it reach the document through hooks.- Rendering and editing run in the browser from the installed package, with no asset folder to host and no server mode. The SDK renders unstyled, and your design system supplies the look through the
themeconfiguration. - 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, the zoom is a scale factor, and positions are user-space units with the origin at the bottom-left corner of the page.
| Concern | Syncfusion PDF Viewer | @sdk-pdf/react |
|---|---|---|
| UI | Prebuilt toolbars, panels, and dialogs, configured with props (toolbarSettings, enable*) | Viewport and PageCanvas only; you build toolbars, panels, and dialogs |
| Features | Modules registered with Inject and switched with enable* props | Hooks, all available under PdfProviders |
| Rendering | Standalone: PDFium in the browser from resourceUrl; server-backed: a web service at serviceUrl | In the browser, from the package |
| Deployment | License key, theme package CSS, PDFium assets or a web service | The package from a private registry |
| State access | Instance members and module methods through a ref, component events | State hooks that re-render, command hooks, and event hooks |
| Pages | 1-based numbers | Session-scoped page ids |
| Zoom | Percentages, 10 to 400 by default | A scale factor, where 1 is 100% |
| Forms | Fields in formFieldCollections, updated with updateFormFieldsValue | Widgets with a fieldName, values bound with useWidgetValue |
| Annotations | Markup, shape, measurement, free text, ink, stamp, and sticky note tools | Existing annotations display; image stamps, ink from code, and border restyling |
| Text | Free text annotations on top of the page | Page text editable in place under the PDF_EDITOR content policy |
| Persistence | saveAsBlob() and download(); JSON, XFDF, and FDF for annotations and form data | prepareDocument() and downloadDocument() for the PDF |
What this means for your code:
- Build the UI. Every toolbar item, panel, and dialog your users rely on becomes a React component built from the hooks in the API reference.
- Drop the ref. Calls through
viewerRef.currentbecome hook calls in the components that need them, and event props become state hooks or event hooks. - Drop the modules and the deployment pieces:
Inject, theenable*props, the PDFium assets or the web service, the license registration, and the theme CSS. - Replace numbers and percentages with page ids and scale factors; convert at the edges, such as deep links and stored preferences.
- Rethink forms as widgets. Field objects become widgets, and the form designer becomes widget hooks.
- Plan for the annotation tools. Markup, shape, measurement, and sticky note tools have no equivalent; image stamps become image placement, and ink can be created from code.
- Replace exported annotation and form data with storing the PDF produced by
prepareDocument().
API map
Rows marked No equivalent are covered in Features without an equivalent.
| Syncfusion PDF Viewer | @sdk-pdf/react | Notes |
|---|---|---|
<PdfViewerComponent> with <Inject services={[...]} /> | <PdfProviders> and Viewport | No modules to register. |
resourceUrl, resourcesLoaded, registerLicense | Nothing to configure | No asset URL or license key. |
serviceUrl, serverActionSettings, ajaxRequestSettings, retryCount | No equivalent | Everything runs in the browser. |
documentPath, load(document, password) | loadPdf(Uint8Array, { password }) from useLoadPdf | Fetch the URL or decode the Base64 into bytes first. |
| Built-in password popup | Catch InvalidPasswordError and ask yourself | |
documentLoad event | loadState.status === 'ready' | |
documentLoadFailed event | The rejected loadPdf promise | See Errors and logging. |
unload(), documentUnload event | resetPdf() from useResetPdf | |
pageCount | usePageCount | |
currentPageNumber, pageChange event | useCurrentPageId | Re-renders with the id of the page in the middle of the viewport. |
navigation.goToPage(pageNumber) and the first, previous, next, and last page methods | scrollToPage(pageId) from useScrollToPage | Resolve numbers through usePages. |
getPageInfo(pageIndex) | The width, height, and rotation of the usePages() entries | Same unit as points; rotation applied. |
magnification.zoomTo(percent), zoomIn(), zoomOut(), zoomValue, zoomPercentage | useViewportScale | A factor, not a percentage. |
magnification.fitToWidth(), fitToPage(), zoomMode | useViewportAutoFitScale, useViewportFitScale | One-time fits. |
minZoom, maxZoom | zoomGestures on PdfProviders | Bounds the pinch and wheel zoom. |
zoomChange event | scale from useViewportScale in a useEffect | zoomGestureEnd from useViewportEvent reports pinch and wheel zoom. |
ThumbnailView, thumbnailView.openThumbnailPane(), thumbnailClick event | PageCanvas in your own sidebar | |
Organize Pages dialog, pageOrganizerSettings, pageOrganizer.openPageOrganizer() | The page hooks in your own panel | See Page organizer. |
extractPages(pages) | prepareDocument({ pageIds }) | Saves only the listed pages, in the listed order. |
| Importing pages from another PDF | No equivalent | |
formFieldCollections, retrieveFormFields() | useWidgets, useWidgetsGetter | Widgets, not fields. |
updateFormFieldsValue(field), clearFormFields() | useWidgetValue | One widget per hook call. |
resetFormFields() | No equivalent | |
focusFormField(name) | scrollToBlock(widgetId) from useScrollToBlock | Scrolls to the widget. |
formFieldAdd, formFieldRemove, formFieldPropertiesChange, and other field events | useWidgets re-renders on changes; widgetInput from useViewportEvent reports a value the user changed | useDocumentRevision changes on any edit. |
importFormFields(), exportFormFields(), exportFormFieldsAsObject() | No equivalent | prepareDocument() produces the filled PDF. |
isRequired, enableFormFieldsValidation, validateFormFields event | No equivalent | Validate in your own submit flow. |
formDesigner.addFormField(type, settings) | createWidgets(props) from useWidgetsCreate | |
formDesigner.setFormFieldMode(type) | The { type: 'addWidget', widgetProps } interaction tool | Pass PDF_EDITOR with the tool. |
formDesigner.updateFormField(field, settings) | setWidgetProps(ids, props) from useSetWidgetProps | |
formDesigner.deleteFormField(field) | deleteBlocks(ids) from useBlockDelete | |
interactionMode, annotation.setAnnotationMode(type) | useSetInteractionMode | See Editing and interaction tools. |
enableAnnotation, enableFormDesigner, enableFormFields, designerMode | A content policy from CONTENT_POLICY_PRESET, or a custom one | |
annotation.addAnnotation(), editAnnotation(), annotationCollection | No equivalent | Ink: useInkAnnotationCreate; image stamps: useStampAnnotationCreate; selected shapes, lines, and ink: border color and width through useSelectionAnnotationProps. See Annotations. |
exportAnnotation(), exportAnnotationsAsObject(), importAnnotation() | No equivalent | No JSON or XFDF. |
annotationAdd, annotationRemove, annotationPropertiesChange events | addToolApplied, blocksDragEnd, and blockResizeDragEnd from useViewportEvent; useDocumentRevision for any change | Only for what the user does in Viewport. |
toolbarSettings, enableToolbar, toolbar.showToolbarItem(), toolbarClick event | Your own components | See Display the document. |
| Theme packages, dark themes | The theme configuration, see Theming and customization | |
undo(), redo() | useUndoRedo | With canUndo and canRedo. |
isDocumentEdited | useDocumentRevision, useDocumentRevisionGetter | Compare with the revision you last saved. |
saveAsBlob() | prepareDocument() from useDocumentSaving | Resolves with a Uint8Array, not a Blob. |
download(), downloadFileName | downloadDocument() | No file name option. |
downloadStart, downloadEnd events, annotationSettings.skipDownload | No equivalent | |
showNotificationDialog, showNotificationPopup() | Your own error UI | The SDK shows no dialogs. |
destroy(), enableLocalStorage | No dispose call; unsubscribe log listeners yourself | See Cleanup and lifecycle. |
extractText(pageIndex, options) | getParagraphsByPage(pageId) from useProgrammaticEditor | Paragraph text and bounds. |
textSearch.searchText(), searchNext(), searchPrevious() | useDocumentSearch | Highlights the matches and scrolls to the active one. |
Print module, BookmarkView module | No equivalent | |
convertClientPointToPagePoint(), convertPagePointToClientPoint() | No equivalent |
Install and set up
Before: Syncfusion PDF Viewer
The Syncfusion PDF Viewer installs as @syncfusion/ej2-react-pdfviewer, takes its styles from a theme package, and, in standalone mode, loads PDFium from a CDN or from assets you copy into your app:
npm install @syncfusion/ej2-react-pdfviewer @syncfusion/ej2-tailwind3-theme
# Only to host PDFium yourself instead of using the CDN.
cp -R ./node_modules/@syncfusion/ej2-pdfviewer/dist/ej2-pdfviewer-lib \
public/assets/ej2-pdfviewer-lib@import '../node_modules/@syncfusion/ej2-tailwind3-theme/styles/pdfviewer/index.css';The license key is registered once, and the component lists the modules it uses:
import { registerLicense } from '@syncfusion/ej2-base'
import {
Annotation,
FormDesigner,
FormFields,
Inject,
Magnification,
Navigation,
PageOrganizer,
PdfViewerComponent,
TextSelection,
ThumbnailView,
Toolbar,
} from '@syncfusion/ej2-react-pdfviewer'
registerLicense('YOUR_LICENSE_KEY')
// The PDFium assets must match the installed version.
export const RESOURCE_URL =
'https://cdn.syncfusion.com/ej2/35.1.37/dist/ej2-pdfviewer-lib'
export function App() {
return (
<PdfViewerComponent
id="container"
documentPath="https://example.com/files/contract.pdf"
// Standalone mode; server-backed integrations set serviceUrl instead.
resourceUrl={RESOURCE_URL}
style={{ height: '100vh' }}
>
<Inject
services={[
Toolbar,
Magnification,
Navigation,
ThumbnailView,
TextSelection,
Annotation,
FormFields,
FormDesigner,
PageOrganizer,
]}
/>
</PdfViewerComponent>
)
}The other Before snippets in this guide reuse RESOURCE_URL.
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 PdfViewerComponent was. Its props must keep their identity between renders, so define tools outside the component and memoize the configuration:
import { useMemo } from 'react'
import { CONTENT_POLICY_PRESET, InteractionTool, PdfProviders } from '@sdk-pdf/react'
import '@sdk-pdf/react/style.css'
// Like interactionMode="TextSelection": native text selection for reading and copying.
const textSelectTool: InteractionTool = { type: 'textSelect' }
// Select, move, and resize blocks.
const selectTool: 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 },
// Both apply when the viewport first mounts; switch later with
// useSetInteractionMode.
initialInteractionTool: canEdit ? selectTool : textSelectTool,
initialContentPolicy: canEdit
? CONTENT_POLICY_PRESET.PDF_EDITOR
: CONTENT_POLICY_PRESET.PDF_VIEWER,
}),
[canEdit]
)
return (
<PdfProviders {...config}>
<DocumentLoader url="/files/contract.pdf" />
</PdfProviders>
)
}What to know
- No assets, no service, no license key in code. The install is the registry scope and the package.
resourceUrl, the copiedej2-pdfviewer-libfolder,serviceUrland its web service, andregisterLicensehave no counterpart. A server-backed integration keeps only the endpoints that serve and store the PDF bytes. - No modules.
Injectand theenable*props go away. Every hook works underPdfProviders, and a feature your users shouldn't see is a control you don't render; the content policy decides what users may change insideViewport. - No ref. The
reftoPdfViewerComponent, or theej2_instanceslookup, goes away: any component underPdfProvidersreaches the document through hooks. - Both default to editing. The Syncfusion viewer enables annotations, the form designer, form fields, and the page organizer by default, and
PdfProvidersdefaults to theselecttool andCONTENT_POLICY_PRESET.PDF_EDITOR, where text, annotations, links, and widgets are all editable. Where your app turned those modules off for read-only users, start withPDF_VIEWER(text and annotations read-only, links and widgets interactive) and thetextSelecttool, as above.
Load and reset a document
Before: Syncfusion PDF Viewer
documentPath loads the first document, and load(document, password) loads another one at runtime from a URL, a Base64 data URI, or a Uint8Array. Encrypted files get the built-in password popup. Loading reports through documentLoad and documentLoadFailed, whose arguments carry documentName, isPasswordRequired, and the incorrect password, and unload() closes the document:
import { useRef } from 'react'
import {
Inject,
Navigation,
PdfViewerComponent,
Toolbar,
} from '@syncfusion/ej2-react-pdfviewer'
function Viewer() {
const viewerRef = useRef(null)
async function openFromServer() {
const response = await fetch('/api/documents/contract.pdf', {
headers: { Authorization: `Bearer ${token}` },
})
const blob = await response.blob()
// The second argument is the password; the popup asks for it when it's missing or
// wrong.
viewerRef.current?.load(URL.createObjectURL(blob), null)
}
return (
<>
<button onClick={openFromServer}>Open</button>
<button onClick={() => viewerRef.current?.unload()}>Close</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
documentLoad={(args) => showDocumentUi(args.documentName)}
documentLoadFailed={(args) => showError(args.documentName)}
style={{ height: '100vh' }}
>
<Inject services={[Toolbar, Navigation]} />
</PdfViewerComponent>
</>
)
}In standalone mode, a load call made before the PDFium assets are ready can fail, so integrations that load right away wait for the resourcesLoaded event.
After: @sdk-pdf/react
loadPdf from useLoadPdf takes the file bytes, and the load state decides what to render. Fetching the file is your own code, with whatever headers your server needs:
import { useRef, useState } from 'react'
import { InvalidPasswordError, useLoadPdf } from '@sdk-pdf/react'
function DocumentLoader({ url }: { url: string }) {
const [loadState, loadPdf] = useLoadPdf()
const bytesRef = useRef<Uint8Array | null>(null)
const [needsPassword, setNeedsPassword] = useState(false)
const [loadError, setLoadError] = useState<unknown>()
async function open(password?: string) {
setLoadError(undefined)
try {
// Replaces documentPath: fetch the file once and keep the bytes for a password
// retry.
if (!bytesRef.current) {
const response = await fetch(url)
if (!response.ok)
throw new Error(`The document request failed with ${response.status}`)
bytesRef.current = new Uint8Array(await response.arrayBuffer())
}
// loadPdf reads from the buffer it receives for the document's lifetime, so pass a
// copy.
await loadPdf(bytesRef.current.slice(), { password })
setNeedsPassword(false)
} catch (error) {
if (error instanceof InvalidPasswordError) {
// Replaces the built-in password popup: ask, then call open(password) again.
setNeedsPassword(true)
} else {
// Replaces documentLoadFailed and the built-in error message box.
setLoadError(error)
}
}
}
if (needsPassword)
return <PasswordPrompt onSubmit={(password) => void open(password)} />
if (loadError) return <LoadError error={loadError} onRetry={() => void open()} />
if (loadState.status === 'initial')
return <button onClick={() => void open()}>Open</button>
if (loadState.status === 'loading') return <p>Loading the document…</p>
return <PdfApplication />
}useResetPdf replaces unload():
import { useResetPdf } from '@sdk-pdf/react'
function CloseDocumentButton() {
const resetPdf = useResetPdf()
// The load state returns to 'initial', and DocumentLoader shows its Open button again.
return <button onClick={() => resetPdf()}>Close</button>
}What to know
- Bytes, not paths.
loadPdftakes aUint8Array. A URL becomes afetch, aBloborFilebecomesnew Uint8Array(await blob.arrayBuffer()), and a Base64 string is decoded to bytes first. A server-backeddocumentPaththat names a file on the web service becomes a request to your own endpoint. - 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 your UI. Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with
InvalidPasswordError. There is no popup: catch the rejection, ask, and callloadPdfagain. - No events to wire.
documentLoadbecomes the awaitedloadPdforloadState.status === 'ready', anddocumentLoadFailedbecomes the rejection. With noresourceUrl, there is noresourcesLoadedstep before the first load. - Closing invalidates ids. After
resetPdf(), page and block ids, widget snapshots, and document revisions from the old document are no longer valid. For an open-another-document action, callresetPdf()and thenloadPdf.
Display the document
Before: Syncfusion PDF Viewer
The component renders the document with its toolbars and panels. Integrations pick the primary toolbar items, handle custom items in toolbarClick, hide toolbars with props or with toolbar.showToolbar(false), and style everything with a theme package:
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
// Only these items, in this order; a custom item is an object with an id.
toolbarSettings={{
toolbarItems: [
'OpenOption',
'PageNavigationTool',
'MagnificationTool',
{ id: 'fit_width', text: 'Fit to width', align: 'Center' },
'DownloadOption',
],
}}
toolbarClick={(args) => {
if (args.item.id === 'fit_width') viewerRef.current?.magnification.fitToWidth()
}}
enableNavigationToolbar={false}
style={{ height: '640px' }}
>
<Inject services={[Toolbar, Magnification, Navigation]} />
</PdfViewerComponent>For a fully custom toolbar, integrations set enableToolbar={false} and call the viewer API from their own buttons, which is the shape every toolbar takes with @sdk-pdf/react.
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 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: { '--selection-color': '#0b5cad' } as React.CSSProperties },
}
function PdfApplication() {
// Replaces zoomMode="FitToWidth": fits once per loaded document.
useViewportAutoFitScale('width')
return (
<div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: '100vh' }}>
{/* Replaces the primary toolbar: the controls from the sections below. */}
<header style={{ display: 'flex', gap: 8 }}>
<PageNavigation />
<ZoomControls />
<HistoryControls />
<SaveControls />
</header>
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', minHeight: 0 }}>
{/* Replaces the navigation panel. */}
<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.
toolbarItems,enableToolbar,enableNavigationToolbar, and the show and hide methods of thetoolbarmodule have no counterpart: every control exists only if you build it. Selection, text editing, form filling, links, and zoom gestures work insideViewport, limited by the content policy. - Toolbar clicks become handlers. Custom toolbar items and the
toolbarClickbranches onargs.item.idbecomeonClickhandlers on your own buttons, which call the hooks shown in this guide. - Theming is CSS you own. The Material 3, Fluent 2, Bootstrap 5, and Tailwind theme packages and their dark variants have no counterpart.
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. - Size the parent. Where
PdfViewerComponenttakes aheightor astyle,Viewportfills its parent element, so give the parent an explicit size. - Render only when ready.
ViewportthrowsDocumentNotLoadedErrorwhen no document is loaded.
Pages and navigation
Before: Syncfusion PDF Viewer
The navigation module moves between 1-based page numbers, pageCount and currentPageNumber read the position, and pageChange reports changes:
import { useRef, useState } from 'react'
import { Inject, Navigation, PdfViewerComponent } from '@syncfusion/ej2-react-pdfviewer'
function Viewer() {
const viewerRef = useRef(null)
const [pageNumber, setPageNumber] = useState(1)
const [pageCount, setPageCount] = useState(0)
return (
<>
<button onClick={() => viewerRef.current?.navigation.goToPreviousPage()}>
Previous
</button>
<span>
{pageNumber} of {pageCount}
</span>
<button onClick={() => viewerRef.current?.navigation.goToNextPage()}>Next</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
documentLoad={() => setPageCount(viewerRef.current.pageCount)}
pageChange={(args) => setPageNumber(args.currentPageNumber)}
style={{ height: '640px' }}
>
<Inject services={[Navigation]} />
</PdfViewerComponent>
</>
)
}After: @sdk-pdf/react
usePages returns the pages in document order, so a page number is an index in that list plus one. useScrollToPage scrolls to a page id, and useCurrentPageId re-renders with the page in the middle of the viewport:
import { useCurrentPageId, usePages, useScrollToPage } from '@sdk-pdf/react'
function PageNavigation() {
const pages = usePages()
// Replaces currentPageNumber and pageChange.
const currentPageId = useCurrentPageId()
const scrollToPage = useScrollToPage()
// Until there is a current page, treat the first page as current.
const index =
currentPageId === undefined ? 0 : pages.findIndex((page) => page.id === currentPageId)
// Replaces navigation.goToPage(pageNumber): resolve the 1-based number to an id first.
function scrollToPageNumber(pageNumber: number) {
const page = pages[pageNumber - 1]
if (page) scrollToPage(page.id, { behavior: 'smooth' })
}
return (
<div role="toolbar">
<button disabled={index <= 0} onClick={() => scrollToPageNumber(1)}>
First
</button>
{/* The current page number is index + 1, so the previous one is index. */}
<button disabled={index <= 0} onClick={() => scrollToPageNumber(index)}>
Previous
</button>
<span>
{index === -1 ? '–' : index + 1} of {pages.length}
</span>
<button
disabled={index === -1 || index >= pages.length - 1}
onClick={() => scrollToPageNumber(index + 2)}
>
Next
</button>
<button
disabled={index === -1 || index >= pages.length - 1}
onClick={() => scrollToPageNumber(pages.length)}
>
Last
</button>
</div>
)
}When you need a page's number inside a handler, such as for analytics, getPageIndex(pageId) from usePageIndexGetter resolves an id to its zero-based index without re-rendering on order changes; usePageCount replaces pageCount.
What to know
- Ids survive reordering; numbers don't. An id keeps pointing at the same page while pages are added, removed, and rearranged by the user, where a number would drift to another page. Ids are session-scoped: persist page numbers, not ids, and resolve them once the document is ready.
- Invalid targets throw.
goToPagewith a number out of range keeps the current page.scrollToPagewith an unknown or deleted id throwsInvalidInputError, so resolve the number first, asscrollToPageNumberabove does. - Scroll options.
scrollToPage(pageId, options?)takesbehavior('auto','smooth', or'instant'),vertical('top'by default), andhorizontal('nearest'by default). - Page size.
getPageInfo(pageIndex)becomes thewidth,height, androtationof ausePages()entry. The sizes are user-space units, one unit being 1/72 of an inch like a point, with the rotation applied. - Links and bookmarks. Links work inside
Viewportwhen the content policy'slinkslevel is'interactive'. The bookmarks panel (BookmarkView),hyperlinkClick, andhyperlinkOpenStatehave no equivalent.
Zoom and fit modes
Before: Syncfusion PDF Viewer
Zoom is a percentage, 10 to 400 by default and clamped to minZoom and maxZoom. zoomMode sets the initial fit, and the magnification module zooms and fits from your controls:
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
// 'Default', 'FitToWidth', or 'FitToPage'.
zoomMode="FitToWidth"
minZoom={50}
maxZoom={300}
zoomChange={(args) => persistZoom(args.zoomValue)}
style={{ height: '640px' }}
>
<Inject services={[Toolbar, Magnification, Navigation]} />
</PdfViewerComponent>viewerRef.current?.magnification.zoomTo(150)
viewerRef.current?.magnification.zoomIn()
viewerRef.current?.magnification.fitToPage()
viewerRef.current?.magnification.fitToWidth()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'
function ZoomControls() {
const [scale, setScale] = useViewportScale()
const getFitScale = useViewportFitScale()
// Replaces zoomChange: every scale change, whatever the source.
useEffect(() => persistZoom(scale), [scale])
function fitPage() {
const { fitWidthScale, fitHeightScale } = getFitScale()
// The smaller scale fits the pages in both directions, like fitToPage().
setScale(Math.min(fitWidthScale, fitHeightScale))
}
return (
<div role="toolbar">
<button onClick={() => setScale(scale / 1.25)}>Zoom out</button>
{/* The scale is a factor: 1 is 100%. */}
<span>{Math.round(scale * 100)}%</span>
<button onClick={() => setScale(scale * 1.25)}>Zoom in</button>
{/* 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
- Percentages become factors.
setScaletakes a positive finite number, not a percentage, and throwsInvalidInputErrorotherwise; scale1is one user-space unit per CSS pixel. Compare the page size on screen with a test document before converting zoom levels you stored as Syncfusion percentages. - Fits are one-time calculations.
useViewportAutoFitScaletakes'width'or'height'and applies when a document loads; it doesn't re-apply on viewport resizes or manual zooming.zoomMode="FitToPage"has no automatic counterpart: fit the page from a control, asfitPageabove does. - What the fits measure.
fitWidthScalefits the widest page of the document into the viewport width andfitHeightScalethe tallest page into its height, both accounting forviewportPadding. - Zoom range and gestures. Pinch and wheel zoom are built in and tuned with
zoomGesturesonPdfProviders:minScale(0.5by default) andmaxScale(4) take the place ofminZoomandmaxZoomfor the gestures, and the wheel speed is configurable too. - Zoom around a point.
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. The third item ofuseViewportScale(),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.
Thumbnails
Before: Syncfusion PDF Viewer
The ThumbnailView module adds a thumbnails panel, opened from the navigation toolbar on the left of the viewer. isThumbnailViewOpen opens it on load, the thumbnailView module opens and closes it, and thumbnailClick reports the clicked page:
;<>
<button onClick={() => viewerRef.current?.thumbnailView.openThumbnailPane()}>
Thumbnails
</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
isThumbnailViewOpen={true}
thumbnailClick={(args) => trackThumbnailClick(args.pageNumber)}
style={{ height: '640px' }}
>
<Inject services={[Toolbar, Navigation, ThumbnailView]} />
</PdfViewerComponent>
</>After: @sdk-pdf/react
PageCanvas renders a page lazily at the canvas's CSS size; the panel, its open state, and its highlight of the current page are yours:
import { useState } from 'react'
import { PageCanvas, useCurrentPageId, usePages, useScrollToPage } from '@sdk-pdf/react'
function ThumbnailsSidebar() {
const pages = usePages()
const currentPageId = useCurrentPageId()
const scrollToPage = useScrollToPage()
// Replaces isThumbnailViewOpen, openThumbnailPane(), and closeThumbnailPane().
const [isOpen, setIsOpen] = useState(true)
return (
<aside>
<button aria-expanded={isOpen} onClick={() => setIsOpen(!isOpen)}>
Thumbnails
</button>
{isOpen && (
<nav>
{pages.map((page, index) => (
<button
key={page.id}
aria-current={page.id === currentPageId ? 'page' : undefined}
// Replaces thumbnailClick: run your own code before or after scrolling.
onClick={() => scrollToPage(page.id)}
>
<PageCanvas
pageId={page.id}
className="thumbnail"
role="img"
aria-label={`Page ${index + 1}`}
/>
</button>
))}
</nav>
)}
</aside>
)
}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 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. - Render only when ready.
PageCanvasthrowsDocumentNotLoadedErrorwhen no document is loaded.
Page organizer
Before: Syncfusion PDF Viewer
The PageOrganizer module adds the Organize Pages dialog: users rotate, rearrange, insert, delete, and copy pages, import pages from another PDF, and then save over the document or save a copy. pageOrganizerSettings chooses the actions, pageOrganizer.openPageOrganizer() opens the dialog, and pageOrganizerSaveAs receives the result as Base64. extractPages returns selected pages as a new PDF:
const pageOrganizerSettings = {
canDelete: true,
canInsert: true,
canRotate: true,
canCopy: true,
canRearrange: true,
canImport: false,
}
function Viewer() {
const viewerRef = useRef(null)
return (
<>
<button onClick={() => viewerRef.current?.pageOrganizer.openPageOrganizer()}>
Organize pages
</button>
{/* A new PDF with pages 1 and 2, as a Uint8Array. */}
<button onClick={() => uploadExtract(viewerRef.current?.extractPages('1,2'))}>
Extract
</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
pageOrganizerSettings={pageOrganizerSettings}
pageOrganizerSaveAs={(args) => {
// Upload the result instead of the default Save As download.
args.cancel = true
uploadBase64(args.fileName, args.downloadDocument)
}}
style={{ height: '640px' }}
>
<Inject services={[Toolbar, Navigation, ThumbnailView, PageOrganizer]} />
</PdfViewerComponent>
</>
)
}After: @sdk-pdf/react
Each page operation is a hook that returns a command taking page ids: usePageCreate, usePageDelete, usePageMove, useSetPageRotation, and usePageDuplicate. The organizer panel is yours; it offers only the actions you render:
import {
PageCanvas,
usePageCreate,
usePageDelete,
usePageDuplicate,
usePageMove,
usePages,
useSetPageRotation,
} from '@sdk-pdf/react'
function OrganizePagesPanel() {
const pages = usePages()
const createPage = usePageCreate()
const deletePages = usePageDelete()
const duplicatePage = usePageDuplicate()
const { moveBefore, moveAfter } = usePageMove()
const setPageRotation = useSetPageRotation()
return (
<ol>
{pages.map((page, index) => (
<li key={page.id}>
<PageCanvas
pageId={page.id}
className="organizer-thumbnail"
role="img"
aria-label={`Page ${index + 1}`}
/>
{/* Rotation is absolute: add to the current angle. -90 is normalized to
270. */}
<button onClick={() => setPageRotation(page.id, page.rotation - 90)}>
Rotate left
</button>
<button onClick={() => setPageRotation(page.id, page.rotation + 90)}>
Rotate right
</button>
<button
disabled={index === 0}
onClick={() => moveBefore(page.id, pages[index - 1].id)}
>
Move up
</button>
<button
disabled={index === pages.length - 1}
onClick={() => moveAfter(page.id, pages[index + 1].id)}
>
Move down
</button>
{/* Copy pages: the duplicate goes right after the source. */}
<button onClick={() => void duplicatePage(page.id)}>Copy</button>
{/* A blank page the size of this one as displayed, inserted after it. */}
<button onClick={() => createPage(page.width, page.height, page.id)}>
Insert blank page
</button>
<button disabled={pages.length <= 1} onClick={() => deletePages(page.id)}>
Delete
</button>
</li>
))}
</ol>
)
}What to know
| Organize Pages action | @sdk-pdf/react |
|---|---|
| Rotate | setPageRotation(pageIdOrIds, rotation) sets an absolute angle; values outside 0..360 are normalized, so -90 and 450 work, and angles that aren't quarter turns throw InvalidInputError |
| Rearrange | moveBefore(ids, beforePageId?) (start when omitted) or moveAfter(ids, afterPageId?) (end when omitted); drag and drop is your UI |
| Insert blank pages | createPage(width, height, insertAfterPageId?): after the given page, or appended; the size in user-space units, with no defaults |
| Delete | deletePages(pageIdOrIds) |
| Copy | duplicatePage(sourcePageId, insertAfterPageId?) is asynchronous and inserts after the source by default |
| Select all, bulk actions | Pass several ids in one call |
| Import a PDF | No equivalent |
extractPages | prepareDocument({ pageIds }) from useDocumentSaving resolves with a new PDF of the listed pages, in the listed order |
| Save, Save As | Changes are part of the document; save with prepareDocument() or downloadDocument() |
- Undo. Deleting, moving, or rotating several pages in one call is a single undo step, and an undo can restore a deleted page with its content; separate edits within 500 ms merge into one step.
- No page events. Page operations your code calls emit no events: run your follow-up where you call them, and watch
useDocumentRevisionfor any change. - No dialog to open.
openPageOrganizer(),isPageOrganizerOpen, and the zoom slider settings become your own panel state and thumbnail size.
Forms
Before: Syncfusion PDF Viewer
The FormFields module makes fields fillable. Field objects come from formFieldCollections or retrieveFormFields(), with name, type, value, and isChecked, and a changed field goes back through updateFormFieldsValue. With enableFormFieldsValidation, required fields are checked before printing and downloading:
function Viewer() {
const viewerRef = useRef(null)
function prefill() {
const viewer = viewerRef.current
const field = viewer
.retrieveFormFields()
.find((field) => field.name === 'customer.name')
if (field) {
field.value = 'Jane Doe'
viewer.updateFormFieldsValue(field)
}
}
return (
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
documentLoad={prefill}
formFieldFocusOut={(args) => autosave(args.fieldName, args.value)}
enableFormFieldsValidation={true}
validateFormFields={(args) => showMissingFields(args.nonFillableFields)}
style={{ height: '100vh' }}
>
<Inject services={[Toolbar, Navigation, FormFields]} />
</PdfViewerComponent>
)
}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. Users fill fields in Viewport with nothing to build when the content policy allows interacting with forms. For form UI outside the main view, useWidgets lists the widgets in reading order and useWidgetValue binds one widget's value like a useState backed by the document:
import {
CheckboxWidget,
TextWidget,
useWidgets,
useWidgetValue,
Widget,
} from '@sdk-pdf/react'
// The counterpart of formFieldCollections, re-rendered as the widgets change.
function FormPanel({ savedValues }: { savedValues: Record<string, string> }) {
const { widgets, isLoaded } = useWidgets()
// The list is partial until every page containing widgets has been loaded.
if (!isLoaded) return <p>Loading the form…</p>
return (
<form>
{widgets.map((widget) => (
<FormField
key={widget.id}
widget={widget}
savedValue={savedValues[widget.fieldName]}
/>
))}
</form>
)
}
function FormField({ widget, savedValue }: { widget: Widget; savedValue?: string }) {
switch (widget.widgetType) {
case 'text':
return <TextField widget={widget} savedValue={savedValue} />
case 'checkbox':
return <CheckboxField widget={widget} />
default:
return null
}
}
function TextField({ widget, savedValue }: { widget: TextWidget; savedValue?: string }) {
// Filling in the main view and undo update this value.
const [value, setValue] = useWidgetValue(widget)
return (
<label>
{widget.fieldName}
<input value={value} onChange={(event) => setValue(event.currentTarget.value)} />
{/* Replaces setting field.value and calling updateFormFieldsValue. */}
{savedValue !== undefined && (
<button type="button" onClick={() => setValue(savedValue)}>
Fill in
</button>
)}
</label>
)
}
function CheckboxField({ widget }: { widget: CheckboxWidget }) {
// isChecked becomes the boolean value.
const [isChecked, setIsChecked] = useWidgetValue(widget)
return (
<label>
<input
type="checkbox"
checked={isChecked}
onChange={(event) => setIsChecked(event.currentTarget.checked)}
/>
{widget.fieldName}
</label>
)
}From fields to widgets
| Concept | Syncfusion PDF Viewer | @sdk-pdf/react |
|---|---|---|
| Lookup | formFieldCollections, retrieveFormFields() | useWidgets() (re-renders), or await getWidgets() from useWidgetsGetter (one-time, loads every page with widgets first) |
| Shared values | Radio buttons grouped by the same name | Every widget with the same field name shows and synchronizes the same value, radio groups included |
| Values | field.value, field.isChecked, then updateFormFieldsValue | useWidgetValue(widget): string for text and choice, boolean for checkbox and radio, option indices for multiChoice |
| Clearing | clearFormFields(), resetFormFields() | Set the empty value per widget with useWidgetValue; no reset to defaults |
| Focus | focusFormField(name) | scrollToBlock(widget.id) from useScrollToBlock scrolls to the widget |
| Events | formFieldFocusOut, formFieldClick, and the other field events | widgetInput from useViewportEvent reports a value the user changed, and useFocusedWidgetId returns the widget being filled; useWidgetValue re-renders |
| Validation | isRequired, enableFormFieldsValidation, validateFormFields | No equivalent; validate in your own submit flow |
| Form data | importFormFields(), exportFormFields() as JSON, FDF, or XFDF | No equivalent; prepareDocument() produces the filled PDF |
What to know
- 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, render a bound component per widget, likeTextFieldabove. - Field kinds. The widget kinds are
text,choice,multiChoice,checkbox,radio,pushButton, andsignature. Syncfusion's password fields and initial fields have no matching kind. - Read-only forms.
enableFormFields={false}maps to a policy wherewidgetsis'readonly', such asPDF_PRINTER, or'hidden'to leave the widgets out of the view. To lock a submitted form instead, setisReadOnlyon the widgets, as Form designer shows. - Date fields. A text widget's
formatexposes Acrobat date formats asFormatType.DateTime, recognized without running scripts; seeFormatType. - Signatures.
signaturewidgets carry a read-onlyisCryptoSigned; the SDK never signs.
Form designer
Before: Syncfusion PDF Viewer
The FormDesigner module adds the form designer toolbar and the formDesigner module. Fields are added at given bounds or placed by the user after setFormFieldMode, updated with settings objects, and deleted by object or id:
function Designer() {
const viewerRef = useRef(null)
function addNameField() {
viewerRef.current?.formDesigner.addFormField('Textbox', {
name: 'customer.name',
pageNumber: 1,
bounds: { X: 100, Y: 150, Width: 200, Height: 24 },
maxLength: 40,
isRequired: true,
})
}
function lockForm() {
const viewer = viewerRef.current
for (const field of viewer.retrieveFormFields()) {
viewer.formDesigner.updateFormField(field, { isReadOnly: true })
}
}
function deleteField(name) {
const viewer = viewerRef.current
const field = viewer.retrieveFormFields().find((field) => field.name === name)
if (field) viewer.formDesigner.deleteFormField(field)
}
return (
<>
<button onClick={addNameField}>Add name field</button>
{/* The next click on a page places a text box. */}
<button onClick={() => viewerRef.current?.formDesigner.setFormFieldMode('Textbox')}>
Place text box
</button>
<button onClick={lockForm}>Lock form</button>
<button onClick={() => deleteField('customer.name')}>Delete name field</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
designerMode={true}
formFieldAdd={(args) => trackFieldAdded(args.pageIndex)}
style={{ height: '100vh' }}
>
<Inject services={[Toolbar, Navigation, FormFields, FormDesigner]} />
</PdfViewerComponent>
</>
)
}After: @sdk-pdf/react
useWidgetsCreate creates widgets, useSetWidgetProps updates them by id, and useBlockDelete deletes them:
import {
useBlockDelete,
useSetWidgetProps,
useWidgetsCreate,
useWidgetsGetter,
} from '@sdk-pdf/react'
function useFormDesigner() {
const createWidgets = useWidgetsCreate()
const setWidgetProps = useSetWidgetProps()
const deleteBlocks = useBlockDelete()
const getWidgets = useWidgetsGetter()
// Replaces formDesigner.addFormField('Textbox', { name, bounds, maxLength }).
async function addNameField(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: 180,
height: 24,
fieldName: 'customer.name',
maxLength: 40,
})
return widget
}
// Replaces updateFormField(field, { isReadOnly: true }) for every field, in one call.
async function lockForm() {
const widgets = await getWidgets()
// excludeFromHistory keeps undo from unlocking the submitted form.
// The props apply at once; the promise resolves when the widgets show them.
await setWidgetProps(
widgets.map((widget) => widget.id),
{ isReadOnly: true },
{ excludeFromHistory: true }
)
}
// Replaces deleteFormField(field): same-named widgets act as one field, so delete them
// together.
async function deleteField(fieldName: string) {
const widgets = await getWidgets()
const widgetIds = widgets
.filter((widget) => widget.fieldName === fieldName)
.map((widget) => widget.id)
if (widgetIds.length > 0) deleteBlocks(widgetIds)
}
return { addNameField, lockForm, deleteField }
}To let users place a widget by clicking the page, like setFormFieldMode, switch the interaction tool to { type: 'addWidget', widgetProps } with useSetInteractionMode; pass a policy where widgets are editable, such as PDF_EDITOR, with the tool.
From field settings to widget props
| Syncfusion field settings | @sdk-pdf/react widget props |
|---|---|
Textbox, CheckBox, RadioButton | widgetType 'text', 'checkbox', 'radio' |
DropDown, ListBox | 'choice' (with isCombo for a dropdown) or 'multiChoice' |
SignatureField | 'signature' |
Password, InitialField | No equivalent |
name | fieldName; widgets sharing it share the value |
pageNumber and bounds | pageId and x, y, width, height in user-space units from the bottom-left corner |
isReadOnly, maxLength, isMultiline | isReadOnly, maxLength, isMultiline |
backgroundColor, borderColor, thickness | fillColor, borderColor, borderWidth |
color, fontSize, alignment | textColor, fontSize, horizontalAlign |
options with itemName and itemValue | options with label and value |
isRequired, tooltip, visibility, isPrint, fontFamily | No equivalent |
What to know
- Convert stored positions.
@sdk-pdf/reactwidget positions are user-space units, 1/72 of an inch, with the origin at the bottom-left corner of the page. Check placement on a test document before convertingboundsyou stored for Syncfusion. - Designer UI. The form designer toolbar and its properties panel become your own controls.
useSelectBlocksreplacesselectFormFieldandclearSelection(selectBlocks([])),useSelectedBlocksreturns the selected widgets for a properties panel,setWidgetPropsfromuseSetWidgetPropschanges their props, such asisMultilineandformat,useSelectionAnnotationPropschanges their border color and width, anduseDuplicateSelectioncopies them under new field names. - Replacing a kind. A widget's kind can't be changed through its props: create the new widget with the same field name and delete the old one, as Working with forms shows.
- Undo. Creating widgets is an undo step, and so is changing their props, unless you pass
{ excludeFromHistory: true }tosetWidgetProps. - Design and fill modes.
designerModeandenableFormDesignermap to the content policy'swidgetslevel:'interactive'(as inPDF_VIEWER) lets users fill fields, and'editable'(as inPDF_EDITOR) makes widgets editable, as theaddWidgettool needs. The level can be a function of the widget id, such aswidgets: (blockId) => (lockedIds.has(blockId) ? 'interactive' : 'editable').isReadOnlyrestricts filling a field, not authoring the widget.
Editing and interaction tools
Before: Syncfusion PDF Viewer
interactionMode switches between text selection (the default) and panning, annotation.setAnnotationMode arms an annotation tool for the next interaction, and the enable* props remove the editing features for read-only users:
function Viewer({ isReadOnly }) {
const viewerRef = useRef(null)
return (
<>
<button onClick={() => (viewerRef.current.interactionMode = 'TextSelection')}>
Select text
</button>
<button onClick={() => (viewerRef.current.interactionMode = 'Pan')}>Pan</button>
<button onClick={() => viewerRef.current?.annotation.setAnnotationMode('FreeText')}>
Free text
</button>
<button onClick={() => viewerRef.current?.annotation.setAnnotationMode('None')}>
Stop
</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
interactionMode="TextSelection"
enableAnnotation={!isReadOnly}
enableFormDesigner={!isReadOnly}
enablePageOrganizer={!isReadOnly}
style={{ height: '100vh' }}
>
<Inject
services={[
Toolbar,
Navigation,
TextSelection,
Annotation,
FormFields,
FormDesigner,
PageOrganizer,
]}
/>
</PdfViewerComponent>
</>
)
}After: @sdk-pdf/react
The interaction mode combines a content policy (what may change) and an interaction tool (what a click, tap, or drag does); useSetInteractionMode switches both at once, and useInteractionTool reads the tool back:
import {
CONTENT_POLICY_PRESET,
InteractionTool,
useInteractionTool,
useSetInteractionMode,
} from '@sdk-pdf/react'
const textSelectTool: InteractionTool = { type: 'textSelect' }
const selectTool: InteractionTool = { type: 'select' }
const addTextTool: InteractionTool = { type: 'addText' }
function ModeSwitch() {
const interactionTool = useInteractionTool()
const setInteractionMode = useSetInteractionMode()
return (
<div role="toolbar">
{/* Like interactionMode="TextSelection" with the editing modules off. */}
<button
aria-pressed={interactionTool.type === 'textSelect'}
onClick={() =>
setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_VIEWER)
}
>
Read
</button>
{/* Select, move, and resize blocks; page text is editable in place. */}
<button
aria-pressed={interactionTool.type === 'select'}
onClick={() => setInteractionMode(selectTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
>
Edit
</button>
{/* The nearest tool to free text: a new paragraph at the click position. */}
<button
aria-pressed={interactionTool.type === 'addText'}
onClick={() => setInteractionMode(addTextTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
>
Add text
</button>
</div>
)
}Mapping the modes
| Syncfusion PDF Viewer | @sdk-pdf/react |
|---|---|
interactionMode="TextSelection" | { type: 'textSelect' }: native text selection for reading and copying, with read-only text. |
interactionMode="Pan" | No tool: Viewport scrolls and zooms on its own. |
setAnnotationMode('None') | Back to { type: 'select' } or { type: 'textSelect' }. |
setAnnotationMode('FreeText') | The nearest tool is { type: 'addText' }, which creates a text paragraph in editable text. |
| Image stamps | { type: 'addImage', image, width?, height? } with await stageImage(blob) places a stamp annotation. See Annotations. |
formDesigner.setFormFieldMode(type) | { type: 'addWidget', widgetProps } with editable widgets. |
Ink | No drawing tool. useInkAnnotationCreate creates an ink annotation from strokes your code collects. |
Text markup, shapes, measurement, StickyNotes, HandWrittenSignature, Initial, Redaction | No equivalent. |
enableAnnotation, enableFormDesigner, enableFormFields, designerMode | The annotations and widgets levels of the content policy: PDF_EDITOR makes everything editable, PDF_VIEWER lets users fill forms, PDF_PRINTER makes everything read-only but links. |
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 policy is a plain object with
text,annotations,links, andwidgetslevels, finer than turning modules on and off;useSetInteractionModelists them. Each level can also be'hidden', which leaves that content out of the view, or a function of the block id for per-block levels. There is no hook to read the policy back: keep the policy you set in your own state to show or hide editing UI. - Editing existing text is new. Under
PDF_EDITOR, page text is editable in place andViewporthandles typing.useSelectionTextStyledrives a style toolbar (font, size, color, bold, italic, alignment, spacing), anduseFocusSelectionreturns the focus after a toolbar click. - Local mode. Switching the mode affects this client only.
- Keyboard shortcuts. Wire the shortcuts you need, such as delete with
useDeleteSelectionor arrow-key nudging withuseMoveSelection, throughuseHotkeyswith a memoized mapping. Wrap block shortcuts increateUnfocusedHandler, so they stay inactive while the user types in a paragraph or a form field.
Annotations
Before: Syncfusion PDF Viewer
The Annotation module adds the annotation toolbar and the annotation module: text markup, shapes, measurements, free text, ink, stamps, and sticky notes, with comments, default settings such as the author, and JSON or XFDF export:
function ReviewTools() {
const viewerRef = useRef(null)
function addHighlight() {
viewerRef.current?.annotation.addAnnotation('Highlight', {
bounds: [{ x: 97, y: 110, width: 350, height: 14 }],
pageNumber: 1,
})
}
function recolorMine() {
const viewer = viewerRef.current
for (const annotation of viewer.annotationCollection) {
if (annotation.author === 'Reviewer') {
annotation.color = '#ff0000'
viewer.annotation.editAnnotation(annotation)
}
}
}
async function storeAnnotations() {
// Annotations are stored apart from the PDF.
await saveAnnotations(await viewerRef.current.exportAnnotationsAsObject())
}
return (
<>
<button onClick={addHighlight}>Highlight</button>
<button onClick={recolorMine}>Recolor mine</button>
<button onClick={storeAnnotations}>Store annotations</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
annotationSettings={{ author: 'Reviewer' }}
annotationAdd={(args) => trackAnnotation(args.annotationType, args.pageIndex)}
style={{ height: '100vh' }}
>
<Inject services={[Toolbar, Navigation, TextSelection, Annotation]} />
</PdfViewerComponent>
</>
)
}After: @sdk-pdf/react
Existing annotations display in Viewport and in PageCanvas, and selected shapes, lines, ink, and widgets can be restyled with useSelectionAnnotationProps. Images become stamp annotations, placed with the addImage tool, which takes an image staged with stageImage:
import {
CONTENT_POLICY_PRESET,
stageImage,
useSelectionAnnotationProps,
useSetInteractionMode,
} from '@sdk-pdf/react'
function AnnotationToolbar() {
const { isApplicable, borderColor, borderWidth } = useSelectionAnnotationProps()
const setInteractionMode = useSetInteractionMode()
async function placeImage(file: File) {
// Replaces an image stamp: the next click on a page places the image as a stamp
// annotation.
const image = await stageImage(file)
// Placing images is editing, so set the policy with the tool. The width is in
// user-space units.
setInteractionMode(
{ type: 'addImage', image, width: 150 },
CONTENT_POLICY_PRESET.PDF_EDITOR
)
}
return (
<div role="toolbar">
<input
type="file"
accept="image/*"
onChange={(event) => {
const file = event.currentTarget.files?.[0]
if (file) placeImage(file).catch(showError)
}}
/>
{/* Like the stroke color and thickness pickers, for the selected annotations. */}
{isApplicable && (
<>
<input
type="color"
value={borderColor.value ?? '#000000'}
onChange={(event) => borderColor.set(event.currentTarget.value)}
/>
<input
type="number"
min={0}
value={borderWidth.value ?? ''}
onChange={(event) => borderWidth.set(Number(event.currentTarget.value))}
/>
</>
)}
</div>
)
}What to know
- Few annotation kinds. Creating text markup, shapes, measurements, free text, stamps other than images, sticky notes, handwritten signatures, and redactions has no equivalent, from the UI or from code (
addAnnotation). From code,useStampAnnotationCreateplaces an image stamp at a position on a page, anduseInkAnnotationCreatecreates ink from strokes your code collects; there is no ink drawing tool. - Restyling is the border.
useSelectionAnnotationPropschanges the border color, with the opacity in its alpha channel, and the border width of the selected shapes, lines, ink, and widgets, and skips the rest of the selection.editAnnotationwith other properties has no equivalent. - Images.
stageImage(blob)accepts any image format the browser decodes, and SVGs are rasterized when the PDF is saved. The size goes on the tool or onuseStampAnnotationCreate:widthandheightin user-space units, a single dimension keeping the aspect ratio. Without a size, the image's natural size is used, fit down to the page. - No annotation data.
annotationCollection, the annotation events, the comments panel,annotationSettings(author, locking,skipPrint,skipDownload), and JSON or XFDF import and export have no equivalent.useDocumentRevisionchanges on any edit, andaddToolAppliedfromuseViewportEventreports a stamp the user placed. - Hiding annotations in previews.
PageCanvasdraws annotations unlesshideAnnotationsis set.
Undo, redo, and change tracking
Before: Syncfusion PDF Viewer
undo() and redo() revert and reapply annotation edits, from the toolbar's undo and redo tools, the keyboard, or code. isDocumentEdited tells whether the document was edited, such as for a warning before leaving the page:
function Viewer() {
const viewerRef = useRef(null)
useEffect(() => {
function warnIfEdited(event) {
if (viewerRef.current?.isDocumentEdited) event.preventDefault()
}
window.addEventListener('beforeunload', warnIfEdited)
return () => window.removeEventListener('beforeunload', warnIfEdited)
}, [])
return (
<>
<button onClick={() => viewerRef.current?.undo()}>Undo</button>
<button onClick={() => viewerRef.current?.redo()}>Redo</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
style={{ height: '100vh' }}
>
<Inject services={[Toolbar, Navigation, Annotation, FormFields]} />
</PdfViewerComponent>
</>
)
}After: @sdk-pdf/react
useUndoRedo returns the commands and their availability, and useHotkeys wires the shortcuts:
import { useMemo } from 'react'
import { useHotkeys, useUndoRedo, useViewportEvent } from '@sdk-pdf/react'
function HistoryControls() {
// canUndo and canRedo re-render the component as the history changes.
const { undo, redo, canUndo, canRedo } = useUndoRedo()
// The mapping must be memoized, or the listener re-installs on every render.
useHotkeys(useMemo(() => ({ 'Mod-z': undo, 'Shift-Mod-z': redo }), [undo, redo]))
// Events report what the user does in the viewport, such as moving blocks by dragging.
useViewportEvent('blocksDragEnd', ({ blockIds }) => trackMove(blockIds.length))
return (
<div role="toolbar">
<button disabled={!canUndo} onClick={undo}>
Undo
</button>
<button disabled={!canRedo} onClick={redo}>
Redo
</button>
</div>
)
}isDocumentEdited becomes 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 and the leave-page warning live with the save button in Saving and downloading.
What to know
- One history for the whole document, not only for annotations: deleting, moving, or rotating pages are undo steps, widget property changes are undo steps unless excluded, page text edits are undo steps, 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. - Shortcuts.
Modis Cmd on macOS and Ctrl elsewhere; a handler that returnsfalselets the event reach the browser, and a falsy value instead of the mapping disables the shortcuts, such as while a modal is open. - Events for user actions only.
useViewportEventreports what users do in the main view, such astextEditorInput,addToolApplied,blocksDragEnd,blockResizeDragEnd, 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: Syncfusion PDF Viewer
saveAsBlob() resolves with the edited PDF as a Blob, with annotations, form values, and page changes applied, and download() saves it on the user's device under downloadFileName. downloadStart can cancel a download, and annotationSettings.skipDownload leaves annotations out. Server-backed integrations can save on the web service in its Download action instead:
function Viewer() {
const viewerRef = useRef(null)
async function upload() {
const blob = await viewerRef.current.saveAsBlob()
const body = new FormData()
body.append('file', blob, 'contract.pdf')
await fetch('/api/documents', { method: 'POST', body })
}
return (
<>
<button onClick={upload}>Save</button>
<button onClick={() => viewerRef.current?.download()}>Download</button>
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
downloadStart={(args) => {
if (!canDownload()) args.cancel = true
}}
style={{ height: '100vh' }}
>
<Inject services={[Toolbar, Navigation, Annotation, FormFields]} />
</PdfViewerComponent>
</>
)
}After: @sdk-pdf/react
useDocumentSaving applies the recorded changes to the original document. The component that saves also owns the saved revision, so the save, the indicator, and the leave-page warning compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's:
import { useEffect, useRef, useState } from 'react'
import {
useDocumentRevision,
useDocumentRevisionGetter,
useDocumentSaving,
} from '@sdk-pdf/react'
function SaveControls() {
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()
// Replaces isDocumentEdited: undoing back to the saved state clears it too.
const hasUnsavedChanges = revision !== savedRevision
// Warn before leaving the page with unsaved changes.
useEffect(() => {
if (!hasUnsavedChanges) return
const warn = (event: BeforeUnloadEvent) => event.preventDefault()
window.addEventListener('beforeunload', warn)
return () => window.removeEventListener('beforeunload', warn)
}, [hasUnsavedChanges])
async function upload() {
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()
// Replaces saveAsBlob(). The bytes go to fetch, so don't hand out the loaded
// document's buffer.
const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })
const body = new FormData()
body.append(
'file',
new Blob([bytes as BlobPart], { type: 'application/pdf' }),
'contract.pdf'
)
const response = await fetch('/api/documents', { method: 'POST', body })
// A failed upload keeps the unsaved-changes indicator.
if (!response.ok) return
setSavedRevision(startRevision)
} finally {
isSavingRef.current = false
}
}
return (
<div role="toolbar">
<button onClick={() => void upload()}>Save</button>
<button onClick={() => void downloadDocument()}>Download</button>
{hasUnsavedChanges && <span>Unsaved changes</span>}
</div>
)
}What to know
- One artifact. Where Syncfusion lets you store annotations and form data apart from the PDF (JSON, XFDF, or FDF),
@sdk-pdf/reactsaves the complete PDF:prepareDocument()resolves with its bytes as aUint8Array, anddownloadDocument()saves the same bytes on the user's device. - A Uint8Array, not a Blob. Wrap the bytes in a
BlobforFormData, as above, or send them as the request body. - returnOriginalIfUnchanged defaults to
true, returning the buffer passed toloadPdfwhen nothing changed; set it tofalsewhen the bytes will be transferred or mutated. The other option,pageIds, saves only the listed pages, asextractPagesdoes. There is no download file name, flattening, or option to leave annotations out. - No download events. Instead of cancelling in
downloadStartor reacting indownloadEnd, run your checks before callingdownloadDocument()and your follow-up after awaiting it. Both functions throwDocumentNotLoadedErrorwhen no document is loaded. - Server-backed saving. The web service's
Downloadaction has no counterpart: post the bytes fromprepareDocument()to your own endpoint, which stores them as it did before.
Collaboration
Syncfusion's collaborative editing connects the viewer to a Collaboration Server through @syncfusion/ej2-collaborator and a PdfViewerAdapter, with Redis behind the server, and shares annotations, form field changes, and Organize Pages operations between users.
Real-time collaboration in @sdk-pdf/react is coming soon. The adapter and the Collaboration Server setup don't carry over.
Errors and logging
Before: Syncfusion PDF Viewer
Load failures arrive through documentLoadFailed, and the viewer shows errors in its own message box, which showNotificationDialog={false} turns off; showNotificationPopup(message) shows a message in it:
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
// Show your own error UI instead of the built-in message box.
showNotificationDialog={false}
documentLoadFailed={(args) => showError(`Couldn't open ${args.documentName}`)}
style={{ height: '100vh' }}
>
<Inject services={[Toolbar, Navigation]} />
</PdfViewerComponent>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 useReportSdkWarnings() {
useEffect(() => {
// The SDK never prints to the console: subscribe, and unsubscribe on unmount.
const unsubscribe = logMessages.on('message', (logMessage) => {
if (logMessage.level >= LogLevel.Warning) {
reportToMonitoring(logMessage.message, logMessage.data)
}
})
return () => {
unsubscribe()
}
}, [])
}
function useOpenBytes() {
const [, loadPdf] = useLoadPdf()
return async (data: Uint8Array) => {
try {
// Pass a copy and keep `data` for a password retry.
await loadPdf(data.slice())
} catch (error) {
// Replaces documentLoadFailed for this load.
if (error instanceof InvalidPasswordError) askForPassword()
else showError(error)
}
}
}What to know
- Error classes.
DocumentNotLoadedError,InvalidInputError,InvalidPasswordError,DocumentFormatError(malformed beyond what the SDK can work around),NotSupportedError(a PDF feature not supported yet), 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.
ViewportandPageCanvasare the exception: they throwDocumentNotLoadedErrorwhen rendered without a document. - No dialogs. The SDK shows no message box, so
showNotificationDialogandshowNotificationPopuphave no counterpart: error UI is yours. - Logs are opt-in. Each message carries
level(LogLevel.Debug,Info,Warning,Error),reason, a staticmessage,data, andstack. Filter bylevelin your subscriber.
Cleanup and lifecycle
Before: Syncfusion PDF Viewer
unload() closes the document and fires documentUnload, and destroy() releases the instance's resources. By default the viewer keeps session data, such as form fields, annotations, and signatures, in the browser's sessionStorage; with enableLocalStorage it keeps them in memory, and the instance should be destroyed when it's no longer needed:
<PdfViewerComponent
ref={viewerRef}
resourceUrl={RESOURCE_URL}
enableLocalStorage={true}
documentUnload={() => clearDocumentState()}
style={{ height: '100vh' }}
>
<Inject services={[Toolbar, Navigation]} />
</PdfViewerComponent>After: @sdk-pdf/react
| Concern | @sdk-pdf/react |
|---|---|
| Closing a document | resetPdf() from useResetPdf unloads the document and invalidates its ids, snapshots, and revisions; run your documentUnload code after calling it. |
| Event listeners | useViewportEvent subscribes for the component's lifetime, with a callback that doesn't need memoizing. |
| Keyboard listeners | useHotkeys installs window-level shortcuts; pass a falsy value instead of the mapping to disable them. |
| Log subscriptions | logMessages.on() returns an unsubscribe function: call it in the effect cleanup, as in Errors and logging. |
| Buffers | Don't reuse a buffer passed to loadPdf; copy it first. |
There is no destroy() to call and no storage setting: the document lives in PdfProviders in your React tree.
Text extraction and search
The Syncfusion viewer extracts text with extractText(pageIndex, options), which resolves with textData and pageText, and searches with the TextSearch module (searchText, findText). In @sdk-pdf/react, getParagraphsByPage(pageId) from useProgrammaticEditor resolves with paragraph entries in content order: id, content, pageNumber, the box bounds ({ x, y, width, height }), and the paragraph layout, such as textAlign and lineHeight:
import { usePages, useProgrammaticEditor } from '@sdk-pdf/react'
function useExtractPageText() {
const pages = usePages()
const { getParagraphsByPage } = useProgrammaticEditor()
// Replaces extractText(pageIndex, ExtractTextOption.TextOnly): the index is zero-based
// in both.
return async (pageIndex: number) => {
const page = pages[pageIndex]
if (!page) return ''
const paragraphs = await getParagraphsByPage(page.id)
return paragraphs.map((paragraph) => paragraph.content).join('\n\n')
}
}The bounds are user-space units with the origin at the bottom-left corner of the page.
useDocumentSearch replaces the TextSearch module: set the query and the options, and the SDK finds the matches, highlights them on the pages, and scrolls to the active one:
import { useDocumentSearch } from '@sdk-pdf/react'
function SearchBar() {
const {
criteria,
matches,
activeMatchIndex,
status,
setSearchCriteria,
goToNextMatch,
goToPreviousMatch,
} = useDocumentSearch()
return (
<div role="search">
{/* Replaces searchText(text, isMatchCase); an empty query replaces
cancelTextSearch(). */}
<input
value={criteria.query}
onChange={(event) =>
setSearchCriteria({ ...criteria, query: event.currentTarget.value })
}
/>
<label>
<input
type="checkbox"
checked={criteria.isCaseSensitive}
onChange={(event) =>
setSearchCriteria({
...criteria,
isCaseSensitive: event.currentTarget.checked,
})
}
/>
Match case
</label>
<span>
{status === 'searching'
? 'Searching…'
: `${activeMatchIndex + 1} of ${matches.length}`}
</span>
{/* Replace searchPrevious() and searchNext(). */}
<button disabled={matches.length === 0} onClick={goToPreviousMatch}>
Previous
</button>
<button disabled={matches.length === 0} onClick={goToNextMatch}>
Next
</button>
</div>
)
}- Options.
isCaseSensitivereplacesisMatchCase, andisWholeWordmatches whole words only. - Matches. Each match carries
id,pageId,paragraphId, and the matchedtext, in document order;goToMatch(match.id)activates one from a results list. A changed query is searched once typing pauses, andstatusturns'complete'once the whole document is searched, in place oftextSearchComplete. Matches carry no bounds, sofindTexthas no counterpart. - No selection API. There is no API to read the user's text selection. To jump to a paragraph you located yourself, call
scrollToBlock(paragraphId)fromuseScrollToBlock.
Features without an equivalent
Plan around these before you migrate:
| Syncfusion PDF Viewer feature | In @sdk-pdf/react |
|---|---|
Server-backed mode (serviceUrl and the web service) | No equivalent; the SDK runs in the browser, and your server keeps serving and storing the bytes |
Printing (Print) | No print API; PageCanvas can render pages for your own print layout, but it draws the pages as loaded from the file, without the session's edits |
Bookmarks (BookmarkView), hyperlink events | No equivalent |
| Text markup, shape, measurement, free text, and sticky note annotations; ink drawing; the comments panel | No equivalent; existing shapes, lines, and ink display, their border color and width can be changed, and ink can be created from code with useInkAnnotationCreate |
| Dynamic, sign here, standard business, and text stamps | No equivalent; images are placed with the addImage tool |
| Handwritten signatures, initials, digital signatures, redaction | No equivalent; the SDK never signs |
Annotation and form data import and export (JSON, XFDF, FDF), flattening, skipDownload | No equivalent |
Resetting form fields to their default values (resetFormFields) | No equivalent; set each widget's value with useWidgetValue |
Form validation (isRequired, validateFormFields), password fields, initial fields, field tooltips | No equivalent |
| Importing pages from another PDF | No equivalent |
Search hit bounds (findText) | No equivalent; useDocumentSearch highlights the matches itself |
Coordinate conversion (convertClientPointToPagePoint, convertPagePointToClientPoint) | No equivalent |
Fit to page on load (zoomMode="FitToPage") | Fits are one-time calculations; see Zoom and fit modes |
Download events and file name (downloadStart, downloadEnd, downloadFileName) | No equivalent |
Built-in toolbars, context menu, dialogs, theme packages, localization (locale) | The UI is yours |