# Building a PDF application

Source: https://sdk.pdf.net/docs/v1/guides/building-a-pdf-application



PDF applications are assembled from the same recurring parts: the main view showing the pages, a toolbar, sidebars, and a selection toolbar for the current selection. This page maps those parts to the SDK's API; the reference pages linked from each section carry the details.

## The main view [#the-main-view]

`Viewport` is the SDK's main component: the scrollable and zoomable area that renders the pages and handles direct interaction with them, from selecting and typing to filling form fields. You place it, size it, and drive everything else through hooks. See [Getting started](https://sdk.pdf.net/docs/v1/guides/getting-started) for the setup and [Viewport](https://sdk.pdf.net/docs/v1/reference/Viewport) for its details.

### Styling and customization [#styling-and-customization]

The viewport's appearance is not fixed: the theme configuration attaches your class names and inline styles to the elements the SDK renders (the viewport, pages, selection, widget overlays) and replaces some of them with your own components. See [Theming and customization](https://sdk.pdf.net/docs/v1/guides/theming-and-customization).

```tsx
import { Config, PdfProviders } from '@sdk-pdf/react'

const theme: Config['theme'] = {
  // The gap between the pages and the viewport edges, in pixels. Follows the CSS padding
  // shorthand syntax.
  viewportPadding: [24, 16],
  // Appended to the class names of the SDK elements.
  classNames: { scrollViewport: 'my-viewport', page: 'my-page' },
  // The viewport exposes CSS variables: --selection-color paints the selection outlines
  // and handles.
  styles: {
    scrollViewport: { '--selection-color': 'rebeccapurple' } as React.CSSProperties,
  },
}

export function App() {
  return (
    <PdfProviders theme={theme}>
      <DocumentLoader />
    </PdfProviders>
  )
}
```

## Interaction tool and content policy [#interaction-tool-and-content-policy]

