A framework-agnostic, universal document renderer with optional chunked loading
polyrender.wisp.place/
1# @polyrender/core
2
3Framework-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.
4
5For React support, see [`@polyrender/react`](https://www.npmjs.com/package/@polyrender/react).
6
7## Installation
8
9```bash
10npm install @polyrender/core
11```
12
13Install peer dependencies only for the formats you need:
14
15```bash
16npm install pdfjs-dist # PDF
17npm install epubjs # EPUB
18npm install docx-preview # DOCX
19npm install jszip # ODT, CBZ comic archives
20npm install xlsx # ODS
21npm install papaparse # CSV/TSV
22npm install highlight.js # Code, Markdown, JSON, XML/HTML
23
24# Comic book archives — additional optional backends:
25npm install node-unrar-js # CBR (.cbr, RAR-compressed comics)
26npm install 7z-wasm # CB7 (.cb7, 7-Zip-compressed comics)
27
28# Comic book archives — optional exotic image format decoders:
29npm install @jsquash/jxl # JPEG XL images inside archives
30npm install utif # TIFF images inside archives
31```
32
33## Usage
34
35```typescript
36import { PolyRender } from '@polyrender/core'
37import '@polyrender/core/styles.css'
38
39const viewer = new PolyRender(document.getElementById('viewer')!, {
40 source: { type: 'url', url: '/document.pdf' },
41 theme: 'dark',
42 toolbar: true,
43 onReady: (info) => console.log(`Loaded: ${info.pageCount} pages`),
44 onPageChange: (page, total) => console.log(`Page ${page} of ${total}`),
45})
46
47// Imperative control
48viewer.goToPage(5)
49viewer.setZoom('fit-width')
50viewer.setZoom(1.5)
51
52// Clean up
53viewer.destroy()
54```
55
56## Document Sources
57
58### File (binary data)
59
60```typescript
61// From a File input
62const file = inputElement.files[0]
63source = { type: 'file', data: file, filename: file.name }
64
65// From an ArrayBuffer
66source = { type: 'file', data: arrayBuffer, mimeType: 'application/pdf' }
67```
68
69### URL
70
71```typescript
72source = { type: 'url', url: '/document.pdf' }
73
74// With custom headers (e.g., auth)
75source = {
76 type: 'url',
77 url: '/api/documents/123.pdf',
78 fetchOptions: { headers: { Authorization: 'Bearer ...' } },
79}
80```
81
82### Pre-rendered Pages
83
84```typescript
85// Direct array
86source = {
87 type: 'pages',
88 pages: [
89 { pageNumber: 1, imageUrl: '/pages/1.webp', width: 1654, height: 2339 },
90 { pageNumber: 2, imageUrl: '/pages/2.webp', width: 1654, height: 2339 },
91 ],
92}
93
94// Lazy fetch adapter
95source = {
96 type: 'pages',
97 pages: {
98 totalPages: 500,
99 fetchPage: async (pageNumber) => ({
100 pageNumber,
101 imageUrl: `/api/pages/${pageNumber}.webp`,
102 width: 1654,
103 height: 2339,
104 }),
105 },
106}
107```
108
109### Chunked PDF
110
111```typescript
112source = {
113 type: 'chunked',
114 totalPages: 500,
115 chunks: {
116 totalChunks: 10,
117 totalPages: 500,
118 fetchChunk: async (index) => {
119 const res = await fetch(`/api/chunks/${index}.pdf`)
120 return {
121 data: await res.arrayBuffer(),
122 pageStart: index * 50 + 1,
123 pageEnd: Math.min((index + 1) * 50, 500),
124 }
125 },
126 getChunkIndexForPage: (page) => Math.floor((page - 1) / 50),
127 },
128 // Optional: fast browse images while chunks load
129 browsePages: {
130 totalPages: 500,
131 fetchPage: async (pageNumber) => ({
132 pageNumber,
133 imageUrl: `/api/browse/${pageNumber}.webp`,
134 width: 1654,
135 height: 2339,
136 }),
137 },
138}
139```
140
141## Options
142
143```typescript
144new PolyRender(container, {
145 source, // Required
146 format?: DocumentFormat, // Override auto-detection
147 theme?: 'dark' | 'light' | 'system', // Default: 'dark'
148 className?: string, // Extra CSS class on root element
149 initialPage?: number, // Starting page (default: 1)
150 zoom?: number | 'fit-width' | 'fit-page' | 'auto',
151 toolbar?: boolean | ToolbarConfig,
152 // ToolbarConfig fields:
153 // navigation?: boolean Show page nav controls (default true)
154 // zoom?: boolean Show zoom controls (default true)
155 // wrapToggle?: boolean Show word-wrap/fit toggle (auto for code, text, comic)
156 // fullscreen?: boolean Show fullscreen button (default true)
157 // info?: boolean Show filename label (default true)
158 // download?: boolean Show download button (default false)
159 // position?: 'top'|'bottom'
160
161 // Callbacks
162 onReady?: (info: DocumentInfo) => void,
163 onPageChange?: (page: number, totalPages: number) => void,
164 onZoomChange?: (zoom: number) => void,
165 onError?: (error: PolyRenderError) => void,
166 onLoadingChange?: (loading: boolean) => void,
167
168 // Format-specific
169 pdf?: PdfOptions,
170 epub?: EpubOptions,
171 code?: CodeOptions,
172 csv?: CsvOptions,
173 odt?: OdtOptions,
174 ods?: OdsOptions,
175 comic?: ComicOptions,
176})
177```
178
179### Format-specific Options
180
181**PDF**
182```typescript
183pdf: {
184 workerSrc?: string, // pdf.js worker URL
185 cMapUrl?: string, // Character map directory
186 textLayer?: boolean, // Enable text selection (default true)
187 annotationLayer?: boolean // Show PDF annotations (default false)
188}
189```
190
191**EPUB**
192```typescript
193epub: {
194 flow?: 'paginated' | 'scrolled', // Default: 'paginated'
195 fontSize?: number, // Font size in px (default 16)
196 fontFamily?: string, // Font override
197}
198```
199
200**Code**
201```typescript
202code: {
203 language?: string, // Force language (auto-detected from extension)
204 lineNumbers?: boolean, // Default true
205 wordWrap?: boolean, // Default false
206 tabSize?: number, // Default 2
207}
208```
209
210**CSV/TSV**
211```typescript
212csv: {
213 delimiter?: string, // Auto-detected
214 header?: boolean, // First row is header (default true)
215 maxRows?: number, // Default 10000
216 sortable?: boolean, // Default true
217}
218```
219
220**ODT**
221```typescript
222odt: {
223 fontSize?: number, // Base font size in px (default 16)
224 fontFamily?: string, // Font override
225}
226```
227
228**ODS**
229```typescript
230ods: {
231 maxRows?: number, // Max rows per sheet (default 10000)
232 sortable?: boolean, // Default true
233 header?: boolean, // First row is header (default true)
234}
235```
236
237**Comic book archives**
238```typescript
239comic: {
240 // Image formats to extract from the archive.
241 // Defaults to all natively supported browser formats.
242 // Add 'jxl' + jxlFallback: true to enable JPEG XL decoding.
243 // Add 'tiff' + tiffSupport: true to enable TIFF decoding.
244 imageFormats?: Array<'png' | 'jpg' | 'gif' | 'bmp' | 'webp' | 'avif' | 'tiff' | 'jxl'>,
245
246 // Enable JPEG XL fallback decoding via @jsquash/jxl.
247 // Requires: npm install @jsquash/jxl
248 jxlFallback?: boolean,
249
250 // Enable TIFF image decoding via utif.
251 // Requires: npm install utif
252 tiffSupport?: boolean,
253}
254```
255
256## Events
257
258Subscribe to events using `.on()` (returns an unsubscribe function):
259
260```typescript
261const off = viewer.on('pagechange', ({ page, totalPages }) => {
262 console.log(`${page} / ${totalPages}`)
263})
264
265// Later:
266off()
267```
268
269Available events: `ready`, `pagechange`, `zoomchange`, `loadingchange`, `error`, `destroy`.
270
271## Theming
272
273PolyRender uses CSS custom properties prefixed `--dv-*`. Override them on the `.polyrender` root element:
274
275```css
276.my-viewer .polyrender {
277 --dv-bg: #1e1e2e;
278 --dv-surface: #2a2a3e;
279 --dv-text: #cdd6f4;
280 --dv-accent: #89b4fa;
281 --dv-border: #45475a;
282}
283```
284
285Built-in themes: `dark` (default), `light`, `system`.
286
287## Custom Renderers
288
289```typescript
290import { PolyRender, BaseRenderer } from '@polyrender/core'
291import type { PolyRenderOptions, DocumentFormat } from '@polyrender/core'
292
293class MarkdownRenderer extends BaseRenderer {
294 readonly format: DocumentFormat = 'custom-markdown'
295
296 protected async onMount(viewport: HTMLElement, options: PolyRenderOptions) {
297 const text = await this.loadText(options.source)
298 viewport.innerHTML = myMarkdownLib.render(text)
299 this.setReady({ format: 'custom-markdown', pageCount: 1 })
300 }
301
302 protected onDestroy() {}
303}
304
305PolyRender.registerRenderer('custom-markdown', () => new MarkdownRenderer())
306```
307
308## Supported Formats
309
310| Format | Peer Dependency | Auto-detected Extensions |
311|--------|----------------|--------------------------|
312| PDF | `pdfjs-dist` | `.pdf` |
313| EPUB | `epubjs` | `.epub` |
314| DOCX | `docx-preview` | `.docx`, `.doc` |
315| ODT | `jszip` | `.odt` |
316| ODS | `xlsx` | `.ods` |
317| CSV/TSV | `papaparse` | `.csv`, `.tsv` |
318| Code | `highlight.js` | `.js`, `.ts`, `.py`, `.rs`, `.go`, +80 more |
319| Text | _(none)_ | `.txt` |
320| Markdown | `highlight.js` | `.md` |
321| JSON | `highlight.js` | `.json` |
322| XML/HTML | `highlight.js` | `.xml`, `.html`, `.svg` |
323| Pages | _(none)_ | N/A (explicit `type: 'pages'`) |
324| Chunked PDF | `pdfjs-dist` | N/A (explicit `type: 'chunked'`) |
325| Comic — CBZ | `jszip` | `.cbz` |
326| Comic — CBR | `node-unrar-js` _(optional)_ | `.cbr` |
327| Comic — CB7 | `7z-wasm` _(optional)_ | `.cb7` |
328| Comic — CBT | _(none, built-in TAR reader)_ | `.cbt` |
329| Comic — CBA | ❌ not supported | `.cba` |
330
331Comic 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).
332
333## Live Demo
334
335A hosted version of the vanilla example is available at **https://polyrender.wisp.place/**.
336
337## Repository
338
339The source code is hosted in two locations:
340
341- **Tangled** (primary): https://tangled.org/aria.pds.witchcraft.systems/polyrender
342- **GitHub** (mirror): https://github.com/BuyMyMojo/polyrender
343
344This package lives under `packages/core` in the monorepo.
345
346## License
347
348Zlib