Migration

Migrating from Apryse WebViewer

This guide moves a web application built on Apryse WebViewer to @sdk-pdf/react. WebViewer ships a complete viewer UI on top of its Core APIs; @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 WebViewer UI you rely on as React components, then moving your instance.Core calls to hooks. Work through the checklist below; each step links to the section that covers it.

Beta: @sdk-pdf/react is in beta, and its exports and APIs may change between releases. Pin the exact package version and review the changes when you update.

Migration checklist

Overview

How the two integrations differ

WebViewer is a packaged application:

  • WebViewer(options, viewerElement) embeds a viewer in the page and resolves with an instance. instance.UI drives the prebuilt interface; instance.Core holds documentViewer, annotationManager, Tools, and Annotations.
  • Since version 11 the UI runs in a Web Component by default (an iframe is opt-in). You customize it from outside: hide elements with UI.disableElements, switch features with UI.enableFeatures and UI.disableFeatures, restructure it with the Modular UI, and theme it with UI.setTheme.
  • WebViewer serves static assets (WebAssembly modules, HTML, CSS) that you copy into your app, and runs in demo mode without a licenseKey. Some features are licensed separately, such as Page Manipulation and the content editing add-on.
  • Form widgets, stamps, and markup are annotation objects managed by annotationManager, exported as XFDF, and merged into the PDF on save with doc.getFileData({ xfdfString }). Collaboration is XFDF that your own server syncs between clients.
  • Pages are 1-based numbers, and annotation positions are viewer coordinates with the origin at the top left of the page.

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

  • There are no prebuilt toolbars, panels, or dialogs. Viewport is the main view and PageCanvas renders single pages; everything around them is your React code driven by hooks.
  • PdfProviders hosts the state and configuration inside your React tree. The SDK renders unstyled, and your design system supplies the look through the theme configuration.
  • Edits don't rewrite the PDF file. Each change is an operation in a CRDT, which powers the history; useDocumentSaving builds the final PDF when you need it.
  • Pages and blocks are addressed by session-scoped ids, and positions are user-space units with the origin at the bottom-left corner of the page.
ConcernWebViewer@sdk-pdf/react
UIPrebuilt, customized from outside (disableElements, Modular UI)Viewport and PageCanvas only; you build toolbars, panels, and dialogs
MountingWebViewer(options, element) in a useEffect, Web ComponentPdfProviders and components in your React tree
DeploymentStatic assets under path, licenseKey, optional WebViewer ServerThe package from a private registry
State accessinstance.Core managers and their eventsState hooks that re-render, command hooks, and event hooks
Pages1-based numbersSession-scoped page ids
FormsFields by full name (FieldManager), widgets as annotationsWidgets with a fieldName, values bound with useWidgetValue
Editing textContent editing add-onBuilt in under the PDF_EDITOR content policy
PersistenceXFDF for annotations, getFileData for the PDFprepareDocument() for the PDF
File typesPDF, Office, and morePDF

What this means for your code:

  1. Build the UI. Every WebViewer UI element your users rely on becomes a React component built from the hooks in the API reference.
  2. Drop the mount effect. The WebViewer(...) promise, the instance reference, and any context that shares it are replaced by PdfProviders and hooks.
  3. Replace managers with hooks. documentViewer, annotationManager, FieldManager, and AnnotationHistoryManager calls become hook calls in the components that need them.
  4. Replace XFDF persistence. Storing annotations or field data as XFDF becomes storing the PDF produced by prepareDocument().
  5. Replace numbers and viewer coordinates with page ids and user-space coordinates; check stored positions after converting them.
  6. Map the read-only switches to content policies, and decide whether to expose editing of existing text, which is built in.
  7. Remove deployment pieces: static assets under path, the license key, and WebViewer Server settings.
API map

Rows marked No equivalent are covered in Features without an equivalent.

