Developer Guide

udoc-viewer

Universal document viewer for the web. Open-source, framework-agnostic viewer powered by a built-from-scratch WebAssembly engine for high-fidelity rendering across PDF, DOCX, PPTX, and images.

  • Truly universal — PDF, Word, PowerPoint, and images in a single viewer
  • High fidelity — powered by a custom Rust/WebAssembly rendering engine, not PDF.js
  • Client-side only — everything runs in the browser, no server round-trips
  • Framework agnostic — works with React, Vue, Angular, Svelte, or plain HTML
  • Free for commercial use — MIT-licensed wrapper, free WASM engine

Live Demo

This demo uses the UDocClient and UDocViewer API directly, exactly as documented below. For a full-featured demo with all viewer capabilities, see the full scale demo.

Installation

Install the package using your preferred package manager:

npm install @docmentis/udoc-viewer

Or with yarn/pnpm:

yarn add @docmentis/udoc-viewer
pnpm add @docmentis/udoc-viewer

Quick Start

Get up and running with the viewer in just a few lines of code:

example.ts
import { UDocClient } from '@docmentis/udoc-viewer';

// Create a client (loads the WASM engine)
const client = await UDocClient.create();

// Create a viewer attached to a container element
const viewer = await client.createViewer({
  container: '#viewer'
});

// Load a document
await viewer.load('https://example.com/document.pdf');

// Clean up when done
viewer.destroy();
client.destroy();

HTML Setup: Make sure you have a container element in your HTML:

<div id="viewer" style="width: 100%; height: 600px;"></div>

Framework Examples

HTML

index.html
<div id="viewer" style="width: 100%; height: 600px;"></div>

<script type="module">
  import { UDocClient } from "@docmentis/udoc-viewer";

  const client = await UDocClient.create();
  const viewer = await client.createViewer({ container: "#viewer" });
  await viewer.load("/path/to/document.pdf");
</script>

React

DocumentViewer.jsx
import { useEffect, useRef } from "react";
import { UDocClient } from "@docmentis/udoc-viewer";

function DocumentViewer({ src }) {
  const containerRef = useRef(null);

  useEffect(() => {
    let client, viewer;

    (async () => {
      client = await UDocClient.create();
      viewer = await client.createViewer({
        container: containerRef.current,
      });
      await viewer.load(src);
    })();

    return () => {
      viewer?.destroy();
      client?.destroy();
    };
  }, [src]);

  return <div ref={containerRef} style={{ width: "100%", height: "600px" }} />;
}

More Examples

Full working examples for every major framework are available in the examples directory:

ExampleStack
vanillaTypeScript + Vite
react-viteReact + Vite
vue-viteVue + Vite
svelte-viteSvelte 5 + Vite
angularAngular 19
nextjs-webpackNext.js + Webpack
nextjs-turbopackNext.js + Turbopack

Client API

The UDocClient is the entry point that manages the WASM engine and creates viewers.

Creating a Client

const client = await UDocClient.create({
  // License key for commercial use (optional)
  license: 'your-license-key',

  // Custom base URL for worker and WASM files (optional)
  // Expected files: {baseUrl}/worker.js and {baseUrl}/udoc_bg.wasm
  baseUrl: 'https://cdn.example.com/udoc/',

  // Locale for UI strings (optional, default: 'en')
  locale: 'en',
});

Client Options

OptionTypeDefaultDescription
licensestringLicense key for commercial use. Runs in free mode if not provided
baseUrlstringCustom base URL for worker and WASM files. Expected files: {baseUrl}/worker.js and {baseUrl}/udoc_bg.wasm
localestringenLocale for UI strings and date formatting

Viewer Options

Configure the viewer with various display and interaction options:

