Releases: earonesty/boxpdf
v1.9.0
What's Changed
- Zero-boilerplate getting-started ergonomics by @earonesty in #3
Full Changelog: v1.8.0...v1.9.0
boxpdf 1.8.0
CSS-standard layout and rendering
This release makes core layout and rendering behavior more predictable for CSS-like document generation.
Added
hstack({ wrap: true })with greedy row wrapping and per-row justification.minHeightandmaxHeightstack sizing.alignSelfacross flex items, including text, paragraphs, spacers, lines, images, links, and SVG paths.letterSpacingfor text and paragraph runs.opacityfor text and stack rendering.
Fixed
letterSpacingnow participates in measurement, wrapping, hard-breaking, ellipsizing, alignment, and decoration widths instead of only affecting drawn glyphs.- Stack opacity now propagates to backgrounds, borders, background images, images, SVG paths, text, and text decorations.
- Text opacity now applies consistently to underline and strikethrough decorations.
- Wrapped
hstackrows now justify against the full row width. maxHeightnow constrains vertical shrink layout instead of only clamping the final reported height.minHeight/maxHeightclamp order follows CSS: max first, then min wins if they conflict.
Validation
pnpm run typecheckpnpm testpnpm run build
Merged via PR #2.
What's Changed
- Align layout rendering with CSS standards by @earonesty in #2
New Contributors
- @earonesty made their first contribution in #2
Full Changelog: v1.7.0...v1.8.0
v1.7.0 — measurement memoization
Summary
- Dramatically speeds up repeated stack measurement by memoizing measurements within a single
renderFlowcall. - Adds scoped profiling hooks for pagination and measurement diagnostics, including cache-hit visibility.
- Keeps the diagnostic API focused on
renderFlow(..., { profile })rather than exposing cache internals. - Adds regression coverage for measurement memoization and render-flow cache reuse.
Commits
- e159c5b Speed up repeated stack measurement
- dda6e8d Add stack overflow clipping
- cda6668 Revert "Track only PNG visual artifacts"
- 31e8b61 Track only PNG visual artifacts
- 8d3dddd Track float visual artifact with LFS
- 433d243 Add paragraph float wrapping
- bef9c35 Add box background image primitive
- 252e070 Add hard breaks and no-wrap text layout
- 7cb0148 Add collapsed table border support
- 789031d Use rendered text metrics for paragraph wrapping
- a35e9e4 Add paragraph hanging indent support
- cb857ff Ignore nested html package
Full Changelog: v1.6.1...v1.7.0
v1.4.0 — streamFlow (incremental PDF generation)
Stream PDFs page-by-page with bounded memory, regardless of total page count.
New: `streamFlow`
```ts
import { PDFDocument, StandardFonts } from "pdf-lib";
import { streamFlow, text } from "boxpdf";
const pdf = await PDFDocument.create();
const font = await pdf.embedFont(StandardFonts.Helvetica);
// Workers / edge — stream into the Response body.
const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
streamFlow(pdf, writable, generateRows(font)).catch(console.error);
return new Response(readable, { headers: { "content-type": "application/pdf" } });
async function* generateRows(font) {
for await (const order of fetchOrders()) {
yield buildRow(font, order); // GC-able after this yield is consumed
}
}
```
For Node, wrap a Writable:
```ts
import { createWriteStream } from "node:fs";
import { streamFlow, nodeAdapter } from "boxpdf";
const out = nodeAdapter(createWriteStream("./report.pdf"));
await streamFlow(pdf, out, nodes);
```
Memory bench (50 lines/page)
| pages | renderFlow Δheap | streamFlow Δheap | output |
|---|---|---|---|
| 50 | 6.1 MB | 0 MB | 70 KB |
| 250 | 35.2 MB | 0 MB | 347 KB |
| 1000 | 157.4 MB | 0 MB | 1.4 MB |
A 1000-page report peaks at 0 MB above baseline with streamFlow vs. 157 MB with renderFlow + pdf.save(). Same byte output within 0.2%.
Contract
- Embed before streaming. All `embedFont` / `embedJpg` calls must complete BEFORE `streamFlow`. Mid-stream embedding throws with a clear message.
- Lazy input. Pass a generator — don't materialize the whole Node[].
- Exclusive writable. streamFlow takes ownership; closes on success, aborts on failure.
- No `totalPages` in headers/footers. Accessing `ctx.totalPages` throws — switch to `renderFlow` if you need "Page X of Y".
- ~5% size overhead. Per-batch ObjStm packing is slightly less efficient than whole-doc compression.
Internals
- Built on pdf-lib's public `PDFObjectStream` + `PDFCrossRefStream` exports. Zero deep imports, no fork.
- Per-page snapshot/delta of `PDFContext.indirectObjects`. PDFStream objects (content streams, image XObjects) get written + `ctx.delete()`d immediately. PDFDicts (page dicts, annots) batch into compressed ObjStms (`objectsPerStream` default 50).
- Cross-reference stream uses PDF 1.5+ format — keeps output competitive with renderFlow's default `save()`.
Tests + design
- 12 new tests; 113 total passing
- Full design rationale in `docs/design/streaming.md`
- Comparison harness: `pnpm exec tsx scripts/compare-stream.ts`
- Memory bench: `node --expose-gc --import tsx scripts/bench-memory.ts`
v1.3.0 — LETTER default + page helpers + overflow warning
Closes the page-size footgun that bit me writing the v1.2.0 demo (renderFlow defaulted to A4 while my constants assumed LETTER).
⚠️ Default page size changed: A4 → LETTER
`renderFlow` and `renderToPdf` now default to LETTER (612×792) to match pdf-lib (which boxpdf is built on). If you were relying on the A4 default, pass it explicitly:
```ts
await renderFlow(pdf, nodes, { size: PageSizes.A4 });
```
New helpers
Stop hardcoding `612 - 72`:
```ts
import { pageInner, pageContent, PageSizes } from "boxpdf";
const WIDTH = pageInner(PageSizes.Letter, 36); // 540
const { width, height } = pageContent(PageSizes.A4, 36);
```
Now your widths follow the page if you change `size` later.
Overflow warning
When a top-level child's measured width exceeds the page content area, you get a one-line warning at dev time:
```
[boxpdf] top-level vstack measured 540.0pt — exceeds page content area 523.3pt (A4 with 36pt margins). Pass {size: PageSizes.Letter} if that matches your intent, reduce the child's width, or add shrink/wrapping so it fits.
```
The warning fires from both `renderFlow` and `renderToPdf`. Suppress with `{warnings: false}` if you're deliberately overflowing.
Other surveys other PDF libs
We chose LETTER because:
| Lib | Default |
|---|---|
| pdf-lib (our base) | LETTER |
| @react-pdf/renderer | A4 |
| PDFKit | LETTER |
| jsPDF | A4 |
| Puppeteer | LETTER |
Defaults are genuinely split. Matching pdf-lib was the cleanest answer for a layout DSL sitting directly on top of it.
Tests
101/101 passing (12 new for page helpers + warning + LETTER default).
v1.2.0 — flex-shrink
CSS-style flex-shrink on hstack / vstack. When intrinsic children exceed the container, items with shrink > 0 give up shares of the overflow proportional to shrink × baseSize — and text re-wraps to fit.
New
shrink: numberontext(),vstack(),hstack(),spacer().link()forwards its child's weight.breakWords: booleanontext()— CSSoverflow-wrap: break-word; allows char-level wrap below the longest-word floor.maxLines(existing) now also lowers the shrink floor so the engine can ellipsize cleanly into a narrower slot.
Behavior
- Multi-word text floors at its widest whitespace-separated word — never breaks mid-word.
- Single-token strings (URLs, hashes, identifiers) overflow visibly by default. Opt in to truncation (
maxLines: 1) or char-break (breakWords: true). - Iterative weight redistribution when an item hits its floor — CSS flexbox semantics.
- Vertical shrink fires when a
vstackhas a fixedheightsmaller than the sum of its children. resolveMainAxisexported for layout consumers.
Other
- Debug overlay strokes now sit fully outside the content box, so glyphs at the box edge never visually overlap the stroke.
Tests
89/89 passing (24 new for flex-shrink). Verified end-to-end on Cloudflare Workers.
See examples/flex-shrink.ts for a runnable showcase.