WebViewer@sdk-pdf/reactNotes
WebViewer({ path, licenseKey }, element)<PdfProviders>No asset path or license key.
initialDoc, UI.loadDocument(url | File | Blob, options)loadPdf(Uint8Array, { password }) from useLoadPdfRead the file or fetch the URL into bytes first.
loadDocument option passwordloadPdf(data, { password }), catch InvalidPasswordError
loadDocument options onLoadingProgress, customHeaders, withCredentialsNo equivalentHeaders and credentials go on your own fetch.
documentLoaded eventloadState.status === 'ready'
UI.closeDocument(), documentViewer.closeDocument()resetPdf() from useResetPdf
documentViewer.getPageCount()usePageCount
documentViewer.getCurrentPage(), pageNumberUpdateduseCurrentPageIdRe-renders with the id of the page in the middle of the viewport.
documentViewer.setCurrentPage(pageNumber, isSmoothScroll)scrollToPage(pageId, { behavior: 'smooth' }) from useScrollToPage
documentViewer.displayPageLocation(...)No equivalentscrollToBlock scrolls to a block, not to coordinates.
doc.insertBlankPages(insertBeforeThesePages, width, height)createPage(width, height, insertAfterPageId?) from usePageCreateInserts after, not before.
doc.removePages(pageArray)deletePages(pageIdOrIds) from usePageDelete
doc.movePages(pageArray, insertBeforeThisPage)moveBefore or moveAfter from usePageMove
doc.rotatePages(pageArray, rotation)setPageRotation(pageIdOrIds, rotation) from useSetPageRotationAbsolute degrees, not an added rotation.
—duplicatePage(sourcePageId, insertAfterPageId?) from usePageDuplicate
pagesUpdated eventusePagesRe-renders with the pages in their current order; page commands emit no events.
documentViewer.zoomTo(), UI.setZoomLevel(), getZoomLevel()useViewportScale
UI.setFitMode(UI.FitMode.FitWidth)useViewportAutoFitScale, useViewportFitScaleA one-time fit, not a fixed mode.
UI.FitMode.FitPageMath.min(fitWidthScale, fitHeightScale)See Zoom and fit modes.
zoomUpdated eventscale from useViewportScale in a useEffectzoomGestureEnd from useViewportEvent reports pinch and wheel zoom.
UI.setLayoutMode(UI.LayoutMode.Single) and other layout modesNo equivalent
Thumbnails panel, doc.loadThumbnail(), doc.loadCanvas()PageCanvas
annotationManager.getFieldManager().getField(name)useWidgets filtered by fieldNameWidgets, not fields.
field.setValue(value), field.getValue()useWidgetValueInside a component.
fieldChanged eventwidgetInput from useViewportEventReports values the user changes in the main view, not values set from code.
new Annotations.Forms.Field() with TextWidgetAnnotation, addField, addAnnotationcreateWidgets(props) from useWidgetsCreate
FormFieldCreationManager.startFormFieldCreationMode()The { type: 'addWidget', widgetProps } tool
documentViewer.setToolMode(), UI.setToolMode()useSetInteractionModeSee Editing and interaction tools.
ContentEditManager.startContentEditMode()The PDF_EDITOR content policy, where text is editable
isReadOnly, annotationManager.enableReadOnlyMode(), UI.enableViewOnlyMode()PDF_VIEWER or PDF_PRINTER from CONTENT_POLICY_PRESET
UI.disableElements(), Modular UI, UI.setTheme()Your own components and the theme configuration, see Theming and customization
AnnotationHistoryManager undo(), redo(), canUndo(), canRedo(), historyChangeduseUndoRedo
annotationChanged eventuseViewportEvent for user actions, useDocumentRevision for any changeIds, not annotation objects.
Unsaved-change tracking through annotationChangeduseDocumentRevision, useDocumentRevisionGetterCompare revisions to tell whether there are unsaved changes.
annotationManager.exportAnnotations(), importAnnotations()No equivalentNo XFDF.
doc.getFileData({ xfdfString })prepareDocument() from useDocumentSavingResolves with a Uint8Array, not an ArrayBuffer.
UI.downloadPdf({ filename })downloadDocument()No file name option.
flatten save optionNo equivalent
UI.Events.LOAD_ERROR, loadDocument option onErrorThe rejected loadPdf promiseSee Errors and logging.
disableLogs, Core.disableLogs(), Core.enableWorkerLogs()logMessagesThe SDK never prints to the console.
UI.dispose()No dispose call; unsubscribe log listeners yourselfSee Cleanup and lifecycle.
UI.searchText(), documentViewer.textSearchInit()useDocumentSearchSee Text search.
doc.getPDFCoordinates(), doc.getViewerCoordinates()No equivalentSee Coordinates.

Install and set up

Before: WebViewer

WebViewer installs as @pdftron/webviewer, copies its static assets into public, and mounts into a ref from an effect:

npm i @pdftron/webviewer
npx --yes cpy-cli "node_modules/@pdftron/webviewer/public/**/*" public/lib/webviewer
import { useEffect, useRef } from 'react'
import WebViewer from '@pdftron/webviewer'

function App() {
  const viewerRef = useRef(null)

  useEffect(() => {
    WebViewer(
      {
        path: '/lib/webviewer',
        licenseKey: 'YOUR_LICENSE_KEY',
        initialDoc: 'https://example.com/files/contract.pdf',
      },
      viewerRef.current
    ).then((instance) => {
      // const { UI, Core } = instance
    })
  }, [])

  return <div ref={viewerRef} style={{ height: '100vh', width: '100%' }} />
}

Under React Strict Mode the effect runs twice in development, so WebViewer integrations add a hasMounted ref guard.

After: @sdk-pdf/react

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

@sdk-pdf:registry=https://registry.sdk.pdf.net
//registry.sdk.pdf.net/:_authToken=${PDF_NET_REGISTRY_TOKEN}
npm install @sdk-pdf/react

Set up the project as Getting started describes: import the stylesheet, render the SDK on the client only, alias canvas in the bundler, and turn off React Strict Mode.