const viewer = await client.createViewer({
  // Container element or CSS selector (required for UI mode)
  container: '#viewer',

  // --- View modes ---
  scrollMode: 'continuous',     // 'continuous' or 'spread'
  layoutMode: 'single-page',   // 'single-page', 'double-page', etc.
  pageRotation: 0,              // 0, 90, 180, or 270
  spacingMode: 'all',           // 'all', 'none', 'spread-only', 'page-only'

  // --- Zoom ---
  zoomMode: 'fit-spread-width',
  zoom: 1,
  minZoom: 0.1,
  maxZoom: 5,

  // --- Theme ---
  theme: 'light',               // 'light', 'dark', or 'system'

  // --- Panels ---
  activePanel: null,             // 'thumbnail', 'outline', 'search', etc.
  disableSearch: false,
  disableThumbnails: false,

  // --- UI visibility ---
  hideToolbar: false,
  hideFloatingToolbar: false,
  disableFullscreen: false,
  hideAttribution: false,        // requires license with 'no_attribution'
});

All Viewer Options

OptionTypeDefaultDescription
containerstring | HTMLElementContainer element or CSS selector (required for UI mode, omit for headless)
scrollModeScrollModecontinuousScroll mode: 'continuous' or 'spread'
layoutModeLayoutModesingle-pageLayout mode: 'single-page', 'double-page', 'double-page-odd-right', 'double-page-odd-left'
pageRotationnumber0Initial page rotation: 0, 90, 180, or 270
spacingModeSpacingModeallSpacing mode: 'all', 'none', 'spread-only', 'page-only'
zoomModeZoomModefit-spread-widthZoom mode: 'fit-spread-width', 'fit-spread-width-max', 'fit-spread-height', 'fit-spread', 'custom'
zoomnumber1Initial zoom level (when zoomMode is 'custom')
zoomStepsnumber[][0.1, 0.25, 0.5, ...]Custom zoom steps for zoom in/out. Default: [0.1, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4, 5]
minZoomnumber0.1Minimum zoom level
maxZoomnumber5Maximum zoom level
pageSpacingnumber10Spacing between pages in pixels
spreadSpacingnumber20Spacing between spreads in pixels
thumbnailWidthnumber150Width of thumbnail images in pixels. Height is derived from page aspect ratio
dpinumber96Target display DPI
themestringlightColor theme: 'light', 'dark', or 'system'
disableThemeSwitchingbooleanfalseHide the theme toggle button
disableTextSelectionbooleanfalseDisable text selection and copying
googleFontsbooleantrueEnable automatic font fetching from Google Fonts for fonts not embedded in the document
activePanelstring | nullnullInitially active panel. Left: 'thumbnail', 'outline', 'bookmarks', 'layers', 'attachments'. Right: 'search', 'comments'. Or null
disableLeftPanelbooleanfalseDisable the entire left panel area
disableRightPanelbooleanfalseDisable the entire right panel area
disableThumbnailsbooleanfalseDisable the thumbnails tab
disableOutlinebooleanfalseDisable the outline tab
disableBookmarksbooleanfalseDisable the bookmarks tab
disableLayersbooleanfalseDisable the layers tab
disableAttachmentsbooleanfalseDisable the attachments tab
disableSearchbooleanfalseDisable the search panel
disableCommentsbooleanfalseDisable the comments panel
hideToolbarbooleanfalseHide the top toolbar
hideFloatingToolbarbooleanfalseHide the floating toolbar (page nav, zoom, view mode)
disableFullscreenbooleanfalseRemove the fullscreen button
hideAttributionbooleanfalseHide the 'Powered by docMentis' attribution link. Requires a valid license with the 'no_attribution' feature
enablePerformanceCounterbooleanfalseEnable performance tracking
onPerformanceLogfunctionCallback for performance log entries (called when enablePerformanceCounter is true)

UI Mode vs Headless Mode: When you provide a container, the viewer renders with full UI (toolbar, thumbnails, etc.). Without a container, it runs in headless mode for programmatic use.

Loading Documents

The viewer accepts multiple document sources:

// From URL
await viewer.load('https://example.com/document.pdf');

// From File object (e.g., from file input)
await viewer.load(file);

// From raw bytes
await viewer.load(new Uint8Array(buffer));

// Close current document
viewer.close();

Password Protection

When a password-protected document is loaded in UI mode, the viewer automatically prompts the user to enter the password. For headless mode, handle it programmatically:

