npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@docmentis/udoc-viewer

v0.5.15

Published

Free, open-source, universal document viewer for the web. Render PDF, PPTX, and images with high fidelity — no server required.

Readme

@docmentis/udoc-viewer

A free, open-source, universal document viewer for the web. Render PDF, PPTX, and images with high fidelity — no server required.

npm version license

Live Demo · Guide · Report Issue


Why udoc-viewer?

Most web document viewers only handle PDF, rely on server-side rendering, or require expensive commercial licenses. udoc-viewer is different:

  • Truly universal — PDF, PowerPoint, and images in a single viewer, with more formats coming
  • 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

Supported Formats

| Format | Extensions | |--------|------------| | PDF | .pdf | | PPTX | .pptx | | Images | .png, .jpg, .jpeg, .gif, .webp, .bmp, .tif, .tiff, .ico, .tga, .ppm, .pgm, .pbm, .hdr, .exr, .qoi |

Quick Start

Install

npm install @docmentis/udoc-viewer

Basic Usage

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

<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

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' }} />;
}

Examples

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

| Example | Stack | |---------|-------| | vanilla | TypeScript + Vite | | react-vite | React + Vite | | vue-vite | Vue + Vite | | svelte-vite | Svelte 5 + Vite | | angular | Angular 19 | | nextjs-webpack | Next.js + Webpack | | nextjs-turbopack | Next.js + Turbopack |

Features

  • 📄 Multi-format rendering — PDF, PPTX, and images in one unified viewer
  • 🎯 High-fidelity output — custom Rust rendering engine compiled to WebAssembly
  • 🔍 Zoom & navigation — toolbar with zoom controls, page thumbnails, and keyboard navigation
  • 📱 Responsive — works on desktop and mobile browsers
  • 🌊 Streaming — pages render progressively as the document loads
  • 🔒 Private — documents never leave the browser; no server upload required

API Reference

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-Protected Documents

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

await viewer.load(source);

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

Client Options

const client = await UDocClient.create({
  // 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/',
});

Viewer Options

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

  // Scroll mode: 'continuous' or 'spread' (default: 'continuous')
  scrollMode: 'continuous',

  // Layout mode: 'single-page', 'double-page', 'double-page-odd-right', 'double-page-odd-left'
  // (default: 'single-page')
  layoutMode: 'single-page',

  // Zoom mode: 'fit-spread-width', 'fit-spread-height', 'fit-spread', 'custom'
  // (default: 'fit-spread-width')
  zoomMode: 'fit-spread-width',

  // Initial zoom level (when zoomMode is 'custom', default: 1)
  zoom: 1,

  // 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])
  zoomSteps: [0.1, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4, 5],

  // Spacing between pages in pixels (default: 10)
  pageSpacing: 10,

  // Spacing between spreads in pixels (default: 20)
  spreadSpacing: 20,

  // Initially active panel, or null for no panel (default: null)
  // Left panels: 'thumbnail', 'outline', 'bookmarks', 'layers', 'attachments'
  // Right panels: 'search', 'comments'
  activePanel: null,

  // Target display DPI (default: 96)
  dpi: 96,

  // Enable Google Fonts for automatic font fetching (default: true)
  googleFonts: true,

  // Enable performance tracking (default: false)
  enablePerformanceCounter: false,
});

Navigation

// Get current page (1-based)
const page = viewer.currentPage;

// Go to a specific page (1-based)
viewer.goToPage(5);

// Navigate to a destination (from outline)
viewer.goToDestination(destination);

Document Information

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

  // Get document metadata
  const meta = viewer.metadata;
  console.log(meta?.title, 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);
}

Headless Rendering

Render pages to images without UI:

// 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 thumbnail
const thumb = await viewer.renderThumbnail(0, { scale: 1 });

Document Export

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

// Download document as a file
await viewer.download('document.pdf');

Document Composition

Compose new documents by cherry-picking and rotating pages:

// 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');

Document Utilities

// Split a document by its outline (table of contents)
const { viewers, sections } = await client.splitByOutline(source, {
  maxLevel: 2,
  splitMidPage: false,
});

// Extract images from a document
const images = await client.extractImages(source, {
  convertRawToPng: true,
});

// Extract fonts from a document
const fonts = await client.extractFonts(source);

// Compress a document
const compressed = await client.compress(source);

// Decompress a document
const decompressed = await client.decompress(source);

Events

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

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

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

How It Works

udoc-viewer uses a custom document processing engine written in Rust, compiled to WebAssembly. Documents are parsed and rendered entirely in the browser with near-native performance — no PDF.js, no iframe hacks, no server-side conversion.

The JavaScript wrapper (@docmentis/udoc-viewer) is MIT-licensed and open source. The WASM rendering engine is free to use, including in commercial applications. See LICENSE for details.

Browser Support

| Browser | Supported | |---------|-----------| | Chrome / Edge | ✅ 80+ | | Firefox | ✅ 80+ | | Safari | ✅ 15+ |

Requires WebAssembly support.

Contributing

We welcome bug reports, feature requests, and pull requests.

License

The JavaScript/TypeScript source code is licensed under the MIT License.

The WebAssembly binary (src/wasm/udoc_bg.wasm) is distributed under the docMentis WASM Runtime License -- free to use with the docMentis Viewer in commercial and non-commercial applications.

Links