Mount PdfProviders in your tree. Its props must keep their identity between renders, so memoize the configuration:

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

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

  return (
    <PdfProviders {...config}>
      <DocumentLoader />
    </PdfProviders>
  )
}

What to know

  • No assets, no license key in code. The install is the registry scope and the package; there is no asset folder to copy and no path or licenseKey to pass.
  • No mount effect. The SDK renders as components, so the useRef and useEffect mount, the Strict Mode guard, and sharing the instance through React context go away: any component under PdfProviders reaches the document through hooks.
  • Both default to editable. WebViewer's isReadOnly defaults to false, and PdfProviders defaults to PDF_EDITOR, where text, annotations, links, and widgets are all editable. Map your read-only flag to initialContentPolicy, as above. The initial policy applies when the viewport first mounts; switch at runtime with useSetInteractionMode, as in Editing and interaction tools.

Load and reset a document

Before: WebViewer

UI.loadDocument(documentPath, options, documentViewerKey) takes a URL, File, Blob, Core.Document, or Core.PDFNet.PDFDoc. Its options include filename, extension, password (a string, or a function that provides it through a callback), onLoadingProgress (for network loads only), and onError. The document is usable after documentLoaded:

const { documentViewer } = instance.Core

documentViewer.addEventListener('documentLoaded', () => {
  showDocumentUi()
})

instance.UI.loadDocument('https://example.com/files/contract.pdf', {
  filename: 'contract.pdf',
  password: enteredPassword,
  // Only for files loaded over network.
  onLoadingProgress: (percent) => showProgress(percent),
  onError: (error) => showError(error),
})

loadDocument closes the current document itself, so there is no need to close it before loading another one; UI.closeDocument() closes it explicitly.

After: @sdk-pdf/react

loadPdf from useLoadPdf takes the file bytes. A File from an input becomes a Uint8Array first, and the load state decides what to render:

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

function DocumentLoader() {
  const [loadState, loadPdf] = useLoadPdf()

  async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.currentTarget.files?.[0]
    if (!file) return

    const data = new Uint8Array(await file.arrayBuffer())

    try {
      // loadPdf reads from the buffer it receives, so keep `data` for a retry and pass a
      // copy.
      await loadPdf(data.slice())
    } catch (error) {
      if (error instanceof InvalidPasswordError) {
        // The password is missing or wrong: ask for it, then call
        // loadPdf(data.slice(), { password }) again.
      }
    }
  }

  if (loadState.status === 'initial') {
    return <input type="file" accept=".pdf" onChange={handleFileChange} />
  }

  if (loadState.status === 'loading') {
    return <p>Loading the document…</p>
  }

  return <PdfApplication />
}

function CloseDocumentButton() {
  const resetPdf = useResetPdf()

  // Replaces UI.closeDocument(): the load state returns to 'initial'.
  return <button onClick={() => resetPdf()}>Close</button>
}

For a URL, fetch it and pass new Uint8Array(await response.arrayBuffer()).

What to know

  • PDF only. useLoadPdf loads PDF documents; there is no Office, image, or XOD input, and no extension or filename to set.
  • Keep a copy for retries. The document reads from the given buffer for its whole lifetime, and another loadPdf call can detach it. To reuse the bytes for a password retry, pass a copy: loadPdf(data.slice(), { password }).
  • Without a password, the empty password is tried, as the PDF specification prescribes; a missing or wrong password rejects with InvalidPasswordError. Instead of a password function, catch the rejection and call loadPdf again.
  • No progress or multi-tab loading. There is no onLoadingProgress, and no equivalent of passing several documents to initialDoc for multi-tab mode.
  • 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, call resetPdf() and then loadPdf.

Display the document

Before: WebViewer

The prebuilt UI renders the document, toolbars, and panels; integrations trim and theme it from outside:

instance.UI.disableElements(['leftPanel', 'leftPanelButton'])
instance.UI.setTheme(instance.UI.Theme.DARK)

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: 'my-viewport', page: 'my-page' },
  styles: {
    scrollViewport: { '--selection-color': 'rebeccapurple' } as React.CSSProperties,
  },
}

