Skip to content

About

Generate PDFs from HTML/CSS in Node.js and browser workers with the Fullbleed Rust engine, bundled fonts, and PNG previews.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Fullbleed for Node.js, browsers, and Cloudflare Workers

Generate PDFs from static HTML/CSS or reusable components. The package bundles Fullbleed's Rust engine as WebAssembly and four font faces; installation needs no Python, Rust compiler, browser, or system fonts. MIT licensed.

Use fullbleed in Node.js, fullbleed/browser in a web application, or fullbleed/cloudflare inside a Cloudflare Worker. Package 0.9.5 wraps the published Fullbleed 2.5.51 engine.

This release fixes a Windows worker-startup cancellation race. Aborted renders release their worker before the queue starts the next render.

A winning font-family declaration replaces an earlier variable-based family. If its variable cannot resolve, the text inherits its parent family instead of reviving a declaration that lost the cascade.

Numbering continues through display: contents wrappers, including their children and pseudo-elements. Selectors and inherited styles retain their document-tree ancestry.

Generated CSS counter labels follow document order, including a closing ::after label that counts the preceding items. This applies to ordinary PDF rendering and fixed or reflow batches. See the counter authoring guide for an example and the remaining scope and list-marker limitations.

Try the browser playground · Documentation · CSS coverage

Install and render

Use Node.js 22 or newer:

npm install fullbleed

Save this as invoice.mjs, then run node invoice.mjs:

import { writeFile } from 'node:fs/promises';
import { renderPdf } from 'fullbleed';

const result = await renderPdf({
  html: '<h1>Invoice NS-1042</h1><p>Consulting: USD 1,200.00</p>',
  css: '@page { size: A4; margin: 20mm } h1 { color: #175c52 }',
  previewDpi: 96,
});
await writeFile('invoice.pdf', result.pdf);
await writeFile('invoice.png', result.previews[0]);

Open invoice.pdf or its PNG preview. Inter is the default font. CommonJS also works: const { renderPdf } = require('fullbleed'). TypeScript declarations are included. For a pinned installation, use npm install --save-exact fullbleed@0.9.0.

Prefer components? Compose documents with functions or JSX and export HTML with or without styles using fullbleed/ui.

Try a complete project online: JavaScript + Vite or React + TypeScript. The React project starts with a typed component invoice and editable line items; the JavaScript project includes HTML/CSS templates. Preview the result and download a PDF. Each starter also includes a ZIP for local development.

Using Vue? The Vue and TypeScript starter includes editable templates, automatic previews, downloads, and a reusable usePdfPreview composable.

A designed invoice

Northstar invoice with cream paper, serif typography, green tables, and an orange accent.

The runnable example uses these same invoice sources and the bundled Inter, DM Serif Display, and Bebas Neue families. It escapes the customer name before inserting it into HTML. The data is fictional.

From a source checkout, after the build steps below:

npm run example

Open output/invoice/invoice.pdf and page-1.png. The three-page report sources demonstrate pagination, tables, and vector artwork.

Compose documents with components

Use named components, typed props, ordinary conditions and arrays, and reusable styles instead of HTML strings. Plain JavaScript functions need no React runtime or build step:

import { writeFile } from 'node:fs/promises';
import { renderPdf } from 'fullbleed';
import { Document, Heading, Paragraph, Strong, Stack, StyleSheet } from 'fullbleed/ui';

const Line = ({ label, amount }) => Paragraph(Strong(label), ': ', amount);
const invoice = Document({ title: 'Invoice', styles: new StyleSheet({
  '@page': { size: 'A4', margin: '20mm' },
  h1: { color: '#175c52' },
}) }, Stack({ gap: '12pt' },
  Heading({ level: 1 }, 'Invoice NS-1042'),
  Line({ label: 'Consulting', amount: 'USD 1,200.00' }),
));
const result = await invoice.renderPdf(renderPdf, { previewDpi: 96 });

await writeFile('invoice.pdf', result.pdf);
await writeFile('invoice.png', result.previews[0]);
console.log(`${result.pages} page; engine ${result.engineVersion}`);

The component guide includes JavaScript and TSX versions of a designed invoice, className styling, and HTML export with or without styles. The optional JSX/TSX runtime works with each renderer. Importing fullbleed/ui does not load the PDF renderer or add JavaScript to exported HTML.

