Skip to content

Releases: earonesty/boxpdf

v1.9.0

17 Jun 00:07
11116f9

Choose a tag to compare

What's Changed

  • Zero-boilerplate getting-started ergonomics by @earonesty in #3

Full Changelog: v1.8.0...v1.9.0

boxpdf 1.8.0

18 May 23:00
5aa2cb7

Choose a tag to compare

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.
  • minHeight and maxHeight stack sizing.
  • alignSelf across flex items, including text, paragraphs, spacers, lines, images, links, and SVG paths.
  • letterSpacing for text and paragraph runs.
  • opacity for text and stack rendering.

Fixed

  • letterSpacing now 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 hstack rows now justify against the full row width.
  • maxHeight now constrains vertical shrink layout instead of only clamping the final reported height.
  • minHeight/maxHeight clamp order follows CSS: max first, then min wins if they conflict.

Validation

  • pnpm run typecheck
  • pnpm test
  • pnpm run build

Merged via PR #2.

What's Changed

  • Align layout rendering with CSS standards by @earonesty in #2

New Contributors

Full Changelog: v1.7.0...v1.8.0

v1.7.0 — measurement memoization

17 May 15:00

Choose a tag to compare

Summary

  • Dramatically speeds up repeated stack measurement by memoizing measurements within a single renderFlow call.
  • 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)

14 May 22:02

Choose a tag to compare

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

  1. Embed before streaming. All `embedFont` / `embedJpg` calls must complete BEFORE `streamFlow`. Mid-stream embedding throws with a clear message.
  2. Lazy input. Pass a generator — don't materialize the whole Node[].
  3. Exclusive writable. streamFlow takes ownership; closes on success, aborts on failure.
  4. No `totalPages` in headers/footers. Accessing `ctx.totalPages` throws — switch to `renderFlow` if you need "Page X of Y".
  5. ~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

14 May 20:28

Choose a tag to compare

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

14 May 20:18

Choose a tag to compare

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: number on text(), vstack(), hstack(), spacer(). link() forwards its child's weight.
  • breakWords: boolean on text() — CSS overflow-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 vstack has a fixed height smaller than the sum of its children.
  • resolveMainAxis exported 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.