renderpdf-js 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jan Michálek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,224 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/MichalekJan93/renderPDF.js/main/assets/renderpdf-logo.png" alt="RenderPDF.js logo" width="560" />
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ # RenderPDF.js
6
+
7
+ An open-source TypeScript library for converting HTML/CSS to PDF in the browser,
8
+ with selectable text, local raster fallbacks and automatic pagination. The browser
9
+ calculates layout; RenderPDF.js uses its measured text, box, image and table
10
+ coordinates to write the PDF. Conversion runs entirely on the client.
11
+
12
+ The renderer is experimental and supports the bounded subset described below.
13
+ Chromium is the verified browser; Firefox/Safari compatibility has not been
14
+ established.
15
+
16
+ ## Getting started
17
+
18
+ Install the library in a browser application with a bundler, such as Vite:
19
+
20
+ ```sh
21
+ npm install renderpdf-js
22
+ ```
23
+
24
+ The package provides an ESM entry and TypeScript declarations. Version 0.1.x is
25
+ experimental; review the supported scope and limits before using it for documents.
26
+ To try HTML preview and PDF download from a source checkout, run the independent
27
+ [playground](https://github.com/MichalekJan93/renderPDF.js/tree/main/playground).
28
+
29
+ ## Usage
30
+
31
+ Import the public API from the installed package in your application's browser entry:
32
+
33
+ ```ts
34
+ import { registerFont, renderPDF, RenderPDFError } from 'renderpdf-js';
35
+
36
+ // Fetch a licensed static TTF/OTF program owned by the application.
37
+ const response = await fetch('/fonts/MyFont-Regular.ttf');
38
+ if (!response.ok) throw new Error('The document font could not be loaded.');
39
+ // Register before displaying/measuring the source; use a family owned by this registration.
40
+ const regular = await registerFont({
41
+ family: 'Document Sans',
42
+ data: await response.arrayBuffer(),
43
+ });
44
+ // The source stays connected and keeps its existing browser-calculated dimensions.
45
+ const source = document.querySelector<HTMLElement>('#invoice');
46
+ if (!source) throw new Error('Invoice element was not found.');
47
+ source.style.fontFamily = 'Document Sans';
48
+
49
+ try {
50
+ // At the default A4 margins the source must fit within approximately 745.7 CSS pixels.
51
+ const pdf = await renderPDF(source, {
52
+ fonts: [regular],
53
+ format: 'A4',
54
+ orientation: 'portrait',
55
+ margin: 24,
56
+ rasterScale: 2,
57
+ fallback: 'auto',
58
+ pageBreak: 'css',
59
+ onWarning: (warning) => console.info(warning.code, warning.message),
60
+ });
61
+ await pdf.download('invoice.pdf');
62
+ // Independent byte and Blob forms do not render the source again.
63
+ const bytes = await pdf.toUint8Array();
64
+ // Applications can preview, store or upload this Blob themselves if required.
65
+ const blob = await pdf.toBlob();
66
+ console.info(pdf.pageCount, bytes.byteLength, blob.type);
67
+ } catch (error) {
68
+ if (error instanceof RenderPDFError) console.error(error.code, error.message);
69
+ else throw error;
70
+ }
71
+ ```
72
+
73
+ Register each used family, weight and style explicitly, including bold headings.
74
+ `registerFont({ family, data, weight: 700, style: 'normal' })` registers a bold
75
+ program; italic uses `style: 'italic'`. The first computed CSS family must match a
76
+ provided handle. System fonts cannot be recovered through browser layout APIs.
77
+ Registration copies bytes, rejects conflicting browser faces and reuses identical
78
+ registrations. Importing the module is safe without a DOM; conversion and font
79
+ registration require a browser document.
80
+ Variable fonts, WOFF/WOFF2 and font collections are rejected; supply static
81
+ TTF/OTF instances. Do not remove or replace registered browser faces during use.
82
+
83
+ ## Supported scope
84
+
85
+ - Static block, inline, nonoverlapping Flexbox and Grid geometry with horizontal
86
+ Latin text, including Czech diacritics.
87
+ - Solid backgrounds, uniform solid rectangular borders and browser-measured
88
+ padding, margins and alignment.
89
+ - Native PNG/JPEG images loaded from data, blob, same-origin or CORS-readable URLs.
90
+ Other image formats fail explicitly. External images can require a browser
91
+ fetch at export time; document data is never submitted to a conversion service.
92
+ JPEG EXIF orientation must be absent or 1; normalize rotated image pixels first.
93
+ - Legacy `FONT`, `CENTER` and simple tables with separate borders. Table rows and
94
+ rowspan cells remain atomic; oversized atomic content fails.
95
+ - A4, A3 and Letter, portrait/landscape, a numeric margin or four explicit edges.
96
+ Custom paper uses `format: { width: 500, height: 400 }`; dimensions include
97
+ margins. Landscape swaps the supplied base axes, including custom dimensions.
98
+ Page dimensions and margins use CSS pixels; `1 CSS px = 0.75 PDF pt`. The renderer preserves
99
+ source width and wrapping and fails if it exceeds the page's content width.
100
+ - Automatic local fallback for gradients, inline bitmap backgrounds, rounded
101
+ corners, inset shadows and opacity on independently capturable rectangular
102
+ blocks. Plain parents stay native; a selected parent owns all nested effects
103
+ and is captured once in paint order. Descendant text adds extraction-only PDF
104
+ objects where safe. Layout/order/overlap guards also apply inside automatic
105
+ regions. Inline fragments and table-internal raster boundaries are rejected.
106
+ Raster resources must be inline bitmaps; external resources, backdrop effects,
107
+ transforms and effects outside the border box are rejected.
108
+ Wrapped native inline backgrounds/borders need an explicit containing raster;
109
+ a single union rectangle cannot reproduce their separate line fragments.
110
+ - Explicit `data-raster` remains available. `fallback: 'strict'` disables automatic
111
+ selection while honoring these caller-selected boundaries. Neither mode silently
112
+ substitutes a whole-document screenshot when a local feature cannot be rendered.
113
+ - `pageBreak: 'css'` (default) respects block/table-row `break-before: page` and
114
+ `break-after: page`, their legacy `page-break-*: always` aliases, and
115
+ `break-inside: avoid/avoid-page`. Before/after avoidance conservatively keeps
116
+ adjacent complete block siblings together when they fit. `pageBreak: 'auto'`
117
+ ignores authored fragmentation hints and uses only measured atomic geometry.
118
+
119
+ Page mapping keeps consecutive source intervals: native backgrounds and boxes may
120
+ span pages, while measured text lines, images, raster regions, table rows and
121
+ rowspan cells remain atomic. Fitting avoid blocks move intact to the next page.
122
+ An oversized or incompatible avoid preference relaxes with `BREAK_AVOID_RELAXED`;
123
+ it never authorizes cutting an atomic item. Forced breaks take precedence over
124
+ soft avoidance, and coincident/root-edge rules create no empty pages. A forced
125
+ break through an atomic object, including a raster descendant or spanning cell,
126
+ fails with `PAGINATION_FAILED`. Atomic content taller than a page also fails.
127
+
128
+ Keep the source static while exporting. The renderer waits for fonts and image
129
+ decoding, observes source and stylesheet mutations, and checks computed styles and
130
+ geometry across asynchronous stages. A changed source fails with `SOURCE_CHANGED`.
131
+ Pause animations before export. Cancellation uses `signal: controller.signal` and
132
+ rejects with the standard `AbortError`; it is cooperative between rendering
133
+ stages. No partial PDF is returned. Temporary observers and download URLs are
134
+ released.
135
+
136
+ ## Diagnostics and limits
137
+
138
+ `RenderPDFError.code` distinguishes invalid options/source, missing fonts/glyphs,
139
+ unsupported CSS/elements, failed resources, source changes/overflow, pagination,
140
+ raster and PDF writing failures. Warnings are available through both `onWarning`
141
+ and the immutable `result.warnings` collection, without runtime console logging.
142
+
143
+ PDF text uses browser word origins and fontkit's shaped glyph advances and offsets;
144
+ glyph outlines retain their original proportions. Default OpenType ligatures,
145
+ kerning (including `font-kerning: none`) and combining marks retain a selectable
146
+ Unicode text layer. Baselines still use Canvas font metrics. Nondefault font
147
+ features/variants, stretched faces, font-size adjustment, automatic hyphenation
148
+ and soft hyphens fail explicitly. Keep shaped words in one text node; cross-inline
149
+ ligatures and zero-width text fragments are unsupported and reject explicitly.
150
+ Exact output across shaping engines, arbitrary ligatures, complex scripts, bidi
151
+ and variable font instances are not a fidelity promise. There is no arbitrary CSS/stacking
152
+ engine, collapsed table borders, repeated table headers or header/footer engine.
153
+ Pagination is a measured source-to-page mapping, not full CSS Paged Media: parity
154
+ breaks (`left/right/recto/verso`), column breaks, inline/cell break directives,
155
+ margin-break rules, widow/orphan line-count policies and cloned page decorations
156
+ are unsupported. Nondefault native `widows`/`orphans` and
157
+ `box-decoration-break: clone` fail explicitly. Default line-count values are not
158
+ enforced. Unsupported visible behavior must fail rather than silently produce an
159
+ apparently successful export.
160
+ Direct text overlapping later visible paint is also rejected when the simple
161
+ ordered paint list cannot reproduce the browser's block/inline paint phases.
162
+
163
+ ## Development
164
+
165
+ Development requires Node.js 22.18+ on the 22.x release line, 24.11+ on 24.x, or
166
+ 26+. The `.node-version` file records the verified 22.22.0 runtime. From a source checkout:
167
+
168
+ ```sh
169
+ npm ci
170
+ npm run build
171
+ ```
172
+
173
+ Source code lives in `src/`. Browser regression pages live in `tests/fixtures/`;
174
+ their measurement helpers in `experiments/` use the same rendering pipeline as
175
+ the public API. `npm run dev` serves these fixtures for development, while
176
+ `npm run build` builds only the library.
177
+
178
+ The independent HTML editor in `playground/` installs the
179
+ packed library as an npm dependency. It provides a live preview and PDF download
180
+ without importing renderer source files.
181
+
182
+ Vite+ provides the development server, builds, Oxlint, Oxfmt and Vitest. Shared
183
+ lint, format and unit-test settings live in `vite.config.ts`; the library build
184
+ uses `vite.library.config.ts` and TypeScript to generate declarations. The toolchain
185
+ is installed locally by `npm ci`; a global Vite+ installation is optional.
186
+
187
+ The playground deliberately uses one explicitly stated font family;
188
+ the reusable API supports multiple families. Private documents and local system
189
+ font programs belong only in ignored `artifacts/`; regression tests use synthetic
190
+ documents and do not require them.
191
+
192
+ Run `npm run check` before submitting a change. It checks types, lint, unit tests,
193
+ browser tests, builds, formatting and package contents. Browser tests require an installed Google
194
+ Chrome and use synthetic fixtures, Playwright and PDF.js; those tools never run in
195
+ conversion code. Generated PDFs, screenshots and reports stay outside versioned
196
+ source files.
197
+
198
+ Typography regressions compare tight browser/PDF text crops for Noto Sans regular,
199
+ bold and italic plus Noto Serif, including Czech accents, ligatures, kerning and
200
+ wrapped mixed-font lines on multiple pages. The [benchmark guide](https://github.com/MichalekJan93/renderPDF.js/blob/main/BENCHMARKS.md)
201
+ describes repeatable exports of small, medium, large and hybrid documents.
202
+ Run `npm run benchmark` to record timings separately from correctness checks.
203
+ GitHub Actions runs the checks and preserves rendered comparisons and timing reports.
204
+
205
+ ### Local package
206
+
207
+ `npm pack --pack-destination artifacts` builds the library and checks its contents
208
+ before creating a local archive. Create the `artifacts/` directory first if it is
209
+ absent. The package includes the ESM entry, TypeScript declarations, package metadata,
210
+ README, MIT license and README logo. Demos, test fonts, source files and local
211
+ documents are excluded by an explicit `files` allowlist.
212
+
213
+ `npm run check:package` checks the current build through npm's dry-run inventory;
214
+ run `npm run build:library` first when no build exists. The check rejects missing
215
+ entry points, empty files and unexpected contents. Packaging does not publish the
216
+ library. Maintainers publish a reviewed archive with
217
+ `npm publish ./artifacts/renderpdf-js-<version>.tgz --access public` after updating
218
+ the package version and verifying the packed consumer.
219
+
220
+ ## License
221
+
222
+ RenderPDF.js is licensed under the [MIT License](LICENSE). Bundled Noto Sans and Noto Serif test
223
+ fonts retain their [SIL Open Font License](https://github.com/MichalekJan93/renderPDF.js/blob/main/public/fonts/README.md); they are not
224
+ bundled into the library entry. Third-party dependencies retain their own licenses.
Binary file
@@ -0,0 +1,84 @@
1
+ import type { RenderWarning } from './diagnostics.js';
2
+ /** One explicit static font program registered for browser layout and PDF embedding. */
3
+ export interface RegisteredFont {
4
+ /** CSS family name; provide the same family in the source element's styles. */
5
+ readonly family: string;
6
+ /** Explicit CSS weight of this static face, without browser-synthesized styling. */
7
+ readonly weight: number;
8
+ /** Supported face style. */
9
+ readonly style: 'normal' | 'italic';
10
+ /** Exact font bytes; registration owns its program rather than extracting system fonts. */
11
+ readonly bytes: Uint8Array;
12
+ }
13
+ /** Input for explicit font registration; bytes are supplied by the application. */
14
+ export interface FontSource {
15
+ /** CSS family to register in the current document. */
16
+ family: string;
17
+ /** Complete TTF/OTF program available to the browser and PDF adapter. */
18
+ data: Uint8Array | ArrayBuffer;
19
+ /** Static face weight, defaulting to 400. */
20
+ weight?: number;
21
+ /** Static face style, defaulting to normal. */
22
+ style?: 'normal' | 'italic';
23
+ }
24
+ /** Physical page and source-to-page margins in CSS reference pixels. */
25
+ export interface PageGeometry {
26
+ /** Page width in CSS reference pixels (1px = 0.75pt). */
27
+ width: number;
28
+ /** Page height in CSS reference pixels. */
29
+ height: number;
30
+ /** Empty page edges in CSS pixels. */
31
+ margin: PageMargins;
32
+ }
33
+ /** Per-edge empty page space, measured in CSS pixels. */
34
+ export interface PageMargins {
35
+ /** Top empty edge. */
36
+ top: number;
37
+ /** Right empty edge. */
38
+ right: number;
39
+ /** Bottom empty edge. */
40
+ bottom: number;
41
+ /** Left empty edge. */
42
+ left: number;
43
+ }
44
+ /** Explicit physical paper dimensions in the shared CSS reference pixel units. */
45
+ export interface CustomPageSize {
46
+ /** Base paper width in CSS pixels; landscape orientation swaps the supplied axes. */
47
+ width: number;
48
+ /** Base paper height in CSS pixels, including margins. */
49
+ height: number;
50
+ }
51
+ /** Small supported API surface; unsupported layout fails explicitly. */
52
+ export interface RenderPDFOptions {
53
+ /** Explicit registered faces used by the supplied source; no implicit font substitution. */
54
+ fonts: readonly RegisteredFont[];
55
+ /** Standard page size (default A4) or explicit physical dimensions in CSS pixels. */
56
+ format?: 'A4' | 'A3' | 'Letter' | CustomPageSize;
57
+ /** Page orientation, defaulting to portrait. */
58
+ orientation?: 'portrait' | 'landscape';
59
+ /** CSS-pixel margins, defaulting to 24 on every edge; content is never implicitly rescaled. */
60
+ margin?: number | PageMargins;
61
+ /** Physical pixels per CSS pixel in selected local raster regions, defaulting to 2. */
62
+ rasterScale?: number;
63
+ /** Automatically rasterize safe local effects (default auto); strict requires explicit data-raster. */
64
+ fallback?: 'auto' | 'strict';
65
+ /** Honor supported CSS page breaks (default css), or use geometry-only automatic cuts. */
66
+ pageBreak?: 'auto' | 'css';
67
+ /** Stop preparation and subsequent rendering stages without producing a partial PDF. */
68
+ signal?: AbortSignal;
69
+ /** Observe documented approximations, raster choices and relaxed pagination preferences. */
70
+ onWarning?: (warning: RenderWarning) => void;
71
+ }
72
+ /** Complete PDF result with independently usable browser output forms. */
73
+ export interface RenderPDFResult {
74
+ /** Number of generated pages. */
75
+ readonly pageCount: number;
76
+ /** Frozen diagnostics for this export; there is no production console logging. */
77
+ readonly warnings: readonly RenderWarning[];
78
+ /** Return a fresh byte copy so callers cannot corrupt future downloads. */
79
+ toUint8Array(): Promise<Uint8Array>;
80
+ /** Return a browser PDF Blob without triggering a download. */
81
+ toBlob(): Promise<Blob>;
82
+ /** Download the completed PDF without invoking a print dialog. */
83
+ download(filename?: string): Promise<void>;
84
+ }
@@ -0,0 +1,7 @@
1
+ import type { LayoutSnapshot } from './types.js';
2
+ /** Collect the supported CSS page-break subset from existing measured browser geometry. */
3
+ export declare function collectPageBreaks(
4
+ /** Connected current-document root; collection leaves its source DOM unchanged. */
5
+ root: HTMLElement,
6
+ /** Ordered display list identifying intrinsic raster boundaries and source extent. */
7
+ snapshot: LayoutSnapshot): Pick<LayoutSnapshot, 'forcedBreaks' | 'avoidBreakBounds'>;
@@ -0,0 +1,26 @@
1
+ /** Machine-readable failure reasons for the supported browser rendering pipeline. */
2
+ export type RenderErrorCode = 'INVALID_OPTIONS' | 'INVALID_SOURCE' | 'FONT_LOAD_FAILED' | 'MISSING_FONT' | 'MISSING_GLYPH' | 'UNSUPPORTED_CSS' | 'UNSUPPORTED_ELEMENT' | 'RESOURCE_LOAD_FAILED' | 'SOURCE_CHANGED' | 'SOURCE_OVERFLOW' | 'PAGINATION_FAILED' | 'RASTER_FAILED' | 'PDF_WRITE_FAILED';
3
+ /** A rendering failure with a stable code and an actionable English explanation. */
4
+ export declare class RenderPDFError extends Error {
5
+ /** Stable category that callers can branch on without parsing message text. */
6
+ readonly code: RenderErrorCode;
7
+ /** Construct an error without hiding its original lower-level cause. */
8
+ constructor(
9
+ /** Stable category that callers can branch on without parsing message text. */
10
+ code: RenderErrorCode,
11
+ /** Human-readable explanation of the failed requirement. */
12
+ message: string,
13
+ /** Optional original browser or adapter failure. */
14
+ options?: ErrorOptions);
15
+ }
16
+ /** Supported approximations and explicit local raster choices, reported without console noise. */
17
+ export interface RenderWarning {
18
+ /** Stable warning category for application diagnostics. */
19
+ readonly code: 'TEXT_METRICS_APPROXIMATED' | 'RASTER_FALLBACK' | 'RASTER_TEXT_OMITTED' | 'BREAK_AVOID_RELAXED';
20
+ /** Explanation of the behavior and its relevant fidelity limit. */
21
+ readonly message: string;
22
+ }
23
+ /** Stop at asynchronous stage boundaries using the browser's standard AbortError contract. */
24
+ export declare function throwIfAborted(
25
+ /** Optional caller cancellation signal. */
26
+ signal?: AbortSignal): void;
@@ -0,0 +1,9 @@
1
+ import type { FontSource, RegisteredFont } from './api-types.js';
2
+ /** Validate immutable registration handles against the current browser's live font set.
3
+ * @param fonts Explicit handles whose owned programs must still control browser font selection.
4
+ */
5
+ export declare function validateRegisteredFonts(fonts: readonly RegisteredFont[]): void;
6
+ /** Register explicit static TTF/OTF bytes in the current browser document.
7
+ * @param source Caller-supplied program and its exact CSS family, weight, and style.
8
+ */
9
+ export declare function registerFont(source: FontSource): Promise<RegisteredFont>;
@@ -0,0 +1,4 @@
1
+ /** Identify native bitmap bytes and reject unsupported or ambiguous JPEG orientation. */
2
+ export declare function nativeImageFormat(
3
+ /** Original browser-accessible compressed image; no filename or MIME type is trusted. */
4
+ bytes: Uint8Array): 'png' | 'jpeg';
@@ -0,0 +1,5 @@
1
+ export { renderPDF } from './render-pdf.js';
2
+ export { registerFont } from './fonts.js';
3
+ export { RenderPDFError } from './diagnostics.js';
4
+ export type { RenderErrorCode, RenderWarning } from './diagnostics.js';
5
+ export type { FontSource, RegisteredFont, PageMargins, CustomPageSize, RenderPDFOptions, RenderPDFResult, } from './api-types.js';