A framework-agnostic, universal document renderer with optional chunked loading polyrender.wisp.place/
README.md

@polyrender/core#

Framework-agnostic TypeScript library for rendering documents in the browser. Supports PDF, EPUB, DOCX, ODT, ODS, CSV/TSV, source code, plain text, and comic book archives with a unified API.

For React support, see @polyrender/react.

Installation#

npm install @polyrender/core

Install peer dependencies only for the formats you need:

npm install pdfjs-dist       # PDF
npm install epubjs           # EPUB
npm install docx-preview     # DOCX
npm install jszip            # ODT, CBZ comic archives
npm install xlsx             # ODS
npm install papaparse        # CSV/TSV
npm install highlight.js     # Code, Markdown, JSON, XML/HTML

# Comic book archives — additional optional backends:
npm install node-unrar-js    # CBR (.cbr, RAR-compressed comics)
npm install 7z-wasm          # CB7 (.cb7, 7-Zip-compressed comics)

# Comic book archives — optional exotic image format decoders:
npm install @jsquash/jxl     # JPEG XL images inside archives
npm install utif             # TIFF images inside archives

Usage#

import { PolyRender } from '@polyrender/core'
import '@polyrender/core/styles.css'

const viewer = new PolyRender(document.getElementById('viewer')!, {
  source: { type: 'url', url: '/document.pdf' },
  theme: 'dark',
  toolbar: true,
  onReady: (info) => console.log(`Loaded: ${info.pageCount} pages`),
  onPageChange: (page, total) => console.log(`Page ${page} of ${total}`),
})

// Imperative control
viewer.goToPage(5)
viewer.setZoom('fit-width')
viewer.setZoom(1.5)

// Clean up
viewer.destroy()

Document Sources#

File (binary data)#

// From a File input
const file = inputElement.files[0]
source = { type: 'file', data: file, filename: file.name }

// From an ArrayBuffer
source = { type: 'file', data: arrayBuffer, mimeType: 'application/pdf' }

URL#

source = { type: 'url', url: '/document.pdf' }

// With custom headers (e.g., auth)
source = {
  type: 'url',
  url: '/api/documents/123.pdf',
  fetchOptions: { headers: { Authorization: 'Bearer ...' } },
}

Pre-rendered Pages#

// Direct array
source = {
  type: 'pages',
  pages: [
    { pageNumber: 1, imageUrl: '/pages/1.webp', width: 1654, height: 2339 },
    { pageNumber: 2, imageUrl: '/pages/2.webp', width: 1654, height: 2339 },
  ],
}

// Lazy fetch adapter
source = {
  type: 'pages',
  pages: {
    totalPages: 500,
    fetchPage: async (pageNumber) => ({
      pageNumber,
      imageUrl: `/api/pages/${pageNumber}.webp`,
      width: 1654,
      height: 2339,
    }),
  },
}

Chunked PDF#

source = {
  type: 'chunked',
  totalPages: 500,
  chunks: {
    totalChunks: 10,
    totalPages: 500,
    fetchChunk: async (index) => {
      const res = await fetch(`/api/chunks/${index}.pdf`)
      return {
        data: await res.arrayBuffer(),
        pageStart: index * 50 + 1,
        pageEnd: Math.min((index + 1) * 50, 500),
      }
    },
    getChunkIndexForPage: (page) => Math.floor((page - 1) / 50),
  },
  // Optional: fast browse images while chunks load
  browsePages: {
    totalPages: 500,
    fetchPage: async (pageNumber) => ({
      pageNumber,
      imageUrl: `/api/browse/${pageNumber}.webp`,
      width: 1654,
      height: 2339,
    }),
  },
}

Options#

new PolyRender(container, {
  source,                    // Required
  format?: DocumentFormat,   // Override auto-detection
  theme?: 'dark' | 'light' | 'system',  // Default: 'dark'
  className?: string,        // Extra CSS class on root element
  initialPage?: number,      // Starting page (default: 1)
  zoom?: number | 'fit-width' | 'fit-page' | 'auto',
  toolbar?: boolean | ToolbarConfig,
  // ToolbarConfig fields:
  //   navigation?: boolean    Show page nav controls (default true)
  //   zoom?: boolean          Show zoom controls (default true)
  //   wrapToggle?: boolean    Show word-wrap/fit toggle (auto for code, text, comic)
  //   fullscreen?: boolean    Show fullscreen button (default true)
  //   info?: boolean          Show filename label (default true)
  //   download?: boolean      Show download button (default false)
  //   position?: 'top'|'bottom'

  // Callbacks
  onReady?: (info: DocumentInfo) => void,
  onPageChange?: (page: number, totalPages: number) => void,
  onZoomChange?: (zoom: number) => void,
  onError?: (error: PolyRenderError) => void,
  onLoadingChange?: (loading: boolean) => void,

  // Format-specific
  pdf?: PdfOptions,
  epub?: EpubOptions,
  code?: CodeOptions,
  csv?: CsvOptions,
  odt?: OdtOptions,
  ods?: OdsOptions,
  comic?: ComicOptions,
})

