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

Configure Feed

Select the types of activity you want to include in your feed.

at main 348 lines 9.4 kB view raw view rendered
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