function PdfApplication() {
  // Fit the width once per loaded document, like opening in FitWidth.
  useViewportAutoFitScale('width')

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', height: '100vh' }}>
      <ThumbnailsSidebar />
      <div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', minHeight: 0 }}>
        {/* Your toolbar: the navigation, zoom, and history controls from this guide. */}
        <Toolbar />
        {/* 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. disableElements, enableFeatures, and the Modular UI configuration have no counterpart. Selection, text editing, form filling, links, and zoom gestures work inside Viewport and are limited by the content policy; every control around it exists only if you build it.
  • Theming is CSS you own. theme.classNames and theme.styles target the SDK's elements (scrollViewport, page, pageCanvas, resizable, widgetOverlay, and more), and CSS variables such as --selection-color and --widget-overlay-background tune the selection and widget visuals; Theming and customization lists them. There are no built-in light and dark themes.
  • Render only when ready. Viewport throws DocumentNotLoadedError when no document is loaded.
  • One layout. UI.setLayoutMode with Single, Continuous, Facing, and the facing and cover variants, and DisplayModeManager.setDisplayMode on DocumentViewer, have no equivalent: Viewport shows the pages in one scrollable area.

Pages and navigation

Before: WebViewer

DocumentViewer navigates by 1-based page number and reports changes with pageNumberUpdated:

const { documentViewer } = instance.Core

pageCountLabel.textContent = documentViewer.getPageCount()

documentViewer.addEventListener('pageNumberUpdated', (pageNumber) => {
  pageSelect.value = pageNumber
})

pageSelect.onchange = () => documentViewer.setCurrentPage(Number(pageSelect.value), true)

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 replaces pageNumberUpdated:

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

function PagePicker() {
  const pages = usePages()
  // The page in the middle of the viewport; undefined until the viewport has one.
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()

  return (
    <select
      value={currentPageId ?? pages[0]?.id}
      onChange={(event) =>
        scrollToPage(event.currentTarget.value, { behavior: 'smooth' })
      }
    >
      {pages.map((page, index) => (
        <option key={page.id} value={page.id}>
          Page {index + 1} of {pages.length}
        </option>
      ))}
    </select>
  )
}

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.

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.
  • Scroll options. scrollToPage aligns the page with vertical ('top' by default) and horizontal ('nearest' by default); isSmoothScroll becomes behavior: 'smooth'.
  • Scrolling to a spot. displayPageLocation(pageNumber, horizontalPosition, verticalPosition) has no equivalent. scrollToBlock(blockId, options?) from useScrollToBlock scrolls to a block, such as a form widget.

Page manipulation

Before: WebViewer

Core.Document changes pages by number. These methods work on PDF documents only, resolve with a PageUpdateResult, and are licensed as part of Page Manipulation:

const doc = instance.Core.documentViewer.getDocument()

// Inserts blank pages before the given pages; the size is in PDF points.
await doc.insertBlankPages([2], 612, 792)
await doc.removePages([4])
await doc.movePages([5], 1)
// Adds the rotation to the pages' current rotation.
await doc.rotatePages([1], instance.Core.PageRotation.E_90)

After: @sdk-pdf/react

Each page operation is a hook that returns a command taking page ids: usePageCreate, usePageDelete, usePageMove, useSetPageRotation, and usePageDuplicate:

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

function PageActions({ pageId }: { pageId: string }) {
  const pages = usePages()
  const createPage = usePageCreate()
  const deletePages = usePageDelete()
  const duplicatePage = usePageDuplicate()
  const { moveBefore } = usePageMove()
  const setPageRotation = useSetPageRotation()

  const page = pages.find((page) => page.id === pageId)
  if (!page) return null

  return (
    <div role="toolbar">
      {/* A4 is roughly 595 × 842 user-space units; the new page goes after this one. */}
      <button onClick={() => createPage(595, 842, pageId)}>
        Insert blank page after
      </button>
      <button disabled={pages.length <= 1} onClick={() => deletePages(pageId)}>
        Delete
      </button>
      <button onClick={() => void duplicatePage(pageId)}>Duplicate</button>
      {/* Without a target page, moveBefore inserts at the start of the document. */}
      <button onClick={() => moveBefore(pageId)}>Move to start</button>
      {/* Rotation is absolute: add to the current angle yourself. */}
      <button onClick={() => setPageRotation(pageId, page.rotation + 90)}>
        Rotate clockwise
      </button>
    </div>
  )
}

What to know

OperationWebViewer@sdk-pdf/react
InsertBefore given page numbers; defaults 612 × 792 PDF pointsAfter insertAfterPageId, or appended; width and height in user-space units (1/72 of an inch), no defaults
MoveBefore insertBeforeThisPagemoveBefore(ids, beforePageId?) (start when omitted) or moveAfter(ids, afterPageId?) (end when omitted)
RotateAdds a Core.PageRotation to the current rotationSets an absolute angle; -90 and 450 are normalized; angles off a quarter turn throw InvalidInputError
Duplicate—duplicatePage is asynchronous and inserts after the source by default
  • 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 commands emit no events: track them where you call them. usePages re-renders with the current pages and their order, which covers what pagesUpdated reported.
  • Extracting pages. The counterpart of extractPages is prepareDocument({ pageIds }) from useDocumentSaving, which saves only the listed pages, in the listed order.
  • No page import, merging, or cropping. insertPages, mergeDocument, and cropPages have no equivalent.

Zoom and fit modes

Before: WebViewer

Zoom is a number or a percentage, and a fit mode fixes the zoom: FitWidth fits the width of the page, and FitPage fits the width or the height, whichever is smaller. Both calls need a loaded document:

const { documentViewer } = instance.Core

documentViewer.addEventListener('documentLoaded', () => {
  instance.UI.setFitMode(instance.UI.FitMode.FitWidth)
  // or a fixed zoom: instance.UI.setZoomLevel('150%')
})