await viewer.load(source);

if (await viewer.needsPassword()) {
  const success = await viewer.authenticate('my-password');
  if (!success) {
    console.error('Incorrect password');
  }
}

Document Information

Access document metadata and structure:

// Check if document is loaded
if (viewer.isLoaded) {
  // Get page count
  const total = viewer.pageCount;

  // Get document metadata
  const meta = viewer.metadata;
  console.log(`Title: ${meta?.title}, Author: ${meta?.author}`);

  // Get page dimensions (0-based index)
  const info = await viewer.getPageInfo(0);
  console.log(`Page 1: ${info.width} x ${info.height} points`);

  // Get document outline (table of contents)
  const outline = await viewer.getOutline();

  // Get annotations on a page (0-based index)
  const annotations = await viewer.getPageAnnotations(0);
}

Programmatic Control

Control zoom, view modes, and fullscreen programmatically — useful when toolbars are hidden:

// Zoom
viewer.zoomIn();
viewer.zoomOut();
viewer.setZoom(1.5);                  // 150%
viewer.setZoomMode('fit-spread-width');
viewer.setMinZoom(0.5);               // clamp minimum to 50%
viewer.setMaxZoom(3);                 // clamp maximum to 300%
console.log(viewer.zoom);            // current zoom level
console.log(viewer.zoomMode);        // current zoom mode

// View modes
viewer.setScrollMode('continuous');    // 'continuous' | 'spread'
viewer.setLayoutMode('double-page');   // 'single-page' | 'double-page' | ...
viewer.setPageRotation(90);            // 0 | 90 | 180 | 270
viewer.setSpacingMode('none');         // 'all' | 'none' | 'spread-only' | 'page-only'

// Theme
viewer.setTheme('dark');               // 'light' | 'dark' | 'system'
viewer.setThemeSwitchingEnabled(false); // hide theme toggle button
console.log(viewer.theme);            // current theme

// Text selection
viewer.setTextSelectionEnabled(false); // disable text selection

// Fullscreen
viewer.setFullscreen(true);
console.log(viewer.isFullscreen);

UI Visibility

Show, hide, or disable UI components at runtime:

// Toolbar visibility
viewer.setToolbarVisible(false);
viewer.setFloatingToolbarVisible(false);

// Fullscreen button
viewer.setFullscreenEnabled(false);

// Disable entire panel areas
viewer.setLeftPanelEnabled(false);
viewer.setRightPanelEnabled(false);

// Disable individual panel tabs
// Panels: 'thumbnail', 'outline', 'bookmarks', 'layers',
//         'attachments', 'search', 'comments'
viewer.setPanelEnabled('thumbnail', false);
viewer.setPanelEnabled('search', false);

// Open/close panels programmatically
viewer.openPanel('outline');
viewer.closePanel();

Export & Download

Export document data or trigger a download:

// Export document as raw bytes
const bytes = await viewer.toBytes();

// Download document to user's device
await viewer.download('document.pdf');

Page Rendering (Headless)

Render pages to images without UI, useful for generating thumbnails or image exports:

// Create headless viewer (no container)
const viewer = await client.createViewer();
await viewer.load(pdfBytes);

// Render page to ImageData (0-based page index)
const imageData = await viewer.renderPage(0, { scale: 2 });

// Render to Blob
const blob = await viewer.renderPage(0, {
  format: 'blob',
  imageType: 'image/png'
});

// Render to data URL
const dataUrl = await viewer.renderPage(0, {
  format: 'data-url',
  imageType: 'image/jpeg',
  quality: 0.9
});

// Render a thumbnail
const thumb = await viewer.renderThumbnail(0, { scale: 1 });

Render Options

OptionTypeDefaultDescription
scalenumber1Scale factor for rendering
formatstringimage-dataOutput format: 'image-data', 'image-bitmap', 'blob', 'data-url'
imageTypestringimage/pngImage MIME type for blob/data-url output
qualitynumber0.92Image quality for JPEG output (0-1)

Document Composition

