renderpdf-js 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,150 +4,197 @@
4
4
 
5
5
  # RenderPDF.js
6
6
 
7
- An open-source TypeScript library for converting HTML/CSS and standalone images
8
- to PDF in the browser. HTML exports retain selectable text, local raster fallbacks
9
- and automatic pagination. The browser calculates HTML layout; RenderPDF.js uses
10
- its measured text, box, image and table coordinates to write the PDF. Standalone
11
- PNG/JPEG and multipage TIFF files use a separate image API. Conversion runs
12
- entirely on the client.
7
+ **Create PDFs from HTML, images or code, and merge or split existing PDFs.**
13
8
 
14
- The renderer is experimental and supports the bounded subset described below.
15
- Chromium is the verified browser; Firefox/Safari compatibility has not been
16
- established.
9
+ [![npm version](https://img.shields.io/npm/v/renderpdf-js)](https://www.npmjs.com/package/renderpdf-js)
10
+ [![CI](https://github.com/MichalekJan93/renderPDF.js/actions/workflows/check.yml/badge.svg?branch=main)](https://github.com/MichalekJan93/renderPDF.js/actions/workflows/check.yml)
11
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/MichalekJan93/renderPDF.js/blob/main/LICENSE)
17
12
 
18
- ## Getting started
13
+ RenderPDF.js is a JavaScript and TypeScript library for creating PDF documents,
14
+ converting HTML and images, and working with existing PDF pages. It runs locally,
15
+ without a conversion server.
16
+
17
+ HTML exports use the browser's layout, keep text selectable and split content
18
+ across pages. Document creation and PDF composition also work in Node.js,
19
+ without a DOM.
20
+
21
+ **Experimental.** HTML rendering is verified in Chromium; Firefox and Safari have
22
+ not been verified. Supported HTML and CSS are [described below](#supported-scope).
23
+
24
+ ## Features
25
+
26
+ - Create documents with text, images, vector graphics, links and form fields.
27
+ - Merge PDFs, extract selected pages and split documents without rasterizing them.
28
+ - Export supported HTML/CSS with selectable text and automatic pagination.
29
+ - Convert PNG/JPEG and multipage TIFF files to PDF.
30
+ - Add repeated table headers, page text, page numbers and document properties.
31
+ - Use the DevExtreme PDF exporter through an optional compatibility adapter.
32
+ - ESM package with TypeScript declarations, cancellation and conversion progress.
19
33
 
20
- Install the library in a browser application with a bundler, such as Vite:
34
+ See the [API guide](https://github.com/MichalekJan93/renderPDF.js/blob/main/API.md)
35
+ for methods, coordinate conventions and compatibility limits.
36
+
37
+ ## Getting started
21
38
 
22
39
  ```sh
23
40
  npm install renderpdf-js
24
41
  ```
25
42
 
26
- The package provides an ESM entry and TypeScript declarations. Version 0.x is
27
- experimental; review the supported scope and limits before using it for documents.
28
- To try HTML preview and PDF download from a source checkout, run the independent
29
- [playground](https://github.com/MichalekJan93/renderPDF.js/tree/main/playground).
43
+ For browser applications, use an ESM bundler such as Vite.
44
+ The [playground](https://github.com/MichalekJan93/renderPDF.js/tree/main/playground)
45
+ provides an HTML editor, preview and PDF download from a source checkout.
30
46
 
31
- ## Standalone image files
47
+ ## Create a document
32
48
 
33
- `imageToPDF` is available from version 0.2.0. It accepts one `File`/`Blob`, `ArrayBuffer` or
34
- `Uint8Array`; actual file signatures determine the format, not filenames or MIME
35
- labels. No HTML element or registered fonts are needed.
49
+ ```ts
50
+ import { createDocument } from 'renderpdf-js';
51
+
52
+ const document = await createDocument({ unit: 'mm', format: 'A4' });
53
+ const page = document.addPage();
54
+ page.drawText('Invoice', { x: 20, y: 25, fontSize: 18 });
55
+ page.drawRectangle({ x: 20, y: 35, width: 170, height: 20, stroke: '#333333' });
56
+
57
+ const pdf = await document.toResult();
58
+ await pdf.download('invoice.pdf');
59
+ ```
60
+
61
+ Coordinates start at the top-left; text `y` is its baseline. Font sizes always use
62
+ points. The document API works without a DOM. Built-in Helvetica, Times and Courier
63
+ cover basic text; provide static TTF/OTF bytes in `fonts` for Czech and other Unicode
64
+ characters. Fonts are prepared before drawing. After `toResult()`, the document is
65
+ immutable.
66
+
67
+ Use `pdf.toUint8Array()` or `pdf.toBlob()` outside the browser; `download()` is a
68
+ browser action.
69
+
70
+ ## Combine and split PDFs
36
71
 
37
72
  ```ts
38
- import { imageToPDF } from 'renderpdf-js';
73
+ import { mergePDFs, extractPDFPages, splitPDF } from 'renderpdf-js';
39
74
 
40
- const file = fileInput.files?.[0];
41
- if (!file) throw new Error('Select a PNG, JPEG or TIFF file.');
75
+ const combined = await mergePDFs([firstFile, secondFile]);
76
+ await combined.download('combined.pdf');
42
77
 
43
- const pdf = await imageToPDF(file, {
44
- format: 'A4',
45
- margin: 24,
46
- metadata: { title: 'Scanned document' },
47
- onProgress: ({ stage, completed, total }) => {
48
- statusElement.textContent = `${stage}: ${completed}/${total}`;
49
- },
50
- });
51
- await pdf.download('document.pdf');
78
+ const selected = await extractPDFPages(firstFile, [3, 1]);
79
+ const individualPages = await splitPDF(firstFile);
52
80
  ```
53
81
 
54
- PNG/JPEG produce one PDF page. Every main linked TIFF frame produces its own page
55
- in file order. Images fit completely inside the selected paper's margins, retain
56
- their aspect ratio and are centered; fitting can enlarge a small image. The
57
- defaults are A4 portrait and 24 CSS-pixel margins. A3, Letter, landscape,
58
- asymmetric margins and custom paper sizes use the same settings as HTML exports.
59
- TIFF/EXIF orientations 1–8, including mirrored orientations and PNG `eXIf`, are
60
- applied when painting the page. JPEG compressed bytes remain unchanged.
61
-
62
- The result has the same `pageCount`, `download()`, `toBlob()` and
63
- `toUint8Array()` methods as HTML exports. Image text remains part of the bitmap;
64
- this API does not perform OCR. Metadata and mutable input bytes are copied before
65
- the first asynchronous stage. Cancellation uses `signal`. Progress stages are
66
- `preparing`, `resources`, `pages`, `saving` and `complete`; image conversion does
67
- not emit HTML layout/pagination stages. Resource/page totals equal the number of
68
- image frames. A failed or canceled conversion returns no partial PDF.
69
-
70
- Supported TIFF input is classic TIFF with strips and either byte order, using
71
- uncompressed, CCITT RLE/Group 3/Group 4, LZW, PackBits or Deflate compression.
72
- The bounded decoder supports common bilevel/grayscale, palette and 8-bit RGB/RGBA
73
- profiles, including associated alpha normalization. Other modes, including
74
- BigTIFF, tiles, SubIFDs/RAW layouts, planar-separated samples, 16-bit/float samples
75
- and JPEG-compressed TIFF, fail with `UNSUPPORTED_IMAGE`. Frames are limited to
76
- 40 million pixels each and TIFF files to 1,000 main frames. PNG must be static;
77
- APNG is rejected explicitly. JPEG supports 8-bit baseline/extended/progressive
78
- grayscale, RGB and CMYK frames.
79
-
80
- Malformed headers, checksums and out-of-file offsets are rejected. JPEG entropy
81
- validation and detection of every corrupt CCITT bitstream remain decoder-limited;
82
- structural checks do not prove that every damaged scan is recoverable. TIFF
83
- pixels are decoded one frame at a time with the MIT-licensed `utif2` dependency,
84
- which loads only when TIFF decoding is requested. The HTML `<img>` pipeline still
85
- accepts native PNG/JPEG resources; standalone TIFF support does not change it.
82
+ Inputs can be `File`/`Blob`, `ArrayBuffer`, `Uint8Array` or a completed export result.
83
+ Page numbers are one-based. Pages retain their native text, vectors, sizes and
84
+ rotation. Flatten interactive forms before importing them; encrypted PDFs and
85
+ unsupported destinations fail explicitly. Document-level bookmarks, signatures and
86
+ metadata are not merged. See the [composition limits](https://github.com/MichalekJan93/renderPDF.js/blob/main/API.md#pdf-composition).
87
+
88
+ ## DevExtreme integration
89
+
90
+ `createJsPDFAdapter()` provides the synchronous drawing and text measurement
91
+ contract used by the DevExtreme PDF exporter. Supply fonts when creating it, pass
92
+ the result as `jsPDFDocument`, then **await** `save()` or `output('blob')`.
93
+ The adapter covers the tested exporter contract; it does not emulate every jsPDF
94
+ plugin or its synchronous serialization. The [API guide](https://github.com/MichalekJan93/renderPDF.js/blob/main/API.md#devextreme-adapter)
95
+ includes an example and the optional integration test command. DevExtreme is not
96
+ a dependency of this library.
86
97
 
87
98
  ## HTML usage
88
99
 
89
- Import the public API from the installed package in your application's browser entry:
100
+ Register a font, select an existing element and export it:
90
101
 
91
102
  ```ts
92
- import { registerFont, renderPDF, RenderPDFError } from 'renderpdf-js';
103
+ import { registerFont, renderPDF } from 'renderpdf-js';
93
104
 
94
- // Fetch a licensed static TTF/OTF program owned by the application.
105
+ // Supply a licensed static TTF/OTF file from your application.
95
106
  const response = await fetch('/fonts/MyFont-Regular.ttf');
96
107
  if (!response.ok) throw new Error('The document font could not be loaded.');
97
- // Register before displaying/measuring the source; use a family owned by this registration.
108
+
98
109
  const regular = await registerFont({
99
110
  family: 'Document Sans',
100
111
  data: await response.arrayBuffer(),
101
112
  });
102
- // The source stays connected and keeps its existing browser-calculated dimensions.
113
+
103
114
  const source = document.querySelector<HTMLElement>('#invoice');
104
115
  if (!source) throw new Error('Invoice element was not found.');
105
116
  source.style.fontFamily = 'Document Sans';
106
117
 
107
- try {
108
- // At the default A4 margins the source must fit within approximately 745.7 CSS pixels.
109
- const pdf = await renderPDF(source, {
110
- fonts: [regular],
111
- format: 'A4',
112
- orientation: 'portrait',
113
- margin: 24,
114
- rasterScale: 2,
115
- fallback: 'auto',
116
- pageBreak: 'css',
117
- onWarning: (warning) => console.info(warning.code, warning.message),
118
- });
119
- await pdf.download('invoice.pdf');
120
- // Independent byte and Blob forms do not render the source again.
121
- const bytes = await pdf.toUint8Array();
122
- // Applications can preview, store or upload this Blob themselves if required.
123
- const blob = await pdf.toBlob();
124
- console.info(pdf.pageCount, bytes.byteLength, blob.type);
125
- } catch (error) {
126
- if (error instanceof RenderPDFError) console.error(error.code, error.message);
127
- else throw error;
128
- }
118
+ const pdf = await renderPDF(source, {
119
+ fonts: [regular],
120
+ format: 'A4',
121
+ margin: 24,
122
+ });
123
+
124
+ await pdf.download('invoice.pdf');
129
125
  ```
130
126
 
131
- Register each used family, weight and style explicitly, including bold headings.
132
- `registerFont({ family, data, weight: 700, style: 'normal' })` registers a bold
133
- program; italic uses `style: 'italic'`. The first computed CSS family must match a
134
- provided handle. System fonts cannot be recovered through browser layout APIs.
135
- Registration copies bytes, rejects conflicting browser faces and reuses identical
136
- registrations. Importing the module is safe without a DOM; conversion and font
137
- registration require a browser document.
138
- Variable fonts, WOFF/WOFF2 and font collections are rejected; supply static
139
- TTF/OTF instances. Do not remove or replace registered browser faces during use.
127
+ Register every font family, weight and style used by the source, including bold
128
+ and italic faces. For example, add `weight: 700` when registering a bold font.
129
+ The first computed CSS font family must match a registered face. Static TTF/OTF
130
+ files are required; system fonts, variable fonts, WOFF/WOFF2 and font collections
131
+ are unsupported.
140
132
 
141
- ### Repeated table headers and page text
133
+ Keep the source connected to the document and unchanged during export. Its width
134
+ and wrapping are preserved, so it must already fit the page's content width:
135
+ about **745.7 CSS pixels** for A4 with the default margins. Oversized HTML is not
136
+ automatically scaled down.
142
137
 
143
- Repeated table headers and page text are available from version 0.2.0.
138
+ ## Standalone image files
139
+
140
+ `imageToPDF` accepts a `File`, `Blob`, `ArrayBuffer` or `Uint8Array`. It detects
141
+ the format from the file contents and requires no HTML element or fonts.
142
+
143
+ ```ts
144
+ import { imageToPDF } from 'renderpdf-js';
145
+
146
+ const fileInput = document.querySelector<HTMLInputElement>('#image');
147
+ const file = fileInput?.files?.[0];
148
+ if (!file) throw new Error('Select a PNG, JPEG or TIFF file.');
149
+
150
+ const pdf = await imageToPDF(file, { format: 'A4', margin: 24 });
151
+ await pdf.download('document.pdf');
152
+ ```
153
+
154
+ PNG and JPEG produce one page. Each main TIFF frame produces a page in file order.
155
+ Images are centered and scaled to fit within the margins, preserving their aspect
156
+ ratio; small images can be enlarged. TIFF/EXIF orientations, including mirrored
157
+ orientations and PNG `eXIf`, are applied. JPEG compressed bytes are preserved.
158
+ Image text remains a bitmap; there is no OCR.
159
+
160
+ See [image format limits](#image-format-limits) for supported TIFF profiles.
161
+ TIFF support applies to this API; HTML `<img>` elements accept PNG/JPEG.
162
+
163
+ ## Options
164
+
165
+ Both conversion functions accept these settings:
166
+
167
+ | Option | Default | Description |
168
+ | ------------- | ----------------------- | --------------------------------------------------------------------- |
169
+ | `format` | `'A4'` | `'A4'`, `'A3'`, `'Letter'` or `{ width, height }`. |
170
+ | `orientation` | `'portrait'` | `'portrait'` or `'landscape'`. |
171
+ | `margin` | `24` | One value or `{ top, right, bottom, left }`. |
172
+ | `metadata` | Title: `'RenderPDF.js'` | Unicode `title`, `author`, `subject` and `keywords` (a string array). |
173
+ | `onProgress` | — | Receives `{ stage, completed, total }`. |
174
+ | `signal` | — | An `AbortSignal` for cancellation. |
175
+
176
+ Dimensions and margins use **CSS pixels**: `1 px = 0.75 PDF pt`. Custom paper
177
+ dimensions include margins; landscape swaps the supplied width and height.
178
+
179
+ HTML exports also accept:
180
+
181
+ | Option | Default | Description |
182
+ | -------------------- | -------- | -------------------------------------------------------------------------------------- |
183
+ | `fonts` | Required | Registered faces used by the source and page text. |
184
+ | `pageBreak` | `'css'` | Honor supported CSS breaks, or use `'auto'` for geometry alone. |
185
+ | `repeatTableHeaders` | `false` | Repeat native `thead` groups on continuation pages. |
186
+ | `header`, `footer` | — | One plain text line in a reserved area on every page. |
187
+ | `fallback` | `'auto'` | Rasterize supported local effects; `'strict'` requires explicit `data-raster` regions. |
188
+ | `rasterScale` | `2` | Raster pixels per CSS pixel. |
189
+ | `onWarning` | — | Receives export warnings. |
190
+
191
+ ### Repeated table headers and page text
144
192
 
145
193
  ```ts
146
194
  const pdf = await renderPDF(source, {
147
195
  fonts: [regular],
148
- margin: 24,
149
196
  repeatTableHeaders: true,
150
- header: { text: 'Account statement', height: 32, fontSize: 12 },
197
+ header: { text: 'Account statement', height: 32 },
151
198
  footer: {
152
199
  text: 'Page {pageNumber} of {pageCount}',
153
200
  height: 28,
@@ -158,233 +205,154 @@ const pdf = await renderPDF(source, {
158
205
  await pdf.download('statement.pdf');
159
206
  ```
160
207
 
161
- `repeatTableHeaders` defaults to `false`, preserving existing exports. Enabling it
162
- repeats native `thead` paint and selectable text on continuation pages of a table.
163
- The header's measured height and spacing reduce the available body space on those
164
- pages. The original header stays together with the first body row. Body rows and
165
- spanning cells remain indivisible; a header and row that cannot fit fail with
166
- `PAGINATION_FAILED`. Repeated headers require nonnested native tables with a
167
- visible `thead` followed by body rows; raster headers, overlapping table ranges,
168
- header rowspans and cells spanning the header/body boundary are unsupported. Table footers are
169
- not repeated.
170
- Header paint must fit inside the measured `thead` box; text overflowing into its
171
- surrounding spacing fails explicitly instead of being clipped on continuation pages.
172
-
173
- `header` and `footer` each reserve their required `height` in CSS pixels **in
174
- addition to** the page's outer margins. They accept one plain Latin text line,
175
- including Czech diacritics, with `{pageNumber}` and `{pageCount}` placeholders.
176
- The text is black, vertically centered, and remains selectable. `fontSize`
177
- defaults to 12 CSS pixels and `align` to `'left'`; `'center'` and `'right'` are
178
- also supported. `font` can select a registered handle included in `fonts`, and
179
- otherwise defaults to the first supplied face. Band text never wraps or shrinks
180
- silently: text that exceeds its reserved height or available width fails with
181
- `SOURCE_OVERFLOW`. Invalid settings or bands leaving no content area fail with
182
- `INVALID_OPTIONS`. HTML templates and per-page callbacks are not supported.
208
+ Header and footer `height` is required and reserves space **in addition to** the
209
+ outer margins. Each accepts one plain Latin text line, including Czech diacritics,
210
+ with `{pageNumber}` and `{pageCount}` placeholders. Text stays selectable and does
211
+ not wrap. `fontSize` defaults to `12`, `align` to `'left'`, and `font` to the first
212
+ face in `fonts`. Alignment can also be `'center'` or `'right'`. Text that does not
213
+ fit fails explicitly. HTML page templates and per-page callbacks are unsupported.
183
214
 
184
215
  ### Export progress and document properties
185
216
 
186
- Progress reporting and document properties are available from version 0.2.0.
187
-
188
217
  ```ts
189
218
  const pdf = await renderPDF(source, {
190
219
  fonts: [regular],
191
- metadata: {
192
- title: 'Account statement',
193
- author: 'Example Company',
194
- subject: 'Quarterly overview',
195
- keywords: ['statement', 'quarterly'],
196
- },
220
+ metadata: { title: 'Account statement', author: 'Example Company' },
197
221
  onProgress: ({ stage, completed, total }) => {
198
- // Keep the status element outside the document being exported.
199
- statusElement.textContent = `${stage}: ${completed}/${total}`;
222
+ console.log(stage, completed, total);
200
223
  },
201
224
  });
202
- await pdf.download('statement.pdf');
203
225
  ```
204
226
 
205
- Progress events are frozen records with `stage`, `completed` and `total` fields.
206
- Stages are `preparing`, `layout`, `pagination`, `resources`, `pages`, `saving`
207
- and `complete`, in that order. Counters describe work **within the current stage**,
208
- not elapsed-time percentages. Preparation, layout, pagination and saving use one
209
- work unit; resources count font programs and processed native image/raster
210
- references, including reused image references. The page stage counts finished
211
- physical pages; its total is known after pagination. A successful export ends
212
- with `{ stage: 'complete', completed: 1, total: 1 }` after source validation.
213
-
214
- Providing `onProgress` allows periodic browser task yields so status updates and
215
- cancellation can run during export. Without it, conversion adds no progress
216
- timers. Callbacks are synchronous notifications; thrown callback errors reject
217
- the export with the original error. Keep the source static during callbacks as
218
- well as asynchronous rendering. Cancellation continues to use `signal` and the
219
- standard `AbortError`, with no partial PDF returned.
220
-
221
- `metadata` accepts optional Unicode `title`, `author`, `subject` and `keywords`
222
- properties. The keywords value is an array of strings. Settings and keyword
223
- entries are copied before the first asynchronous stage; later caller edits do
224
- not alter the current PDF. Invalid runtime values fail with `INVALID_OPTIONS`.
225
- The default title remains `RenderPDF.js`. Metadata does not add visible page
226
- content or change font requirements.
227
+ Counters describe work within the current stage, rather than elapsed-time
228
+ percentages. HTML stages are `preparing`, `layout`, `pagination`, `resources`,
229
+ `pages`, `saving` and `complete`. Image conversion omits `layout` and `pagination`.
230
+ Callbacks are synchronous; a thrown callback error rejects the export. Cancellation
231
+ is cooperative between stages and rejects with `AbortError`, without a partial PDF.
232
+
233
+ ## Output and errors
234
+
235
+ Both functions return the same result:
236
+
237
+ ```ts
238
+ await pdf.download('document.pdf');
239
+ const blob = await pdf.toBlob();
240
+ const bytes = await pdf.toUint8Array();
241
+ console.log(pdf.pageCount, pdf.warnings);
242
+ ```
243
+
244
+ These output methods reuse the completed PDF. `RenderPDFError`, exported from the
245
+ package, exposes a `code` and `message` for conversion failures. Warnings are
246
+ available through `onWarning` for HTML exports and `pdf.warnings`. Unsupported
247
+ input fails explicitly instead of silently producing a whole-page screenshot.
227
248
 
228
249
  ## Supported scope
229
250
 
230
- - Static block, inline, nonoverlapping Flexbox and Grid geometry with horizontal
231
- Latin text, including Czech diacritics.
232
- - Solid backgrounds, uniform solid rectangular borders and browser-measured
233
- padding, margins and alignment.
234
- - Native PNG/JPEG images loaded from data, blob, same-origin or CORS-readable URLs.
235
- Other image formats fail explicitly. External images can require a browser
236
- fetch at export time; document data is never submitted to a conversion service.
237
- JPEG EXIF orientation must be absent or 1; normalize rotated image pixels first.
238
- - Legacy `FONT`, `CENTER` and native tables with separate or collapsed borders.
239
- Collapsed borders are available from version 0.2.0. Table rows and rowspan cells remain atomic; oversized
240
- atomic content fails.
241
- - A4, A3 and Letter, portrait/landscape, a numeric margin or four explicit edges.
242
- Custom paper uses `format: { width: 500, height: 400 }`; dimensions include
243
- margins. Landscape swaps the supplied base axes, including custom dimensions.
244
- Page dimensions and margins use CSS pixels; `1 CSS px = 0.75 PDF pt`. The renderer preserves
245
- source width and wrapping and fails if it exceeds the page's content width.
246
- - Automatic local fallback for gradients, inline bitmap backgrounds, rounded
247
- corners, inset shadows and opacity on independently capturable rectangular
248
- blocks. Plain parents stay native; a selected parent owns all nested effects
249
- and is captured once in paint order. Descendant text adds extraction-only PDF
250
- objects where safe. Layout/order/overlap guards also apply inside automatic
251
- regions. Inline fragments and table-internal raster boundaries are rejected.
252
- Raster resources must be inline bitmaps; external resources, backdrop effects,
253
- transforms and effects outside the border box are rejected.
254
- Wrapped native inline backgrounds/borders need an explicit containing raster;
255
- a single union rectangle cannot reproduce their separate line fragments.
256
- - Explicit `data-raster` remains available. `fallback: 'strict'` disables automatic
257
- selection while honoring these caller-selected boundaries. Neither mode silently
258
- substitutes a whole-document screenshot when a local feature cannot be rendered.
259
- - `pageBreak: 'css'` (default) respects block/table-row `break-before: page` and
260
- `break-after: page`, their legacy `page-break-*: always` aliases, and
261
- `break-inside: avoid/avoid-page`. Before/after avoidance conservatively keeps
262
- adjacent complete block siblings together when they fit. `pageBreak: 'auto'`
263
- ignores authored fragmentation hints and uses only measured atomic geometry.
264
-
265
- Collapsed tables support cell and outer-table borders with `solid`, `none` and
266
- `hidden` styles, including different widths/colors on each side, transparency,
267
- `rowspan` and `colspan`. Shared edges resolve conflicts before native painting:
268
- hidden suppresses a border, otherwise the wider solid candidate wins; cells
269
- outrank the table on equal widths, followed by the upper/left cell in LTR layout.
270
- Crossing fills are partitioned so transparency is not applied twice. Original
271
- browser column widths and selectable text are retained; no table screenshot is
272
- used. Outer borders stay with the first/last complete rows during pagination.
273
-
274
- Collapsed row, row-group, column and column-group border contributions, other
275
- border styles, nested collapsed tables, captions and altered native table-part
276
- display modes fail explicitly. Keep these contributions on cells or the table
277
- itself. Repeated headers on collapsed tables additionally require the same
278
- resolved horizontal border segments, widths and colors at every possible body
279
- continuation boundary; incompatible combinations fail with `UNSUPPORTED_CSS`.
280
- Repeated headers on tables with separate borders retain their existing scope.
281
-
282
- Page mapping keeps consecutive source intervals: native backgrounds and boxes may
283
- span pages, while measured text lines, images, raster regions, table rows and
284
- rowspan cells remain atomic. Fitting avoid blocks move intact to the next page.
285
- An oversized or incompatible avoid preference relaxes with `BREAK_AVOID_RELAXED`;
286
- it never authorizes cutting an atomic item. Forced breaks take precedence over
287
- soft avoidance, and coincident/root-edge rules create no empty pages. A forced
288
- break through an atomic object, including a raster descendant or spanning cell,
289
- fails with `PAGINATION_FAILED`. Atomic content taller than a page also fails.
290
-
291
- Keep the source static while exporting. The renderer waits for fonts and image
292
- decoding, observes source and stylesheet mutations, and checks computed styles and
293
- geometry across asynchronous stages. A changed source fails with `SOURCE_CHANGED`.
294
- Pause animations before export. Cancellation uses `signal: controller.signal` and
295
- rejects with the standard `AbortError`; it is cooperative between rendering
296
- stages. No partial PDF is returned. Temporary observers and download URLs are
297
- released.
298
-
299
- ## Diagnostics and limits
300
-
301
- `RenderPDFError.code` distinguishes invalid options/source, missing fonts/glyphs,
302
- unsupported CSS/elements, failed resources, source changes/overflow, pagination,
303
- raster and PDF writing failures. Warnings are available through both `onWarning`
304
- and the immutable `result.warnings` collection, without runtime console logging.
305
-
306
- PDF text uses browser word origins and fontkit's shaped glyph advances and offsets;
307
- glyph outlines retain their original proportions. Default OpenType ligatures,
308
- kerning (including `font-kerning: none`) and combining marks retain a selectable
309
- Unicode text layer. Baselines still use Canvas font metrics. Nondefault font
310
- features/variants, stretched faces, font-size adjustment, automatic hyphenation
311
- and soft hyphens fail explicitly. Keep shaped words in one text node; cross-inline
312
- ligatures and zero-width text fragments are unsupported and reject explicitly.
313
- Exact output across shaping engines, arbitrary ligatures, complex scripts, bidi
314
- and variable font instances are not a fidelity promise. There is no arbitrary CSS/stacking
315
- engine. Collapsed tables, repeated table headers and plain page text
316
- use the explicit options described above; arbitrary HTML page templates are unsupported.
317
- Pagination is a measured source-to-page mapping, not full CSS Paged Media: parity
318
- breaks (`left/right/recto/verso`), column breaks, inline/cell break directives,
319
- margin-break rules, widow/orphan line-count policies and cloned page decorations
320
- are unsupported. Nondefault native `widows`/`orphans` and
321
- `box-decoration-break: clone` fail explicitly. Default line-count values are not
322
- enforced. Unsupported visible behavior must fail rather than silently produce an
323
- apparently successful export.
324
- Direct text overlapping later visible paint is also rejected when the simple
325
- ordered paint list cannot reproduce the browser's block/inline paint phases.
251
+ HTML rendering supports static blocks, inline content, nonoverlapping Flexbox and
252
+ Grid layouts, horizontal Latin text (including Czech diacritics), solid backgrounds,
253
+ rectangular borders and native tables with `rowspan` and `colspan`.
254
+
255
+ PNG/JPEG resources can use data, blob, same-origin or CORS-readable URLs. External
256
+ images may be fetched by the browser; documents are never sent to a conversion
257
+ service. JPEG orientation in HTML must be absent or `1`; normalize rotated pixels
258
+ before using them in an `<img>` element.
259
+
260
+ Gradients, rounded corners, inline bitmap backgrounds, inset shadows and opacity
261
+ can use local raster fallback on supported rectangular blocks. `data-raster`
262
+ selects a region explicitly. Regions are captured once in paint order; descendant
263
+ text gets an extraction layer where safe. Raster resources must be inline bitmaps;
264
+ external resources, transforms, backdrop effects and effects outside the border
265
+ box are unsupported. Wrapped inline backgrounds or borders require a containing
266
+ raster region; inline and table-internal raster boundaries are unsupported.
267
+
268
+ ### Pagination and tables
269
+
270
+ The default `pageBreak: 'css'` respects block/table-row `break-before: page`,
271
+ `break-after: page`, their legacy `page-break-*: always` aliases and
272
+ `break-inside: avoid/avoid-page`. Before/after avoidance keeps adjacent complete
273
+ block siblings together when they fit.
274
+
275
+ Text lines, images, raster regions, table rows and spanning cells stay whole.
276
+ Content taller than a page, or a forced break through an indivisible item, fails
277
+ with `PAGINATION_FAILED`. An avoid preference that cannot be honored can be relaxed
278
+ with a `BREAK_AVOID_RELAXED` warning. Parity/column breaks, inline/cell break rules,
279
+ widow/orphan policies and cloned page decorations are unsupported.
280
+
281
+ <details>
282
+ <summary>Table restrictions</summary>
283
+
284
+ Collapsed tables support solid, none and hidden borders on cells and the outer
285
+ table, including different widths, colors and transparency. Shared borders are
286
+ painted once. Row, row-group, column and column-group border contributions, other
287
+ border styles, captions, nested collapsed tables and altered table-part display
288
+ modes are unsupported.
289
+
290
+ Repeated headers require a visible native `thead` followed by body rows. The
291
+ header reserves space on continuation pages and stays with the first body row.
292
+ Headers and rows must fit on a page. Nested tables, raster headers, overlapping
293
+ table ranges, header rowspans and cells spanning the header/body boundary are
294
+ unsupported. Header paint must fit its measured box. Table footers are not repeated.
295
+ Collapsed tables also require matching resolved horizontal borders at every
296
+ possible header/body continuation boundary.
297
+
298
+ </details>
299
+
300
+ ### Text and layout limits
301
+
302
+ Default kerning, ligatures and combining marks are supported. Keep shaped words in
303
+ one text node; cross-inline ligatures and zero-width fragments are unsupported.
304
+ Complex scripts, bidirectional text, custom font features/variants, stretched
305
+ faces, font-size adjustment, automatic hyphenation and soft hyphens are unsupported.
306
+ Exact text appearance can vary between shaping engines.
307
+
308
+ Arbitrary CSS stacking and overlapping paint are unsupported. Pause animations
309
+ and keep source styles, geometry and registered fonts stable during export.
310
+ Changes to the source fail with `SOURCE_CHANGED`.
311
+
312
+ ### Image format limits
313
+
314
+ - **PNG:** static images only; APNG is rejected.
315
+ - **JPEG:** 8-bit baseline, extended and progressive grayscale, RGB or CMYK.
316
+ - **TIFF:** classic stripped TIFF in either byte order; common bilevel/grayscale,
317
+ palette and 8-bit RGB/RGBA profiles. Compression: uncompressed, CCITT RLE/Group
318
+ 3/Group 4, LZW, PackBits and Deflate. Limited to **40 million pixels per frame**
319
+ and **1,000 main frames**.
320
+
321
+ BigTIFF, tiled TIFF, SubIFDs/RAW layouts, planar-separated samples, 16-bit/float
322
+ samples and JPEG-compressed TIFF fail with `UNSUPPORTED_IMAGE`. The TIFF decoder
323
+ loads only when needed. Structural checks reject malformed input, but cannot
324
+ identify every damaged JPEG scan or CCITT bitstream.
326
325
 
327
326
  ## Development
328
327
 
329
- Development requires Node.js 22.18+ on the 22.x release line, 24.11+ on 24.x, or
330
- 26+. The `.node-version` file records the verified 22.22.0 runtime. From a source checkout:
328
+ Use Node.js 22.18+ (22.x), 24.11+ (24.x) or 26+. `.node-version` records the
329
+ verified runtime.
331
330
 
332
331
  ```sh
333
332
  npm ci
334
333
  npm run build
334
+ npm run check
335
335
  ```
336
336
 
337
- Source code lives in `src/`. Browser regression pages live in `tests/fixtures/`;
338
- their measurement helpers in `experiments/` use the same rendering pipeline as
339
- the public API. `npm run dev` serves these fixtures for development, while
340
- `npm run build` builds only the library.
341
-
342
- The independent HTML editor in `playground/` installs the
343
- packed library as an npm dependency. It provides a live preview and PDF download
344
- without importing renderer source files.
345
-
346
- Vite+ provides the development server, builds, Oxlint, Oxfmt and Vitest. Shared
347
- lint, format and unit-test settings live in `vite.config.ts`; the library build
348
- uses `vite.library.config.ts` and TypeScript to generate declarations. The toolchain
349
- is installed locally by `npm ci`; a global Vite+ installation is optional.
350
-
351
- The playground deliberately uses one explicitly stated font family;
352
- the reusable API supports multiple families. Private documents and local system
353
- font programs belong only in ignored `artifacts/`; regression tests use synthetic
354
- documents and do not require them.
355
-
356
- Run `npm run check` before submitting a change. It checks types, lint, unit tests,
357
- browser tests, builds, formatting and package contents. Browser tests require an installed Google
358
- Chrome and use synthetic fixtures, Playwright and PDF.js; those tools never run in
359
- conversion code. Generated PDFs, screenshots and reports stay outside versioned
360
- source files.
361
-
362
- Typography regressions compare tight browser/PDF text crops for Noto Sans regular,
363
- bold and italic plus Noto Serif, including Czech accents, ligatures, kerning and
364
- wrapped mixed-font lines on multiple pages. The [benchmark guide](https://github.com/MichalekJan93/renderPDF.js/blob/main/BENCHMARKS.md)
365
- describes repeatable exports of small, medium, large and hybrid documents.
366
- Run `npm run benchmark` to record timings separately from correctness checks.
367
- GitHub Actions runs the checks and preserves rendered comparisons and timing reports.
368
-
369
- ### Local package
370
-
371
- `npm pack --pack-destination artifacts` builds the library and checks its contents
372
- before creating a local archive. Create the `artifacts/` directory first if it is
373
- absent. The package includes the ESM entry, TypeScript declarations, package metadata,
374
- README, MIT license and README logo. Demos, test fonts, source files and local
375
- documents are excluded by an explicit `files` allowlist.
376
-
377
- `npm run check:package` checks the current build through npm's dry-run inventory;
378
- run `npm run build:library` first when no build exists. The check rejects missing
379
- entry points, empty files and unexpected contents. Packaging does not publish the
380
- library. Maintainers publish a reviewed archive with
381
- `npm publish ./artifacts/renderpdf-js-<version>.tgz --access public` after updating
382
- the package version and verifying the packed consumer.
337
+ `npm run check` runs formatting, lint, types, unit tests, browser tests, build and
338
+ package checks. Google Chrome must be installed for browser tests. Vite+ provides
339
+ the development toolchain; Playwright and PDF.js are used for testing.
340
+
341
+ Run `npm run dev` to serve synthetic test fixtures. The independent
342
+ [playground](https://github.com/MichalekJan93/renderPDF.js/tree/main/playground)
343
+ tests the packaged library. See the
344
+ [benchmark guide](https://github.com/MichalekJan93/renderPDF.js/blob/main/BENCHMARKS.md)
345
+ for `npm run benchmark` and timing reports.
346
+
347
+ To create a local package, create `artifacts/` if needed, then run
348
+ `npm pack --pack-destination artifacts`. Only the library build, declarations,
349
+ package metadata, README, license and logo are packaged. Keep private documents
350
+ and generated files out of Git.
383
351
 
384
352
  ## License
385
353
 
386
- RenderPDF.js is licensed under the [MIT License](LICENSE). The TIFF decoder
387
- `utif2` retains its MIT license; its `pako` dependency retains its MIT/Zlib notices.
388
- Runtime dependencies remain external to the library bundle. Bundled Noto Sans and Noto Serif test
389
- fonts retain their [SIL Open Font License](https://github.com/MichalekJan93/renderPDF.js/blob/main/public/fonts/README.md); they are not
390
- bundled into the library entry. Third-party dependencies retain their own licenses.
354
+ [MIT](https://github.com/MichalekJan93/renderPDF.js/blob/main/LICENSE).
355
+ Third-party dependencies retain their own licenses: `utif2` uses MIT and `pako`
356
+ retains MIT/Zlib notices. Noto Sans and Noto Serif test fonts retain the
357
+ [SIL Open Font License](https://github.com/MichalekJan93/renderPDF.js/blob/main/public/fonts/README.md)
358
+ and are not included in the npm package.