documentViewer.addEventListener('zoomUpdated', (zoom) => {
  persistZoom(zoom)
})

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()

  // Every scale change, whatever the source, like zoomUpdated.
  useEffect(() => persistZoom(scale), [scale])

  function fitPage() {
    const { fitWidthScale, fitHeightScale } = getFitScale()
    // The smaller scale fits the pages in both directions, like FitPage.
    setScale(Math.min(fitWidthScale, fitHeightScale))
  }

  return (
    <div role="toolbar">
      <button onClick={() => setScale(scale / 1.25)}>−</button>
      <span>{Math.round(scale * 100)}%</span>
      <button onClick={() => setScale(scale * 1.25)}>+</button>
      <button onClick={() => setScale(getFitScale().fitWidthScale)}>Fit width</button>
      <button onClick={fitPage}>Fit page</button>
    </div>
  )
}

What to know

  • Fits are one-time calculations. useViewportAutoFitScale applies when a document loads and doesn't re-apply on viewport resizes or manual zooming; useViewportFitScale reflects the viewport size at the moment of the call. There is no fit mode that stays applied as the viewport resizes.
  • What the fits measure. fitWidthScale fits the widest page into the viewport width and fitHeightScale the tallest page into its height, both accounting for viewportPadding.
  • Scale 1 is 100%: one user-space unit per CSS pixel. setScale takes a positive finite number, not a percentage string.
  • Zoom around a point. setScale(scale, pivot?) keeps the document still at pivot, a point { x, y } given as fractions of the viewport area (0 the top or left edge, 1 the bottom or right edge), the center by default. The third item of useViewportScale(), animateScale(scale, pivot?), animates the change and resolves when it ends.
  • Gestures. Pinch and wheel zoom are built in and tuned with zoomGestures (minScale, maxScale, wheel speed) on PdfProviders. During a gesture, scale keeps its value until the gesture ends; zoomGestureStart and zoomGestureEnd from useViewportEvent mark it.

Thumbnails

Before: WebViewer

The prebuilt UI has a thumbnails tab in the left panel with page operations; UI.Feature.ThumbnailReordering and ThumbnailMerging toggle reordering and merging. Custom thumbnails come from loadThumbnail:

const { documentViewer } = instance.Core

documentViewer.addEventListener('documentLoaded', () => {
  const doc = documentViewer.getDocument()
  doc.loadThumbnail(1, (thumbnail) => {
    // thumbnail is a HTMLCanvasElement or HTMLImageElement
    thumbnailContainer.appendChild(thumbnail)
  })
})

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. A sidebar with reordering buttons:

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

