Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ jobs:
with:
python-version: '3.12'
- run: python -m pip install pypdf==6.19.0 pypdfium2==5.14.0 fonttools==4.65.0 pillow==12.3.0
- name: Verify the packed Workers entry in local workerd
run: |
npm run verify:cloudflare
python tools/verify-cloudflare.py
npm run verify:cloudflare:starter
python tools/verify-cloudflare-starter.py
- run: python -m unittest discover -s test -p 'test_*.py'
- run: npm run verify:text
- run: python tools/verify-components.py
Expand Down Expand Up @@ -115,6 +121,8 @@ jobs:
output/hidden-text/
output/engine-paint/
output/component-verification/
output/cloudflare-verification/
output/cloudflare-starter-verification/
retention-days: 7

installed:
Expand Down Expand Up @@ -169,6 +177,10 @@ jobs:
node tools/render-engine-paint.mjs
python tools/verify-engine-paint.py
python tools/verify-components.py
npm run verify:cloudflare
python tools/verify-cloudflare.py
npm run verify:cloudflare:starter
python tools/verify-cloudflare-starter.py
- uses: actions/upload-artifact@v4
if: always() && matrix.os == 'windows-latest' && matrix.node == '22'
with:
Expand All @@ -182,6 +194,8 @@ jobs:
output/hidden-text/
output/engine-paint/
output/component-verification/
output/cloudflare-verification/
output/cloudflare-starter-verification/
retention-days: 7
- uses: actions/upload-artifact@v4
if: always()
Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Fullbleed for Node.js and browsers
# Fullbleed for Node.js, browsers, and Cloudflare Workers

Generate PDFs from reusable function components, JSX, or static HTML and CSS in Node.js or a browser worker. The package includes the
Generate PDFs from static HTML/CSS, reusable function components, or JSX in Node.js, a browser worker, or Cloudflare Workers. The package includes the
Fullbleed Rust engine compiled to WebAssembly and four font faces, so installing
it does not require Python, Rust, a browser, or system fonts. MIT licensed.

Import `fullbleed` for Node.js or `fullbleed/browser` for a web application.
Import `fullbleed` for Node.js, `fullbleed/browser` for a web application, or
[`fullbleed/cloudflare`](docs/cloudflare.md) inside a Cloudflare Worker.

Use [`fullbleed/ui`](docs/components.md) to compose documents without writing HTML
strings. Named components, typed props, ordinary conditions and arrays, reusable
styles, and an optional JSX/TSX runtime work with either renderer. No React
styles, and an optional JSX/TSX runtime work with each renderer. No React
runtime or build step is required for ordinary JavaScript functions.

Try a complete project online: [JavaScript + Vite](https://docs.fullbleed.dev/assets/browser-starter/edit-online.html)
Expand All @@ -21,7 +22,7 @@ Using Vue? The [Vue and TypeScript starter](examples/vue) includes editable
templates, automatic previews, downloads, and a reusable `usePdfPreview` composable.

This is an optional integration around the published **Fullbleed 2.5.29** engine.
The package has its own version, **0.7.1**. Node applications can use a shared
The package has its own version, **0.8.0**. Node applications can use a shared
render queue to bound active renders and waiting requests, include queue time in
deadlines, cancel waiting jobs, and stop rendering cleanly during shutdown.
The browser entry renders in a Web Worker, and the Node API supports worker and
Expand All @@ -31,7 +32,12 @@ process isolation. See the [browser guide](docs/browser.md),
[font-family checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-families.md), and
[embedded-font checks](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/font-subsets.md) for the retained fixtures.

Version 0.7.1 brings the stable engine's SVG stylesheet and inset-shadow fixes to
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](docs/cloudflare.md)
describes its local-runtime verification and deployment requirements.