Format-specific Options#

PDF

pdf: {
  workerSrc?: string,       // pdf.js worker URL
  cMapUrl?: string,         // Character map directory
  textLayer?: boolean,      // Enable text selection (default true)
  annotationLayer?: boolean // Show PDF annotations (default false)
}

EPUB

epub: {
  flow?: 'paginated' | 'scrolled', // Default: 'paginated'
  fontSize?: number,               // Font size in px (default 16)
  fontFamily?: string,             // Font override
}

Code

code: {
  language?: string,    // Force language (auto-detected from extension)
  lineNumbers?: boolean, // Default true
  wordWrap?: boolean,    // Default false
  tabSize?: number,      // Default 2
}

CSV/TSV

csv: {
  delimiter?: string, // Auto-detected
  header?: boolean,   // First row is header (default true)
  maxRows?: number,   // Default 10000
  sortable?: boolean, // Default true
}

ODT

odt: {
  fontSize?: number,   // Base font size in px (default 16)
  fontFamily?: string, // Font override
}

ODS

ods: {
  maxRows?: number,   // Max rows per sheet (default 10000)
  sortable?: boolean, // Default true
  header?: boolean,   // First row is header (default true)
}

Comic book archives

comic: {
  // Image formats to extract from the archive.
  // Defaults to all natively supported browser formats.
  // Add 'jxl' + jxlFallback: true  to enable JPEG XL decoding.
  // Add 'tiff' + tiffSupport: true  to enable TIFF decoding.
  imageFormats?: Array<'png' | 'jpg' | 'gif' | 'bmp' | 'webp' | 'avif' | 'tiff' | 'jxl'>,

  // Enable JPEG XL fallback decoding via @jsquash/jxl.
  // Requires: npm install @jsquash/jxl
  jxlFallback?: boolean,

  // Enable TIFF image decoding via utif.
  // Requires: npm install utif
  tiffSupport?: boolean,
}

Events#

Subscribe to events using .on() (returns an unsubscribe function):

const off = viewer.on('pagechange', ({ page, totalPages }) => {
  console.log(`${page} / ${totalPages}`)
})

// Later:
off()

Available events: ready, pagechange, zoomchange, loadingchange, error, destroy.

Theming#

PolyRender uses CSS custom properties prefixed --dv-*. Override them on the .polyrender root element:

.my-viewer .polyrender {
  --dv-bg: #1e1e2e;
  --dv-surface: #2a2a3e;
  --dv-text: #cdd6f4;
  --dv-accent: #89b4fa;
  --dv-border: #45475a;
}

Built-in themes: dark (default), light, system.

Custom Renderers#

import { PolyRender, BaseRenderer } from '@polyrender/core'
import type { PolyRenderOptions, DocumentFormat } from '@polyrender/core'

class MarkdownRenderer extends BaseRenderer {
  readonly format: DocumentFormat = 'custom-markdown'

  protected async onMount(viewport: HTMLElement, options: PolyRenderOptions) {
    const text = await this.loadText(options.source)
    viewport.innerHTML = myMarkdownLib.render(text)
    this.setReady({ format: 'custom-markdown', pageCount: 1 })
  }

  protected onDestroy() {}
}

PolyRender.registerRenderer('custom-markdown', () => new MarkdownRenderer())

Supported Formats#

Format Peer Dependency Auto-detected Extensions
PDF pdfjs-dist .pdf
EPUB epubjs .epub
DOCX docx-preview .docx, .doc
ODT jszip .odt
ODS xlsx .ods
CSV/TSV papaparse .csv, .tsv
Code highlight.js .js, .ts, .py, .rs, .go, +80 more
Text (none) .txt
Markdown highlight.js .md
JSON highlight.js .json
XML/HTML highlight.js .xml, .html, .svg
Pages (none) N/A (explicit type: 'pages')
Chunked PDF pdfjs-dist N/A (explicit type: 'chunked')
Comic — CBZ jszip .cbz
Comic — CBR node-unrar-js (optional) .cbr
Comic — CB7 7z-wasm (optional) .cb7
Comic — CBT (none, built-in TAR reader) .cbt
Comic — CBA ❌ not supported .cba

Comic archives support images in PNG, JPEG, GIF, BMP, WebP, and AVIF natively. TIFF and JPEG XL require additional opt-in peer dependencies (see ComicOptions above).

Live Demo#

A hosted version of the vanilla example is available at https://polyrender.wisp.place/.

Repository#

The source code is hosted in two locations:

This package lives under packages/core in the monorepo.

License#

Zlib