function ThumbnailsSidebar() {
  const pages = usePages()
  const scrollToPage = useScrollToPage()
  const { moveBefore, moveAfter } = usePageMove()

  return (
    <nav>
      {pages.map((page, index) => (
        <div key={page.id}>
          <button onClick={() => scrollToPage(page.id)}>
            <PageCanvas
              pageId={page.id}
              className="thumbnail"
              role="img"
              aria-label={`Thumbnail ${index + 1}`}
            />
          </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>
        </div>
      ))}
    </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 a virtualized list, pass isVisible.
  • Annotations included. PageCanvas draws annotations, form widgets included, unless hideAnnotations is set; there is no separate draw step as with loadCanvas and drawAnnotations.
  • Thumbnails that show session edits are coming soon. PageCanvas draws the page as loaded from the file; the main view shows the edits. Rotation is followed live.
  • No merging. Dropping another file into the thumbnails (ThumbnailMerging) has no equivalent.

Forms

Before: WebViewer

Fields are looked up by full name through the field manager, and widgets are annotations:

const { documentViewer, annotationManager } = instance.Core

documentViewer.addEventListener('annotationsLoaded', () => {
  // getField returns null when no field has that full name.
  const field = annotationManager.getFieldManager().getField('customer.name')
  if (field) field.setValue('Jane Doe')
})

annotationManager.addEventListener('fieldChanged', (field, value) => {
  autosave(field.name, value)
})

Creating a field takes a Field, a widget annotation positioned in viewer coordinates, and both managers:

const { documentViewer, annotationManager, Annotations } = instance.Core

documentViewer.addEventListener('documentLoaded', () => {
  const field = new Annotations.Forms.Field('customer.name', { type: 'Tx' })
  const widgetAnnot = new Annotations.TextWidgetAnnotation(field)
  widgetAnnot.PageNumber = 1
  widgetAnnot.X = 100
  widgetAnnot.Y = 100
  widgetAnnot.Width = 200
  widgetAnnot.Height = 50

  annotationManager.getFieldManager().addField(field)
  annotationManager.addAnnotation(widgetAnnot)
  annotationManager.drawAnnotationsFromList([widgetAnnot])
})

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 {
  TextWidget,
  useWidgets,
  useWidgetsCreate,
  useWidgetValue,
  Widget,
} from '@sdk-pdf/react'

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

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

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

function TextField({ widget }: { widget: TextWidget }) {
  // Like a useState backed by the document: 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 useAddNameField() {
  const createWidgets = useWidgetsCreate()

  return async (pageId: string) => {
    const [widget] = await createWidgets({
      pageId,
      widgetType: 'text',
      // User-space units: the origin is at the bottom-left corner of the page.
      x: 72,
      y: 700,
      width: 180,
      height: 24,
      fieldName: 'customer.name',
    })
    return widget
  }
}

From fields to widgets

ConceptWebViewer@sdk-pdf/react
LookupgetField(name), getFields()useWidgets() (re-renders), or await getWidgets() from useWidgetsGetter (one-time, loads every page with widgets first)
Shared valuesA field with several widgetsEvery widget with the same field name shows and synchronizes the same value
Valuesfield.setValue(value), getValue()useWidgetValue(widget): string for text and choice, boolean for checkbox and radio, option indices for multiChoice
Change eventsfieldChanged(field, value)widgetInput from useViewportEvent reports a value the user changed in the main view; values set from code, undo, and redo emit nothing, and useWidgetValue re-renders on any change
CreationField, widget annotation, addField, addAnnotationcreateWidgets(props) from useWidgetsCreate; required: pageId, widgetType, x, y, width, height; a unique fieldName is generated when omitted
Interactive creationFormFieldCreationManager.startFormFieldCreationMode()The { type: 'addWidget', widgetProps } interaction tool, under an editable widgets policy
DeletionannotationManager.deleteAnnotations()useBlockDelete with widget ids; same-named widgets act as one field
PropertiesWidget annotation properties and flagssetWidgetProps(ids, props, options?) from useSetWidgetProps: isReadOnly, borderColor, fillColor, fontSize, isMultiline, options, and more
Form dataXFDF export and importNo equivalent

What to know

  • Coordinates differ. WebViewer widget X and Y are viewer coordinates with the origin at the top left of the page; @sdk-pdf/react widget positions are user-space units with the origin at the bottom left. Check placement on a test document before converting stored positions.
  • Values are read and written per widget. useWidgetValue binds one widget per hook call, and there is no setter by field name: to prefill a form, render a bound component per widget, like TextFieldByName above.
  • Read-only fields. isReadOnly restricts filling the field, not authoring the widget; with { excludeFromHistory: true }, an undo can't unlock a submitted form.
  • Date fields. A text widget's format exposes Acrobat date formats as FormatType.DateTime, recognized without running scripts; see FormatType. Fields driven by other scripts read as plain text fields.
  • Signatures. signature widgets carry a read-only isCryptoSigned; the SDK never signs.

Editing and interaction tools

Before: WebViewer

Tools are switched by name, content editing is an add-on mode, and read-only is a switch on the annotation manager or the UI:

const { documentViewer, annotationManager, Tools } = instance.Core

documentViewer.setToolMode(documentViewer.getTool(Tools.ToolNames.EDIT))
instance.UI.setToolMode(Tools.ToolNames.TEXT_SELECT)

// Content editing is a separately licensed add-on.
instance.UI.enableFeatures([instance.UI.Feature.ContentEdit])
documentViewer.getContentEditManager().startContentEditMode()

// No annotations can be added or commented on.
annotationManager.enableReadOnlyMode()
// Disables all editing features in the UI.
instance.UI.enableViewOnlyMode()

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:

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 UI.enableViewOnlyMode(): read-only text, interactive links and widgets. */}
      <button
        aria-pressed={interactionTool.type === 'textSelect'}
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_VIEWER)
        }
      >
        View
      </button>
      {/* Edit mode: select, move, and resize blocks, with editable text. */}
      <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 paragraph
      </button>
    </div>
  )
}

Mapping the tools

Core.Tools.ToolNames and modes@sdk-pdf/react
EDIT{ type: 'select' }: select, move, and resize blocks.
TEXT_SELECT{ type: 'textSelect' }: native text selection for reading and copying; needs read-only text.
CONTENT_EDIT and startContentEditMode()Text is editable in place when the policy's text level is 'editable' (PDF_EDITOR); there is no separate mode or add-on.
ADD_PARAGRAPH{ type: 'addText' }: creates a paragraph at the click position and focuses it for typing; needs editable text.
ADD_IMAGE_CONTENT, STAMP with an image{ type: 'addImage', image, width?, height? } with await stageImage(blob) places the image as a stamp annotation, not page content; the optional size is in user-space units, and a single dimension keeps the aspect ratio. See stageImage; useStampAnnotationCreate places one from code.
FormFieldCreationManager{ type: 'addWidget', widgetProps }, under an editable widgets policy.
PANNo tool: Viewport scrolls and zooms on its own.
FREETEXTThe nearest tool is addText (see ADD_PARAGRAPH), which creates a text paragraph and needs editable text.
FREEHANDNo drawing tool. useInkAnnotationCreate creates an ink annotation from strokes your code collects.
RECTANGLE, HIGHLIGHT, STICKY, SIGNATURE, REDACTION, measurement toolsNo equivalent.
isReadOnly, enableReadOnlyMode(), UI.enableViewOnlyMode()CONTENT_POLICY_PRESET.PDF_VIEWER (widgets stay fillable) or PDF_PRINTER (widgets read-only too).