Version 0.7.1 brought the stable engine's SVG stylesheet and inset-shadow fixes to
Node.js and browser workers. The [paint checks](docs/engine-2.5.29.md) cover
stylesheet selectors, inherited SVG colors and opacity, and clipped, blurred,
translated and negative-spread inset shadows. The component and static HTML APIs
Expand Down Expand Up @@ -73,7 +79,7 @@ Use Node.js 22 or newer. Install the [npm package](https://www.npmjs.com/package
npm install fullbleed
```

For a version-pinned installation, use `npm install --save-exact fullbleed@0.7.1`.
For a version-pinned installation, use `npm install --save-exact fullbleed@0.8.0`.
Package archives and retained PDF/PNG evidence are attached to each
[GitHub release](https://github.com/fullbleed-engine/fullbleed-node/releases).
The [installation verification records](https://github.com/fullbleed-engine/fullbleed-node/tree/main/verification)
Expand Down
6 changes: 3 additions & 3 deletions docs/browser.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generate PDFs in a browser

The `fullbleed/browser` entry in Fullbleed 0.5.0 renders static HTML/CSS in a
dedicated Web Worker. It uses the same pinned 2.5.22 engine and bundled fonts as
The `fullbleed/browser` entry in Fullbleed 0.8.0 renders static HTML/CSS in a
dedicated Web Worker. It uses the same pinned 2.5.29 engine and bundled fonts as
the Node entry. The result is a `Uint8Array`, suitable for a PDF `Blob`, plus
optional PNG previews. No PDF server or account is required.

Expand All @@ -21,7 +21,7 @@ Install the package in your web project, then copy its browser files into the
directory your application serves as static files:

```sh
npm install --save-exact fullbleed@0.5.0
npm install --save-exact fullbleed@0.8.0
npx fullbleed-browser-assets public/fullbleed
```

Expand Down
154 changes: 154 additions & 0 deletions docs/cloudflare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Generate PDFs in Cloudflare Workers

Import `renderPdf` from `fullbleed/cloudflare` inside a Worker. Wrangler bundles
the package's pinned WebAssembly engine and four font faces with your application.
The renderer uses an in-memory filesystem and produces PDF bytes plus optional
PNG previews. HTML/CSS and document data stay in the Worker handling the request.

This entry is qualified with the local `workerd` runtime. Hosted CPU, memory,
startup behavior, and free-plan suitability require testing in your deployment.
See [Cloudflare's current limits](https://developers.cloudflare.com/workers/platform/limits/).

For a complete JSON-to-PDF endpoint, use the
[invoice starter](../examples/cloudflare/). It includes an editable Northstar
component template, request limits, integer-cent totals, download headers and
local tests for normal, long, escaped and invalid invoices.

## Add it to a Worker

Use Node.js 22 or newer for your development tools:

```sh
npm install --save-exact fullbleed@0.8.0
npm install --save-dev --save-exact wrangler@4.151.3
```

Add a font module rule to your Wrangler configuration. The `.wasm` import uses
Wrangler's built-in compiled-module rule.

```json
{
"name": "invoice-worker",
"main": "src/index.js",
"compatibility_date": "2026-10-20",
"workers_dev": false,
"preview_urls": false,
"rules": [{ "type": "Data", "globs": ["**/*.ttf"], "fallthrough": false }]
}
```

Save this as `src/index.js`:

```js
import { renderPdf } from 'fullbleed/cloudflare';

export default {
async fetch() {
const result = await renderPdf({
html: '<h1>Invoice NS-1042</h1><p>Design and delivery: $1,870.00</p>',
css: '@page { size: A4; margin: 20mm } h1 { color: #173b35 }',
maxPages: 5,
});
return new Response(result.pdf, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="invoice.pdf"',
'Cache-Control': 'no-store',
},
});
},
};
```

Run `npx wrangler dev --local` and open the displayed localhost URL. No Cloudflare
account is needed for this local check. `workers_dev: false` and
`preview_urls: false` keep public preview URLs disabled if you later deploy;
configure service bindings or your application's authenticated route deliberately.

## Components and styles

The static component API works in the same Worker. A document artifact supplies
its HTML and CSS to the renderer:

```js
import { renderPdf } from 'fullbleed/cloudflare';
import { Document, Heading, Paragraph } from 'fullbleed/ui';