What users may do in the main view is governed by the **interaction mode**, which consists of two parts: the **content policy** and the **interaction tool**. See [useSetInteractionMode](https://sdk.pdf.net/docs/v1/reference/useSetInteractionMode) and [CONTENT_POLICY_PRESET](https://sdk.pdf.net/docs/v1/reference/CONTENT_POLICY_PRESET) for more details.

The **content policy** sets a permission level for each content kind: for example, whether `text` is editable or read-only, or whether `links` are interactive or editable. `CONTENT_POLICY_PRESET` covers the common combinations, such as `PDF_PRINTER` (everything read-only), `PDF_VIEWER` (text read-only, links and form fields interactive), and `PDF_EDITOR` (everything editable).

The **interaction tool** interprets user gestures, that is, what a click, tap, or drag does on a page. For example:

* `{ type: 'select' }`: select, move, and resize blocks;
* `{ type: 'textSelect' }`: native text selection for reading and copying;
* `{ type: 'addText' }`: create a new paragraph at the click position and focus it for typing;
* `{ type: 'addWidget', widgetProps }`: create a form widget at the click position;
* `{ type: 'addImage', image }`: place a staged image at the click position.

A permission can also be a function of the block id, which sets it per block: for example, keeping every form field read-only except the ones assigned to the current user.

The interaction mode is dynamic: it can be changed at runtime (for example, toggling between reading and editing), and it is local to each user. `useSetInteractionMode` returns a setter that changes the tool and, optionally, the policy in one go. For example, a toggle between previewing and editing:

```tsx
import {
  CONTENT_POLICY_PRESET,
  InteractionTool,
  useSetInteractionMode,
} from '@sdk-pdf/react'

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

function ModeSwitch() {
  const setInteractionMode = useSetInteractionMode()

  return (
    <div role="toolbar">
      <button
        onClick={() =>
          setInteractionMode(textSelectTool, CONTENT_POLICY_PRESET.PDF_PRINTER)
        }
      >
        Preview
      </button>
      <button
        onClick={() => setInteractionMode(selectTool, CONTENT_POLICY_PRESET.PDF_EDITOR)}
      >
        Edit
      </button>
    </div>
  )
}
```

The initial mode is part of the `PdfProviders` configuration, as shown in [Getting started](https://sdk.pdf.net/docs/v1/guides/getting-started).

## History tracking and document export [#history-tracking-and-document-export]

Edits don't rewrite the PDF file. Each change is recorded as an operation in a CRDT (a conflict-free replicated data type), which is what powers the change history: `useUndoRedo` provides the undo and redo commands together with their availability ([useUndoRedo](https://sdk.pdf.net/docs/v1/reference/useUndoRedo)).

The final PDF is built only when needed: `useDocumentSaving` ([reference](https://sdk.pdf.net/docs/v1/reference/useDocumentSaving)) applies the recorded changes to the original document, with `downloadDocument` saving the result as a file on the user's device and `prepareDocument` returning the bytes for your own handling, such as uploading to a server.

```tsx
import { useDocumentSaving, useUndoRedo } from '@sdk-pdf/react'

function DocumentControls() {
  const { undo, redo, canUndo, canRedo } = useUndoRedo()
  const { downloadDocument } = useDocumentSaving()

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

## Hotkeys [#hotkeys]

`useHotkeys` installs window-level keyboard shortcuts. Keys are written as modifier chains ending with the key name, such as `Mod-z` or `Shift-Mod-z`, where `Mod` means Cmd on macOS and Ctrl elsewhere. A handled shortcut consumes the event; return `false` from a handler to let the event propagate instead. Passing a falsy value instead of a mapping disables the hotkeys:

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

function useDocumentHotkeys() {
  const { undo, redo } = 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]
    )
  )
}
```

## Pages and navigation [#pages-and-navigation]

The API addresses pages by id rather than by index: an id keeps pointing at the same page while pages are added, removed, and rearranged by the user, where a plain index would silently drift to another page. `usePages` bridges the two views: it returns the pages in document order (so a position like "page 3 of 12" is an index in that list) and re-renders your component when the order changes. `useCurrentPageId` returns the page the user is currently looking at.

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

function PagePicker() {
  const pages = usePages()
  const currentPageId = useCurrentPageId()
  const scrollToPage = useScrollToPage()

  return (
    <select
      value={currentPageId}
      onChange={(event) => scrollToPage(event.currentTarget.value)}
    >
      {pages.map((page, index) => (
        <option key={page.id} value={page.id}>
          Page {index + 1} of {pages.length}
        </option>
      ))}
    </select>
  )
}
```

When a page's position is needed only occasionally, such as inside an event handler, `usePageIndexGetter` returns a getter that resolves a page id to its index without subscribing your component to page order changes:

```tsx
import {
  useBlockPageIdGetter,
  usePageIndexGetter,
  useViewportEvent,
} from '@sdk-pdf/react'

function useTrackAddedContent() {
  const getBlockPageId = useBlockPageIdGetter()
  const getPageIndex = usePageIndexGetter()

  useViewportEvent('addToolApplied', ({ blockId, tool }) => {
    const pageId = getBlockPageId(blockId)
    // The block or its page might be gone already: deleted by an undo or by another user.
    const pageIndex = pageId === undefined ? undefined : getPageIndex(pageId)
    if (pageIndex === undefined) return

    analytics.track('content-added', { type: tool.type, pageNumber: pageIndex + 1 })
  })
}
```

## Zoom and fit [#zoom-and-fit]

Zoom controls read and set the viewport scale with `useViewportScale` (scale 1 means 100%: one user-space unit per CSS pixel). Besides the scale and its setter, it returns `animateScale`, which zooms smoothly; both setters take an optional viewport point that stays in place while zooming. `useViewportFitScale` calculates the scales that fit the pages into the viewport width or height:

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

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

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

To apply a fit automatically when a document loads, instead of wiring it manually, use `useViewportAutoFitScale('width')` (or `'height'`), as the [Getting started](https://sdk.pdf.net/docs/v1/guides/getting-started) guide does.

The viewport also zooms on its own gestures: pinch on touch devices and the zoom wheel/trackpad gestures. They go through the same scale, so your controls and the gestures never disagree. The gesture behavior, such as the scale limits and the wheel zoom speed, is tuned through the `zoomGestures` configuration of `PdfProviders`. The scale returned by `useViewportScale` follows every change regardless of its source, so persisting the zoom level only takes reading it. The `zoomGestureStart` and `zoomGestureEnd` viewport events mark the gestures themselves.

## Thumbnails [#thumbnails]

Page thumbnails are built with `PageCanvas`: a component that renders a single page onto a canvas anywhere outside the main view. It picks the render resolution from its own displayed size (or from an explicit `scale` prop) and renders lazily, skipping the work while it is off-screen. For example, combined with `usePages` and `useScrollToPage`, it makes a navigation sidebar:

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

function ThumbnailsSidebar() {
  const pages = usePages()
  const scrollToPage = useScrollToPage()

  return (
    <nav>
      {pages.map((page, index) => (
        <button key={page.id} onClick={() => scrollToPage(page.id)}>
          {/* The render resolution follows the canvas CSS size. */}
          <PageCanvas
            pageId={page.id}
            className="thumbnail"
            role="img"
            aria-label={`Thumbnail ${index + 1}`}
          />
        </button>
      ))}
    </nav>
  )
}
```

## Selection toolbar [#selection-toolbar]

A selection toolbar shows and edits the properties of whatever is selected. The `useSelection*Props` hooks aggregate a property across the whole selection into a `{ value, set }` pair: `value` holds the common value (or `undefined` when the selected pieces disagree), and `set` applies a new value to the entire selection. For example, a text style toolbar with `useSelectionTextStyle`:

```tsx
import { useSelectionTextStyle } from '@sdk-pdf/react'

function SelectionToolbar() {
  const { isApplicable, fontFamily, isBold, isItalic, textColor } =
    useSelectionTextStyle()

  // Nothing stylable is selected.
  if (!isApplicable) return null

  return (
    <div role="toolbar">
      {/* An empty value means the selection mixes different fonts. */}
      <select
        value={fontFamily.value ?? ''}
        onChange={(event) => fontFamily.set(event.currentTarget.value)}
      >
        {fontFamily.options.regular.map((option) => (
          <option key={option.value} value={option.value}>
            {option.name}
          </option>
        ))}
      </select>
      <button aria-pressed={isBold.value} onClick={() => isBold.set(!isBold.value)}>
        Bold
      </button>
      <button aria-pressed={isItalic.value} onClick={() => isItalic.set(!isItalic.value)}>
        Italic
      </button>
      <input
        type="color"
        value={textColor.value ?? '#000000'}
        onChange={(event) => textColor.set(event.currentTarget.value)}
      />
    </div>
  )
}
```

<!-- TODO: link a "Working with selection" guide here once it is written. -->

Beyond text style, `useSelection` tells whether the user is editing text or has selected blocks, `useSelectedBlocks` returns the selected blocks as snapshots, and `useSetWidgetProps` changes the properties of selected form fields. The [Working with forms](https://sdk.pdf.net/docs/v1/guides/working-with-forms) guide builds on these; the full list is in the [Selection hooks](https://sdk.pdf.net/docs/v1/reference#selection-hooks) reference.