What to know

  • Switch the policy with the tool. Pass PDF_EDITOR with the editing tools, as above. textSelect needs read-only text: with editable text, the combination is unsupported and reports a message through logMessages.
  • Per-kind permissions. A custom content policy is a plain object with text, annotations, links, and widgets levels, finer than a single read-only switch. 'hidden' doesn't render that kind of content at all, and a level can be a function of the block id, such as widgets: (blockId) => (lockedIds.has(blockId) ? 'readonly' : 'interactive'). There is no hook to read the policy back: keep the mode you set in your own state to show or hide editing UI.
  • Local mode. Switching the mode affects this client only.
  • Selection toolbars. useSelectionTextStyle (font, size, color, bold, italic, alignment, spacing) and useSelectionAnnotationProps (border color and width) replace the style popups of the prebuilt UI. For widget settings such as multiline, read the selected widgets from useSelectedBlocks and change them with setWidgetProps from useSetWidgetProps. useSelection tells a paragraph in text editing from selected blocks.
  • Programmatic text edits. useProgrammaticEditor reads paragraphs and rewrites them without UI.

Undo, redo, and change tracking

Before: WebViewer

AnnotationHistoryManager handles undo and redo of annotation changes and emits historyChanged. Change tracking listens to annotationChanged and skips imported changes:

const { documentViewer, annotationManager } = instance.Core
const historyManager = documentViewer.getAnnotationHistoryManager()

undoButton.onclick = () => historyManager.undo()
redoButton.onclick = () => historyManager.redo()

historyManager.addEventListener('historyChanged', () => {
  undoButton.disabled = !historyManager.canUndo()
  redoButton.disabled = !historyManager.canRedo()
})

annotationManager.addEventListener(
  'annotationChanged',
  (annotations, action, { imported }) => {
    if (imported) return
    setUnsavedChanges(true)
  }
)

After: @sdk-pdf/react

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

function HistoryToolbar() {
  // canUndo and canRedo re-render the component as the history changes, like
  // historyChanged.
  const { undo, redo, canUndo, canRedo } = useUndoRedo()

  useHotkeys(useMemo(() => ({ 'Mod-z': undo, 'Shift-Mod-z': redo }), [undo, redo]))

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

Unsaved-change tracking moves to 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 indicator lives with the save button in Saving and downloading.

What to know

  • Beyond annotations. useUndoRedo covers the document: deleting, moving, or rotating pages are undo steps, widget property changes are undo steps unless excluded, and form values follow undo and redo.
  • Local and merged. Undo reverts the local user's changes, and edits within 500 ms merge into one step, even across hook calls. To keep an operation a separate step, call markUndoBoundary() from useUndoBoundary before it starts and after its edits finish.
  • Events for user actions only. Where annotationChanged fed analytics or side effects, useViewportEvent reports what users do in the main view, such as textEditorInput, addToolApplied, blocksDragEnd, and widgetInput, with block ids rather than annotation objects. Commands your code calls, undo, and redo emit no events, so there is no imported flag to check: track your own calls where you make them, and watch useDocumentRevision for any change.
  • No history reset. AnnotationHistoryManager.clear() has no equivalent; a save only moves the revision you compare against.

Saving and downloading

Before: WebViewer

Annotations are exported as XFDF and merged into the file data; getFileData resolves with an ArrayBuffer:

const { documentViewer, annotationManager } = instance.Core

const doc = documentViewer.getDocument()
const xfdfString = await annotationManager.exportAnnotations()
const data = await doc.getFileData({ xfdfString })
const blob = new Blob([new Uint8Array(data)], { type: 'application/pdf' })

const body = new FormData()
body.append('mydoc.pdf', blob, 'mydoc.pdf')
await fetch('/api/documents', { method: 'POST', body })

// Or let the UI download it.
await instance.UI.downloadPdf({ filename: 'contract.pdf' })

After: @sdk-pdf/react

useDocumentSaving applies the recorded changes to the original document; there is no separate annotation layer to export:

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

function SaveButtons() {
  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 annotationChanged-based tracking: undoing back to the saved state clears it
  // too.
  const hasUnsavedChanges = revision !== savedRevision

  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()
      // The buffer goes to fetch, so don't hand out the loaded document's original
      // buffer.
      const bytes = await prepareDocument({ returnOriginalIfUnchanged: false })

      const body = new FormData()
      body.append(
        'mydoc.pdf',
        new Blob([bytes as BlobPart], { type: 'application/pdf' }),
        'mydoc.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>
  )
}

One component owns the saved revision, so the save and the indicator compare against the same value. Render it only once the document is ready, so its first revision is the loaded document's.

What to know

  • One artifact. WebViewer lets you store XFDF apart from the PDF, or post each change as an XFDF command. @sdk-pdf/react saves the complete PDF: prepareDocument() resolves with its bytes as a Uint8Array, and downloadDocument() saves the same bytes on the user's device.
  • returnOriginalIfUnchanged defaults to true, returning the buffer passed to loadPdf when nothing changed; set it to false when the bytes will be transferred or mutated.
  • Save options. Besides returnOriginalIfUnchanged, pageIds saves only the listed pages, in the listed order. There is no download file name, flattening, linearization (Core.SaveOptions.LINEARIZED), or encryption on save.

Collaboration

WebViewer leaves collaboration to you: your server syncs XFDF between clients, optionally as XFDF commands from exportAnnotationCommand.

Real-time collaboration in @sdk-pdf/react is coming soon. Your XFDF sync code doesn't carry over: edits are CRDT operations, not annotation exports.

Coordinates

TopicWebViewer@sdk-pdf/react
Page reference1-based page numbersSession-scoped page ids; the position is the index in usePages()
OriginViewer coordinates: top left of the page, y pointing down; PDF coordinates: bottom leftUser space with the origin at the bottom-left corner of the page
UnitsPDF points for insertBlankPagesUser-space units, one unit is 1/72 of an inch
Page sizedocumentViewer.getPageWidth(), getPageHeight(), with rotation appliedwidth and height of the usePages() entries, with rotation applied
ConversiongetPDFCoordinates, getViewerCoordinatesNo conversion helpers

Before: WebViewer

The prebuilt UI has a search panel, and UI.searchText runs a search from code with options such as caseSensitive, wholeWord, and regex:

instance.UI.searchText('invoice', { caseSensitive: true, wholeWord: true })

After: @sdk-pdf/react

useDocumentSearch holds the query, the options, and the matches; the SDK highlights the matches on the pages and scrolls to the active one, and the search panel is yours:

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

function SearchPanel() {
  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 word
      </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>
  )
}