const doc = Document({
title: 'Monthly statement',
styles: {
'@page': { size: 'A4', margin: '20mm' },
'.balance': { color: '#173b35', fontSize: '24pt' },
},
}, Heading({ level: 1 }, 'Monthly statement'),
Paragraph({ className: 'balance' }, '$1,870.00'));

const result = await doc.renderPdf(renderPdf, { maxPages: 5 });
```

`className` adds an HTML class. The matching rule belongs in `styles` or the
renderer’s `css` string. Use `doc.renderPdf(renderPdf)` or pass both
`doc.toHtml()` and `doc.toCss()` for the separate HTML/CSS form.
`doc.toHtml({ includeStyles: true })` produces a complete HTML document with
its stylesheet embedded. This also renders with `css: ''` and
`defaultStyles: false`; the Workers checks compare both forms' PDF and preview
bytes. Remote stylesheets are not fetched.

## Options and execution

`renderPdf(options)` returns a Promise for `{ pdf, previews, pages,
missingGlyphs, engineVersion }`. PDF and PNG values are `Uint8Array` instances
backed by ordinary `ArrayBuffer` objects.

| Option | Workers behavior |
| --- | --- |
| `html`, `css` | Static source; at most 1,000,000 UTF-8 bytes combined. |
| `defaultStyles` | Defaults to `true`, adding the Inter body rule; `false` preserves your CSS. |
| `fonts` | Additional TrueType font byte arrays, using family names in CSS. |
| `assets` | Byte arrays under relative names; refer to `assets/name.svg` in HTML/CSS. |
| `fonts` + `assets` | At most 8 MiB combined; accepted caller buffers are snapshotted. |
| `maxPages` | Defaults to 20; exceeding the limit rejects the result. |
| `previewDpi` | Defaults to 0; 36–300 enables one PNG per page. Account for memory. |
| `allowMissingGlyphs` | Defaults to `false`; explicit opt-in retains the missing-glyph count. |

The computation is synchronous inside the isolate and returns a settled Promise.
Each call uses a fresh engine instance. Calls on the same isolate do not
interleave engine execution; this does not guarantee that previous allocations
have already been garbage-collected.

`timeoutMs`, `signal`, and `isolation` are rejected. The SDK cannot interrupt
synchronous computation from within the same isolate. Cloudflare owns CPU and
memory enforcement; a platform termination may bypass application error handling.
Set workload-appropriate platform limits, bound accepted data and concurrency in
your application, and measure representative documents in your deployed Worker.
Page and input limits are not CPU or memory guarantees.

Catch `FullbleedError` for input validation, page limits, missing glyphs, and
engine failures. A rejected call returns no partial PDF. A later call creates a
fresh instance. Document scripts do not run, and document URLs do not trigger
network fetching. Supply images and custom fonts as bytes.

This is the static print engine, not browser layout or a live-page renderer.
The entry exposes ordinary PDF generation and previews, not PDF/A, PDF/UA,
PDF/X, or VDP profiles.

## Verify the installed package

```sh
npm ci --ignore-scripts
npm run build
npm test
npm run check:types
npm run verify:pack
npm run verify:cloudflare
python tools/verify-cloudflare.py
```

The Workers check uses the isolated tarball installation. It compares PDF and
PNG bytes with Node for Northstar, Common Ground, a custom font/SVG fixture, and
component-authored documents. It exercises input budgets, unsupported controls,
asset paths, page limits, missing glyphs, repeated calls, and failure recovery.
Runtime fetching and asynchronous Wasm compilation are disabled during the check.
An independent PDF reader checks pages, expected text, and the custom font face;
PNG files are decoded separately. Evidence is retained under
`output/cloudflare-verification/`. Local checks establish local behavior only.
2 changes: 1 addition & 1 deletion engine/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion engine/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fullbleed-node-adapter"
version = "0.7.1"
version = "0.8.0"
edition = "2024"
license = "MIT"
publish = false
Expand Down
6 changes: 6 additions & 0 deletions examples/cloudflare/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.wrangler/
dist/
output/
.dev.vars
.env*
21 changes: 21 additions & 0 deletions examples/cloudflare/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Fullbleed Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
67 changes: 67 additions & 0 deletions examples/cloudflare/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# A Fullbleed invoice Worker

Send structured invoice JSON and receive a styled PDF attachment. This starter
uses `fullbleed/cloudflare` with a fixed, editable component template, bundled
fonts, integer-cent totals, and flowing tables. It stores no invoices or PDFs.
Northstar Studio and the sample customer are fictional.

## Run locally

Use Node.js 22 or newer:

```sh
npm install
npm test
npm run build
npm run dev
```

In a second terminal:

```sh
curl --fail-with-body http://127.0.0.1:8787/invoice \
-H "Content-Type: application/json" \
--data-binary @sample.json --output invoice.pdf
npm run test:worker
```

In Windows PowerShell, use `curl.exe` and place the command on one line.
`test:worker` submits synthetic invoices and retains PDFs under `output/`.
Set `FULLBLEED_WORKER_URL` to change the local test address.

## Change the design

- `src/document.mjs`: text, function components, fields, and table rows.
- `src/styles.mjs`: page size, margins, colors, typography, and class rules.
- `src/handler.mjs`: request schema, response headers, and validation limits.
- `src/index.mjs`: the Workers adapter import.

`className` connects a component to a rule in the document's `StyleSheet`.
`Invoice({ data }).renderPdf(renderPdf)` passes both generated HTML and CSS.
For a browser HTML export, use `.toHtml({ includeStyles: true })`. You can also
replace the component template with your own HTML/CSS and call `renderPdf`.

The sample accepts 1–30 rows, 32 KiB of JSON, dates in `YYYY-MM-DD` format,
nonnegative integer-cent rates, integer quantities, and totals up to $999,999.99.
PDFs have a 10-page limit. Change these bounds alongside representative layout
tests. The template uses USD and does not calculate tax. Invoice identifiers are
restricted to safe filename characters. Text is escaped by the component API.

## Connect it to an application

The configuration disables public Workers and preview URLs and includes no
route. The local endpoint has no authentication. Before providing an external
route, add your application's authentication, authorization and rate limits;
or call it through a private Worker service binding. Keep real invoices out of
logs. `npm run build` bundles locally and does not deploy anything.

Rendering runs synchronously inside an isolate and returns a settled Promise.
The SDK does not expose cancellation or a timeout for Workers. Page and input
limits do not bound CPU time or memory. Local workerd checks are retained in the
SDK repository; hosted CPU, memory, startup and free-plan suitability have not
been qualified. Measure your real documents against
[Cloudflare's current limits](https://developers.cloudflare.com/workers/platform/limits/)
before choosing deployment settings.

See the [adapter guide](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/cloudflare.md)
for custom assets, fonts, options and the supported execution model.
16 changes: 16 additions & 0 deletions examples/cloudflare/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "fullbleed-cloudflare-invoice",
"version": "1.0.0",
"private": true,
"type": "module",
"license": "MIT",
"engines": { "node": ">=22" },
"scripts": {
"dev": "wrangler dev --local --ip 127.0.0.1 --port 8787 --inspector-port 0",
"build": "wrangler deploy --dry-run --outdir dist",
"test": "node --test test/request.test.mjs",
"test:worker": "node test/worker.mjs"
},
"dependencies": { "fullbleed": "0.8.0" },
"devDependencies": { "wrangler": "4.151.3" }
}
Loading
Loading