Use it in Next.js

For a web application, start with the Next.js PDF download example. It includes a designed invoice, an App Router handler, production build settings for the worker and bundled assets, and a standalone-server verification script. The route bounds rendering and returns private PDF attachments from fictional data; connect your own authenticated record lookup when adapting it.

Use it in a browser

For React, use the React and TypeScript starter with a component-authored invoice, automatic previews, editable data, cancellation and a reusable preview hook. Its dedicated print TSX files use Fullbleed's JSX runtime.

Start with the complete browser application: editable HTML/CSS, invoice and report designs, previews, cancellation, and PDF downloads.

Copy the browser runtime into your application's static directory:

npx fullbleed-browser-assets public/fullbleed

Then use the separate browser entry from a page served over HTTPS or localhost:

import { createRenderer } from 'fullbleed/browser';

const renderer = createRenderer({ assetBaseUrl: '/fullbleed/' });
const result = await renderer.renderPdf({
  html: '<h1>Your document</h1>',
  css: 'h1 { color: #175c52 }',
  maxPages: 20,
});
const pdf = new Blob([result.pdf], { type: 'application/pdf' });

The result uses browser byte arrays. The worker loads the shipped engine/fonts from your site and renders locally, with cancellation, deadlines and structured errors. See the browser API, download example and hosting requirements.

Fonts and assets

Supply file bytes explicitly. Additional fonts are registered alongside the bundled fonts; use their actual family names in CSS. Asset names are relative to the virtual assets/ directory:

import { readFile } from 'node:fs/promises';
import { renderPdf } from 'fullbleed';

const result = await renderPdf({
  html: '<img src="assets/brand/logo.svg"><h1>Quarterly report</h1>',
  css: "body { font-family: 'Your Font Family', Inter } img { width: 80pt }",
  fonts: [await readFile('your-font.ttf')],
  assets: { 'brand/logo.svg': await readFile('logo.svg') },
});

To preserve a filename used by a CSS @font-face alias, pass a named font:

const result = await renderPdf({
  html: '<h1>Your brand</h1>',
  css: '@font-face { font-family: Brand; src: url("fonts/brand.ttf") } body { font-family: Brand }',
  fonts: [{ name: 'brand.ttf', data: await readFile('brand.ttf') }],
});

Named fonts work in Node.js, browsers, and Workers from 0.8.5. Names are unique TTF/OTF filenames without folders, up to 120 characters; spaces and Unicode are supported. Plain byte arrays continue to use the font's intrinsic family names. Both forms are copied when the job is accepted and count toward the same asset limit. A font file does not also need to be included in assets.

From 0.9.1, page headers and footers can use these same CSS aliases, including fallback lists and inherited weights. Their typography inherits from html, then @page, then the margin box. A font applied only to body does not set the page-header font. Use defaultStyles: false when your document should inherit its body font from html.

The WebAssembly engine sees an in-memory filesystem containing the supplied inputs and bundled fonts. It does not fetch remote URLs or read application files. Use the asset map or data URIs for document images. Render and review previews when adapting a template; unsupported CSS or an unavailable asset can affect output.

Use registered font families in CSS. The engine can substitute an unavailable family. Its missing-glyph report checks resolved fonts and is not an exhaustive font-substitution or visual-validation report.

Native previews of unembedded Standard 14 fonts use fixed, bundled OFL outline substitutes starting with 0.3.2. Register your chosen font files for a specific branded appearance in both the PDF and its preview. The Standard 14 checks cover the twelve Latin faces, an embedded-font control, and the blank-preview failure in public 0.3.1. Font provenance and complete license notices ship with both the npm package and copied browser runtime.

Generate a batch from one template

Use renderPdfBatch() for notices, letters, invoices, or statements that share a design. The engine compiles the template once per request and returns one PDF in record order, with page ranges and selected-font character reports:

import { writeFile } from 'node:fs/promises';
import { renderPdfBatch } from 'fullbleed';

const result = await renderPdfBatch({
  html: '<h1>Account {{account}}</h1><p>{{message}}</p>',
  css: '@page { size: A5; margin: 18mm } h1 { color: #175c52 }',
  records: [
    { account: '1042', message: 'Your account is up to date.' },
    { account: '1043', message: 'Your next review is scheduled for November.' },
  ],
});
await writeFile('notices.pdf', result.pdf);
for (const row of result.records) {
  console.log(`Record ${row.recordIndex}: pages ${row.pageOffset + 1}–${row.pageOffset + row.pageCount}`);
}