What to know

  • Options. isCaseSensitive and isWholeWord replace caseSensitive and wholeWord; regular expressions and wildcards have no equivalent.
  • Matches. Each match carries id, pageId, paragraphId, and the matched text, in document order; 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.
  • Styling. The highlights take their look from the searchMatch key of theme.classNames and theme.styles, as Theming and customization describes.
  • No selection API. getSelectedText has no equivalent.

Errors and logging

Before: WebViewer

Load failures arrive through the loaderror UI event or the onError option, and logging is switched off with disableLogs or Core.disableLogs:

instance.UI.addEventListener(instance.UI.Events.LOAD_ERROR, (error) => {
  showError(error)
})

instance.Core.disableLogs(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 useOpenDocument() {
  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 the LOAD_ERROR listener 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), and UninitializedError, all extending GenericError with a static message and an optional data object.
  • Nothing half-applied. Commands validate first and throw before touching anything; readers return empty values instead of throwing; hooks never throw during render.
  • Logs replace console output. Where WebViewer logs to the console unless disabled, @sdk-pdf/react delivers level, reason, a static message, data, and stack to your subscriber. There is no UI.displayErrorMessage: error UI is yours.

Cleanup and lifecycle

Before: WebViewer

UI.dispose() cleans up the instance's listeners and data and belongs in the code that removes WebViewer from the DOM. In a React effect, the cleanup also has to handle an unmount that happens before WebViewer() resolves:

useEffect(() => {
  let instance
  let isUnmounted = false

  WebViewer({ path: '/lib/webviewer', licenseKey }, viewerRef.current).then((created) => {
    instance = created
    // The component may unmount before WebViewer resolves, such as under Strict Mode.
    if (isUnmounted) instance.UI.dispose()
  })

  return () => {
    isUnmounted = true
    instance?.UI.dispose()
  }
}, [])

After: @sdk-pdf/react

  • Subscriptions follow components. useViewportEvent subscribes for the component's lifetime, with callbacks that don't need memoizing. useHotkeys installs window-level shortcuts, and passing a falsy value instead of the mapping disables them, such as while a modal is open.
  • Log subscriptions return an unsubscribe function.
  • Closing a document is resetPdf() from useResetPdf, which returns the load state to 'initial'.

Features without an equivalent

Plan around these before you migrate:

WebViewer featureIn @sdk-pdf/react
Reading the selected text (getSelectedText)No equivalent
Creating markup and shape annotations, sticky notes, free text, and measurements; drawing inkNo equivalent; existing shapes, lines, and ink display, and their border color and width can be changed. Ink can be created from code with useInkAnnotationCreate
Redaction, digital signaturesNo equivalent; the SDK never signs
XFDF import and export, annotation commands, flatteningNo equivalent
Coordinate conversion (getPDFCoordinates, getViewerCoordinates)No equivalent
Office, image, and XOD files; multi-tab modePDF only
Layout modes, fixed fit modes, displayPageLocationNo equivalent
Inserting pages from another document, merging, croppingNo equivalent; prepareDocument({ pageIds }) extracts pages
Load progress, custom headers, credentialsUse your own fetch; no progress
Built-in UI, themes, localizationThe UI is yours
WebViewer ServerNo equivalent

On this page