Compose new documents by cherry-picking and rotating pages from existing documents:

// Create a new document from pages of existing documents
const [newDoc] = await client.compose([
  [
    { doc: viewerA, pages: "1-3" },
    { doc: viewerB, pages: "5", rotation: 90 }
  ]
]);

// Export the composed document
const bytes = await newDoc.toBytes();
await newDoc.download('composed.pdf');

Tip: The pages parameter accepts page ranges like "1-3", single pages like "5", or combinations like "1,3,5-7". Pages are 1-indexed.

Split & Extract

Split documents by outline and extract embedded resources:

Split by Outline

// Split a document by its top-level bookmarks
const result = await client.splitByOutline(viewer);
console.log(`Split into ${result.viewers.length} documents`);
result.sections.forEach((section, i) => {
  console.log(`Document ${i}: ${section.title}`);
});

// Split with options
const result = await client.splitByOutline(viewer, {
  maxLevel: 2,         // Include up to 2 levels of bookmarks
  splitMidPage: true,  // Filter content when sections share a page
});

Extract Images

// Extract all embedded images from a document
const images = await client.extractImages(viewer);
for (const image of images) {
  console.log(`${image.name}: ${image.format} (${image.width}x${image.height})`);
  // image.data contains the raw image bytes
}

// Convert raw images to PNG for easier viewing
const pngImages = await client.extractImages(viewer, {
  convertRawToPng: true,
});

Extract Fonts

// Extract all embedded fonts from a document
const fonts = await client.extractFonts(viewer);
for (const font of fonts) {
  console.log(`${font.name}: ${font.fontType} (.${font.extension})`);
  // font.data contains the raw font bytes
}

Compress & Decompress

Optimize document file size or prepare documents for inspection:

// Compress a document (FlateDecode, object streams, compressed xref)
const compressedBytes = await client.compress(viewer);

// Decompress a document (remove all filter encodings)
const decompressedBytes = await client.decompress(viewer);

// Save the result
const blob = new Blob([compressedBytes], { type: 'application/pdf' });

CSS Customization

The viewer uses CSS custom properties for all colors, shadows, and borders. Since no Shadow DOM is used, you can override any variable from your own stylesheet:

custom-theme.css
/* Override the primary color */
.udoc-viewer-root {
  --udoc-primary: #e91e63;
  --udoc-primary-hover: #c2185b;
}

/* Override dark theme colors */
.udoc-viewer-root.udoc-viewer-dark {
  --udoc-primary: #f48fb1;
  --udoc-primary-hover: #f06292;
}

Available CSS Variables

VariableDescriptionLightDark
Backgrounds
--udoc-bg-viewportViewport background#e0e0e0#1a1a1a
--udoc-bg-surfacePage / card surface#fff#2d2d2d
--udoc-bg-panelSide panel background#f5f5f5#252525
--udoc-bg-panel-tabsPanel tab bar#e8e8e8#1e1e1e
--udoc-bg-inputInput fields#fff#3a3a3a
--udoc-bg-overlayModal overlayrgba(0,0,0,0.5)rgba(0,0,0,0.7)
Text
--udoc-text-primaryPrimary textrgba(0,0,0,0.8)rgba(255,255,255,0.87)
--udoc-text-secondarySecondary textrgba(0,0,0,0.7)rgba(255,255,255,0.7)
--udoc-text-mutedMuted textrgba(0,0,0,0.5)rgba(255,255,255,0.5)
--udoc-text-on-primaryText on primary color#fff#fff
Primary color
--udoc-primaryPrimary / accent color#0066cc#4da6ff
--udoc-primary-hoverPrimary hover state#0052a3#80bfff
--udoc-primary-focus-ringFocus ring colorrgba(0,102,204,0.2)rgba(77,166,255,0.25)
Borders
--udoc-borderDefault border#ddd#444
--udoc-border-inputInput border#ccc#555
--udoc-border-lightLight border#eee#3a3a3a
Shadows
--udoc-shadow-pagePage shadow0 2px 8px ...0 2px 8px ...
--udoc-shadow-toolbarToolbar shadow0 2px 12px ...0 2px 12px ...
Search
--udoc-search-highlightSearch matchrgba(255,200,0,0.35)rgba(255,200,0,0.4)
--udoc-search-highlight-activeActive matchrgba(255,140,0,0.6)rgba(255,140,0,0.65)
Selection
--udoc-text-selectionText selectionrgba(0,120,215,0.3)rgba(77,166,255,0.35)