layout: 'reflow' is the default: longer text can wrap and add pages. Choose layout: 'fixed' for values that fit the compiled geometry; it retains the template's layout and selected fonts. Records contain plain strings, including literal < and > characters. Format numbers and dates in your application. Every record must have the same own enumerable fields, exactly matching the template's text bindings. Use ordinary renders for changes to document structure, image sources, attributes, or CSS.

Component-authored templates use the same batch function:

import { renderPdfBatch } from 'fullbleed';
import { Document, Heading, Paragraph } from 'fullbleed/ui';

const notice = Document(Heading('Account {{account}}'), Paragraph('{{message}}'));
const result = await notice.renderPdf(renderPdfBatch, {
  records: [{ account: '1042', message: 'Your account is up to date.' }],
});

Each result.records entry has zero-based recordIndex and pageOffset, a pageCount, and a missingGlyphs array. Missing characters reject the batch by default. With allowMissingGlyphs: true, inspect each entry's codepoint, char, count, and fontsTried before accepting the PDF. fontsTried identifies the first selected registered font where the character was absent. The top-level missingGlyphs number counts distinct missing code points across all records. These checks exclude Base-14 fonts and text in images; they do not establish correct shaping, layout, accessibility, or PDF conformance.

Batches accept the other rendering options, including previews and the combined maxPages limit. Up to 10,000 records are accepted. HTML, CSS, and all record keys/values share the 4,000,000 UTF-8 byte budget (1,000,000 in Cloudflare). The PDF, reports, and optional previews remain in memory; split large workloads according to your host's memory and CPU budget. Compilation is per request. The browser renderer exposes renderer.renderPdfBatch() and Cloudflare exports renderPdfBatch from fullbleed/cloudflare.

Limit concurrent renders

Create one queue per application process and reuse it across requests:

import { createRenderQueue } from 'fullbleed';

const documents = createRenderQueue({ concurrency: 2, maxQueue: 4 });
const result = await documents.renderPdf({
  html: '<h1>Queued invoice</h1>',
  isolation: 'process',
  timeoutMs: 30_000,
});
// Return result.pdf from your request handler.
// At application shutdown, cancel outstanding work and await cleanup:
await documents.close();

This queue admits two active renders and four waiting requests. Further requests reject with QUEUE_FULL, so a server can return a busy response. Waiting jobs can be aborted and their deadlines include the wait. Slots stay occupied until their worker or child process has exited. close() rejects new work, cancels accepted jobs with QUEUE_CLOSED, and waits for cleanup.

Choose limits for your host and document sizes. The queue bounds job counts, not total memory, and each application process has its own queue. It is an in-memory admission control; use your application's durable job system for persistence and delivery. See the queue guide and retained checks and the six-invoice batch example.

API

This section describes the Node entry. The browser entry returns browser byte arrays through a configured renderer; see its API guide.

await renderPdf(options) returns { pdf, previews, pages, missingGlyphs, engineVersion }. PDF and PNG values are Node Buffer objects. Previews are generated from the finalized PDF and are empty when disabled.

The report example uses gradient chart fills. Its finalized PDF previews and separate linear, translucent, and hard radial fixtures are checked against the native engine.

Option Behavior
html Required nonempty HTML string.
css CSS string; defaults to empty.
defaultStyles Default true: prepend body { font-family: Inter; }. Set false to pass CSS unchanged; bundled fonts remain available.
fonts Array of font bytes (Uint8Array/Buffer) or { name, data } objects that preserve TTF/OTF filenames for CSS aliases.
assets Map relative asset names to nonempty byte arrays.
previewDpi 0 disables previews; otherwise an integer from 36 to 300.
maxPages Reject larger documents. Default: 1000.
timeoutMs Deadline including initial engine loading. Default: 30000 ms.
signal An AbortSignal to cancel rendering.
isolation 'worker' (default) or 'process' for a separate Node process per call.
allowMissingGlyphs Default false: reject engine-reported missing glyphs.

