All posts
April 26, 20266 min read

Announcing Sone 1.4 — multi-page PDFs, balanced wrap, and the Metadata API

Sone 1.4 ships first-class multi-page PDF rendering, balanced line wrapping for headlines, and a Metadata API that exports every laid-out node and segment as YOLO or COCO datasets.

It has been a busy quarter. Sone now ships multi-page PDFs as a first-class feature: pass pageHeight to the render config and your node tree is sliced into pages automatically. Headers and footers are ordinary nodes — pass them inline, or as a function that receives { pageNumber, totalPages } for dynamic page chrome.

The PageBreak() node forces a break exactly where you want one, and node.pageBreak("avoid") hints the layout engine to keep a section together when there is room. Margins, last-page trimming, and per-page headers behave as you would expect from a real document engine.

Balanced line wrapping

Default text wrapping is greedy: each line takes as many words as fit, leaving the last line short and ragged. For headings, pull-quotes, and card titles, that looks awkward. .textWrap("balance") shrinks the effective break-width until all lines come out roughly equal, then sizes the node to the balanced content width — so it composes naturally inside a flex container.

Pair it with .hyphenate("en") for narrow-column body copy and you get tight, even-looking paragraphs without manual line breaks.

The Metadata API

Sone now exposes the computed layout of every node, line, and text segment. Call sone(root).canvasWithMetadata() and you get a tree with x, y, width, height and tag labels for every leaf. Useful for hit-testing, debug overlays, and post-processing.

The headline use-case is dataset generation. Tag any node with .tag("title") or .tag("invoice-number"), then pass the metadata to toYoloDataset() or toCocoDataset(). Out the other end you get bounding-box annotations matching your image — perfect for training Document Layout Analysis models on synthetic, perfectly-labeled data.

Bidirectional text and hyphenation

Bidirectional text now follows the Unicode Bidirectional Algorithm out of the box. Mixed-direction paragraphs (an LTR sentence with an inline RTL number) reorder correctly without any manual intervention. Override per-paragraph with .baseDir() on Text or per-span with .textDir() on Span.

Hyphenation now supports 80+ languages via Knuth–Liang patterns from the hyphen package. Pass .hyphenate("de") for German compound words, .hyphenate("fr") for French — most BCP-47-like locales work out of the box.

What's next

1.5 will focus on browser parity: a fully working SoneRenderer implementation backed by the standard Canvas API, so you can run Sone client-side for live preview UIs without wrapping your own platform shim.

If you are already using Sone in production, drop a note in the GitHub repo — we are collecting case studies and would love to feature yours.