All viewer styles are scoped under .udoc-viewer-root, so your overrides won't leak into the rest of the page.

Events

Subscribe to viewer events to react to document state changes:

// Document loaded
const unsubscribe = viewer.on('document:load', ({ pageCount }) => {
  console.log(`Loaded ${pageCount} pages`);
});

// Document closed
viewer.on('document:close', () => {
  console.log('Document closed');
});

// Page changed
viewer.on('page:change', ({ page, previousPage }) => {
  console.log(`Page ${previousPage} -> ${page}`);
});

// Panel opened/closed
viewer.on('panel:change', ({ panel, previousPanel }) => {
  console.log(`Panel: ${previousPanel} -> ${panel}`);
});

// UI component visibility changed
viewer.on('ui:visibilityChange', ({ component, visible }) => {
  console.log(`${component} is now ${visible ? 'visible' : 'hidden'}`);
});

// Download progress
viewer.on('download:progress', ({ loaded, total, percent }) => {
  console.log(`Downloaded ${loaded}/${total} bytes (${percent}%)`);
});

// Error occurred
viewer.on('error', ({ error, phase }) => {
  console.error(`Error during ${phase}:`, error);
});

// Unsubscribe
unsubscribe();
EventPayloadDescription
document:load{ pageCount }Fired when a document is successfully loaded
document:closeFired when the current document is closed
page:change{ page, previousPage }Fired when the current page changes
panel:change{ panel, previousPanel }Fired when a panel is opened or closed
ui:visibilityChange{ component, visible }Fired when a UI component's visibility changes
download:progress{ loaded, total, percent }Fired during document download with progress info
error{ error, phase }Fired when an error occurs (fetch, parse, or render)

Cleanup

Properly dispose of resources when done to prevent memory leaks:

// Destroy the viewer first
viewer.destroy();

// Then destroy the client
client.destroy();

Important: Always destroy the viewer before destroying the client. The client manages the WASM engine which the viewer depends on.

Browser Support

Requires WebAssembly support.

BrowserSupported
Chrome / Edge80+
Firefox80+
Safari15+

Telemetry

udoc-viewer collects anonymous, non-personally-identifiable usage data to help us understand SDK adoption and prioritize format support. Telemetry fires once per document open.

What we collect

FieldDescriptionExample
domainHostname of the embedding websiteexample.com
formatDocument formatpdf
size_bucketFile size in units of 100 KB3
viewer_versionSDK version string0.5.19
license_hashSHA-256 hash of the license keya1b2c3...

What we do NOT collect

  • Document content, filenames, or URLs
  • User identity, cookies, or session data
  • IP addresses (disabled at the collection endpoint)
  • Any other personally identifiable information

Data is sent via navigator.sendBeacon (with fetch fallback). No third-party SDK is loaded.

Licensing

udoc-viewer is free to use, including in commercial applications. A “Powered by docMentis” attribution link is shown by default.

The JavaScript/TypeScript source code is licensed under the MIT License. The WebAssembly binary is distributed under the docMentis WASM Runtime License— free to use with the docMentis Viewer in commercial and non-commercial applications.

To remove the attribution, contact licensing@docmentis.com to purchase a license:

const client = await UDocClient.create({
  license: 'eyJ2Ijox...',
});

const viewer = await client.createViewer({
  container: '#viewer',
  hideAttribution: true,
});

// Check license status
console.log(client.license);
// { valid: true, tier: "licensed", features: ["no_attribution"], ... }

console.log(client.hasFeature('no_attribution')); // true

The hideAttribution option is only honored when the license includes the no_attribution feature. Without a valid license, the attribution link will remain visible.