Rendering runs in a dedicated worker. Concurrent calls use separate workers and document state. The returned promise waits for that worker to exit on success, render failure, timeout, or cancellation. Awaiting jobs in sequence therefore does not overlap their worker lifetimes. A timeout or abort starts termination; settlement includes the time needed to stop the worker.

Use isolation: 'process' when a render-process failure should become a rejected request while the calling application stays running. This mode runs the engine and its worker in a fresh Node process, and waits for process exit and IPC disconnection before settling. It requires a host that permits child processes and adds startup and memory overhead. It does not pool processes or retry jobs. See the guide and failure checks.

Bound concurrency in a server according to its available memory; each worker's WebAssembly memory has a 512 MiB ceiling. HTML and CSS together are limited to 4,000,000 UTF-8 bytes; supplied fonts and assets together to 64 MiB. Preview images can use substantially more memory than PDF-only rendering.

import { FullbleedError, renderPdf } from 'fullbleed';

try {
  const result = await renderPdf({
    html: '<h1>Monthly statement</h1>',
    maxPages: 20,
    timeoutMs: 10_000,
    isolation: 'process',
    signal: AbortSignal.timeout(8_000),
  });
  // Return result.pdf from your HTTP handler, or write it to your storage.
} catch (error) {
  if (error instanceof FullbleedError) console.error(error.code, error.message);
  else throw error;
}

Error codes include INVALID_INPUT, MISSING_GLYPHS, PAGE_LIMIT, TIMEOUT, ABORTED, ENGINE_LOAD_FAILED, ENGINE_LIMIT, RENDER_FAILED, PREVIEW_FAILED, WORKER_FAILED, and PROCESS_FAILED. Process failures may also include exitCode and signal. Failed or cancelled calls do not return a partial PDF. Caller-owned asset buffers are copied when a call starts.

createRenderQueue({ concurrency?, maxQueue? }) exposes renderPdf() and renderPdfBatch() with the same options and result. Defaults are one active render and sixteen waiting jobs. maxQueue: 0 rejects buffering. Accepted calls snapshot their strings and asset/font bytes immediately. The queue adds QUEUE_FULL and QUEUE_CLOSED, read-only activeCount, pendingCount, and closed properties, and an idempotent asynchronous close() method. It does not reuse workers or change the standalone renderPdf() concurrency behavior.

This API covers ordinary document generation and previews. It does not expose PDF/A, PDF/UA, PDF/X, template overlays, or compiled VDP. Use the Python or Rust APIs for those workflows and their verification requirements. Unknown options are rejected. Fullbleed uses static HTML/CSS for print layout; it does not execute document JavaScript or reproduce arbitrary browser pages.

Release notes and verification

Version 0.8.7 corrects radial-gradient endpoint colors and opacity and invalid zero-width gradient stitching intervals. The public JavaScript API and isolation options are unchanged.

Version 0.8.6 fixes authored whitespace and wrapping in code blocks. A pre block with white-space: pre-wrap can wrap within the page; long tokens can use overflow-wrap: anywhere. Default pre and explicit no-wrap policies retain their behavior. Documents requesting wrapping may gain pages.

Version 0.8.4 fixes text decorations across spaces. Underlines, overlines and strikethroughs continue across interior word spaces; nested spans retain the ancestor decoration, while collapsed line edges and atomic inline boxes keep their intended boundaries.

Version 0.8.3 fixes CSS-generated quotation marks in nested inline, block, inline-block and display: contents elements. Silent quotation tokens and layout measurement now preserve document-order quotation depth.

Version 0.8.2 fixes wrapping inside narrow tables: Japanese text and Latin text using word-break: break-all, overflow-wrap: anywhere or line-break: anywhere fit the authored column width, including nested spans and paragraphs. Explicit child widths and intentional no-wrap content keep their minimum widths.

Version 0.8.1 brings the stable engine's pagination correction: auto-height tables and containers with clipping can continue across PDF pages. Explicitly sized clipping viewports keep their existing behavior.

Package archives and retained PDF/PNG checks are attached to each GitHub release. Installation records describe published-package checks, including output hashes and failure recovery. Use the record for the version you are evaluating.

Earlier releases

Version 0.8.0 adds the optional Workers entry with bundled engine/font imports, shared input validation, and component authoring. It computes inside the Worker isolate, with CPU limits owned by Cloudflare. The Workers guide covers local Windows/Linux checks and a hosted Playground smoke test: six invoices produce identical PDF bytes in both environments. Production resource limits and Free-plan suitability still need qualification in your own deployment.

Version 0.7.1 brought the stable engine's SVG stylesheet and inset-shadow fixes to Node.js and browser workers. The paint checks cover stylesheet selectors, inherited SVG colors and opacity, and clipped, blurred, translated and negative-spread inset shadows. The component and static HTML APIs remain unchanged. The pinned engine also reads supported embedded <style> sheets; see component HTML export.

Version 0.7.0 added static HTML export with or without styles: doc.toHtml({ includeStyles: true }) embeds the document stylesheet; doc.toHtml({ includeStyles: false }) removes CSS while preserving class names and content. Documents, fragments and JSX use the same export options. Importing fullbleed/ui does not load the PDF renderer or add JavaScript to the output.

Version 0.6.0 added component authoring, attached stylesheets and explicit document rendering. The component guide includes zero-build JavaScript and TSX versions of a complete designed invoice.

Version 0.5.1 brought the engine's hidden-text and font-decoration fixes to both JavaScript entries. Script/style elements and display:none descendants stay hidden, while explicit author display overrides remain supported. Font-selected decoration thickness uses the selected face's metrics. The engine update checks cover source/control PDFs, font metrics, and browser downloads.

Version 0.5.0 adds defaultStyles: false to both JavaScript entries. Use it to preserve a template's original CSS, including font inheritance from html, low-specificity selectors, and cascade layers. Existing calls keep the Inter body rule. Retained styling checks cover both choices.

Version 0.4.1 brought the engine's recent layout fixes to both JavaScript entries. The engine update checks cover nested counters, clipped effects, border-image tiling, and floated initials with independent text, color, and Chrome print expectations.

Build and verify from source

Contributors need Node.js 22+ and Rust 1.97.0 with the WASI target. The independent PDF checks also use Python with the test-only readers listed below. Package users do not need a compiler or Python. Dependencies are pinned in both lockfiles.

rustup target add wasm32-wasip1
npm ci --ignore-scripts
npm run build
npm test
npm run check:types
npm run verify:native
npm run verify:pack
python -m pip install pypdf==6.19.0 pypdfium2==5.14.0 fonttools==4.65.0 pillow==12.3.0
npm run verify:text
npm run render:fonts
npm run verify:fonts
npm run render:families
npm run verify:families
npm run verify:browser:prepare
python -m pip install -r tools/browser-requirements.txt
python -m playwright install chrome firefox webkit
python tools/check-browser.py --browser chrome --label local
python tools/check-browser.py --browser firefox --label local
python tools/check-browser.py --browser webkit --label local

The build records engine metadata, licenses, and artifact hashes in dist/. The verification suite renders actual invoices, a three-page report, custom fonts, and image assets; compares native and WebAssembly PDF/PNG bytes; checks error recovery; and installs the packed tarball into a separate directory with spaces in its path. CI exercises Node 22, 24, and 26 on Windows, Linux, and macOS. Retained release evidence describes the verified fixtures and scope. Browser CI uses the same packed artifact in Chrome, Firefox and Playwright WebKit, including actual downloads, finalized previews, asset failures and recovery. The font check reads PDFs from the isolated tarball installation, checks the embedded TrueType programs against their source fonts, and compares two independent text readers. CI also compares text and native/PDFium pixels with the previous public npm release.

An intermittent process crash observed on Linux with Node 24.21.0 remains under investigation in issue #7. Version 0.1.3 fixes worker shutdown timing, but does not establish that this crash is resolved. Version 0.2.0 adds optional process fault containment; it does not claim to fix the native crash. See the runtime investigation and diagnostic for the observed scope, retained results, and a synthetic reproducer.

License and support

The adapter and Fullbleed engine are MIT licensed. Bundled fonts use SIL OFL 1.1. The WASI shim is used under its MIT option. Complete notices ship with the package in dist/THIRD_PARTY_NOTICES.txt and assets/fonts/.

Report reproducible integration bugs in this repository's issues. Include the package and Node versions, platform, minimal synthetic inputs, and error code. For engine behavior and general usage, use the Fullbleed discussion forum.

About

Generate PDFs from HTML/CSS in Node.js and browser workers with the Fullbleed Rust engine, bundled fonts, and PNG previews.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages