-
released this
2026-10-22 15:13:15 +02:00 | 0 commits to main since this releaseAdded
- Continuing styled text across regions:
WriteResult.resume_at(a
TextPosition: word index plus byte offset, where writing stopped) and
ParagraphOptions.start. CallwriteStyledagain with the same
StyledTextandstart = resume_atto continue in the next column or page with
every span and style intact. Before, the only continuation was
overflow, which for styled text holds just the rest of one span -
later spans were silently lost. platen.Path: the path builder type returned bydoc.createPath()is
now exported, so helpers can take a*platen.Path.- Complete GSUB: every lookup type - single, multiple, alternate,
ligature, context (formats 1-3), chained context (formats 1-3),
extension, reverse chained - with lookup flags (ignore base glyphs /
ligatures / marks, mark attachment classes, mark filtering sets, via
GDEF), applied in lookup-list order over the whole run as HarfBuzz does,
for any feature. Before, only single, ligature and chained context
format 3 lookups were read, each wired to a few features. New default
features, as in HarfBuzz:ccmp,locl,rlig,clig(turn off with
setFeature).ccmpgives dotless i/j under accents (Gentium, Inter:
j́,ị́, Lithuanianį́) and joins emoji ZWJ sequences, skin tones
and flags into one glyph (Noto Color Emoji). - Default ignorables are removed: U+FE0F/U+FE0E, ZWJ, ZWNJ, zero-width
space, soft hyphen, word joiner, BOM and the other Default_Ignorable code
points no longer draw (U+FE0F was .notdef in Noto Color Emoji, a soft
hyphen passed towriteAta visible hyphen) and add no width. During
GSUB they are skipped where HarfBuzz skips them - ZWJ always, ZWNJ only
in context - so keycaps (1️⃣),❤️and🏳️🌈join,f+ZWJ+istill
ligates and ZWNJ still blocks ligatures. Their code points stay out of
extracted text. - Variation sequences (cmap format 14): a character followed by a
variation selector draws as the glyph the font lists for the pair - CJK
ideograph variants, text-style emoji with U+FE0E in Noto Emoji - as
HarfBuzz does; unlisted selectors are dropped as before.font-info
shows whether a font has them. - CLI
shape <font> [+feat|-feat]...: glyph IDs for each stdin line, as
rendering shapes it.
Fixed
- Shaping differed from HarfBuzz (found by the new differential
test): Inter's<->became<-plus>,casetogether withtnum
gave tabular but not case forms (features ran in a fixed order, not the
font's lookup order), and Satoshi'sfracgave no numerators or
denominators. - CFF fonts lost contextual alternates: a subset of a
.otffont kept
only the first glyph drawn for each character, so a contextual variant
of a character already drawn plainly (Inter's raised hyphen inA-B
aftera-b) rendered as .notdef. Every drawn glyph is kept now, as for
TrueType. - Translucent color-font gradients painted opaque: COLRv1 gradient
stops carry an alpha, which was dropped, so overlays meant to fade out
covered the glyph - Noto's rainbow flag 🏳️🌈 came out grey, and about
2,000 Noto Color Emoji glyphs had such gradients. Stop alpha is now
drawn through a soft mask (or/cawhen it is constant). - A color glyph without an outline painted the whole page: Bungee
Spice's space is a color glyph whose clip glyph has no contours; the
empty clip was written asW nwith no path, which sets no clip, so its
gradient covered the page from that point on. Such paints are now
skipped (an empty clip shows nothing). - Underlines and strikethroughs ran into the space a line wraps at
(write()): the line was drawn with its trailing space, and the
decoration with it. The space now hangs past the line end, as in CSS,
also when it ends one write() call and the next call's first word
wraps; spaces inside the line stay decorated. write()lost text before a double space: line batching reset
the line at every space it skipped, sowrite("aa bb")drew only
"bb" (at aa's place), and a call starting with a space mid-line lost
it (" over"after"jumps"gave "jumpsover"). Spaces inside a line
are kept as typed now.- SVG hairlines were rounded: stroke widths were written with two
decimals in SVG units (the tiger's 0.172 became 0.17); now four. - Bitmap-only fonts had an empty
FontBBox: an OTB pixel font's head
box is 0 wide, so every glyph fell outside its Type3 font's box
(poppler warned on each). Such fonts now get[0 0 0 0], which tells
viewers to make no assumptions about glyph sizes. - Color emoji ligatures extracted as U+FFFD: the Type3 ToUnicode of
a color font mapped only single code points, so a joined emoji (ZWJ
family, flag, keycap) copied as a replacement character. It maps to the
whole sequence now. - Text extraction of substituted glyphs: ToUnicode now follows the
shaped glyphs' clusters - a ligature maps to its characters, a glyph a
multiple substitution produced to its own character of the
decomposition - instead of re-guessing ligatures after shaping. - Words wider than the line lost their end at a region boundary: a
word broken over lines (soft hyphens, regular hyphens, or between
characters) that reached the bottom of the region was cut off and
layout went on with the next word. Writing now stops mid-word and
resume_atcarries the byte offset, so the next region continues the
word (lines are the unit, as in TeX and CSS fragmentation). Pieces are
measured in the span's own style (bold words were broken with regular
widths). - Knuth-Plass put a too-wide word on one endless line: with
.line_breaking = .optimal, a word wider than the line made every
break infeasible and the whole paragraph came out as a single line
running past the region. Such a word now gets a line of its own and is
broken into pieces, as with greedy breaking. measure_onlydrew broken words: measuring a paragraph containing
a word wider than the line drew that word's pieces on the page.- Combining marks compose to the font's precomposed glyphs: a base
followed by combining marks (e.g.i+ U+0308) was drawn as separate
glyphs positioned by GPOS - or simply overprinted in fonts without mark
positioning - giving a dotted i under a diaeresis. Like HarfBuzz,
shaping now composes the sequence into the precomposed character
(U+00EF) when the font has a glyph for it; otherwise it stays
decomposed. Measurement and rendering share the decoding, and the
ToUnicode text of such a glyph is the sequence as typed. - Unused fonts were embedded whole: a font loaded into a document but
never drawn with (only measured, or not used at all) has no glyphs to
subset, sooutput()wrote the complete original font file as orphan
objects - 25 MB for Noto Color Emoji. Only fonts some page draws with
are written now. - Justified text in embedded Unicode fonts: lines set in a single
style were justified only through the PDFTwoperator, which applies
to the single-byte code 32 and never to 2-byte Identity-H codes (nor to
color-font runs), so those lines came out ragged in every
.identityfont. The extra word space is now emitted after each space
glyph as aTJadjustment (orTmoffset in the mark-positioning
path), andTwis 0 for these runs (poppler applies it to the 2-byte
code 0x0020 anyway, e.g. a ligature subset to CID 32). New fpdf2
referencejustify_embedded. char_spacingin runs with combining marks: runs with GPOS mark
positioning place every glyph withTm, soTcnever applied and the
text came out unspaced whilemeasureStyledincluded the spacing. The
spacing is now added per glyph in that path.char_spacingleaked into later text: a run with non-zero
char_spacingset the PDFTcoperator but never reset it, andTc
survivesET, so every later text run on the page was letter-spaced
too, while layout measured it without the spacing (overlapping spans,
text overrunning its region). The run now resetsTcbeforeET.- Gradient stops are copied:
addLinearGradient/addRadialGradient
kept a pointer to the caller'sstopsslice untiloutput(); stops in
a stack frame that had returned were read after free (garbage colors or
a panic). The document now keeps its own copy.
Internal
- GSUB differential test:
test/shaping/harfbuzz.txtholds HarfBuzz's
glyphs for Latin, diacritic, default-ignorable and emoji texts in Inter,
Satoshi, Gentium, Bungee, Charter and Noto Emoji under several feature
sets (HarfBuzz removing default ignorables, as platen does), plus a synthetic font
with one scenario per lookup type and flag
(scripts/gen_gsub_test_font.py);scripts/gen_shaping_reference.py
regenerates it. zig build test -Dtest-filter=NAMEruns only matching tests.- The 19 references platen generates itself (color fonts, SVG,
decorations, line breaking, transforms, diacritics) are now also
compared rendered, pixel for pixel (compare_pdfs.py --compare-render,
poppler): the text-and-position comparison missed drawing changes.
Seven had drifted - three from bugs fixed here (color_v1_gradienthad
the page-filling space baked in), the rest from intended changes
(svg_embeddingpredated per-path stroke widths: its tiger had 3x
thick whisker outlines).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Continuing styled text across regions:
-
released this
2026-10-17 19:26:40 +02:00 | 34 commits to main since this releaseAdded
- Font collections (.ttc/.otc):
doc.loadFontFromCollection(data, index, encoding)loads one face of a collection (the Document keeps its
own copy).platen.extractFacerebuilds face N as a standalone font
(tables byte-identical, 4-byte aligned, checksums kept);
platen.listFaces/font_collection.describeFacegive each face's
family, subfamily, PostScript name, weight, width and bold/italic.
A plain .ttf/.otf counts as a one-face collection. - System font lookup:
platen.findSystemFont(io, allocator, family, .{ .bold, .italic }, opts)andplaten.findSystemFontFamilysearch the
platform font folders recursively and return caller-owned standalone
sfnt bytes (extracted from collections) with family, subfamily,
PostScript name, path and face index;FoundFamily.memoryOptions()
feedsloadFontFamilyFromMemory. Matching uses the name table
(typographic family, case-insensitive) and the subfamily; the per-user
folders come fromopts.home, so the library stays stateless. - CLI:
font-faces,extract-face,find-font. - Legacy
kerntable kerning: fonts that kern only through a TrueType
kerntable (format 0; Microsoft and Apple layouts) are now kerned in
rendering and inmeasureStyled; before, only GPOS kerning was read and
such fonts (e.g. Bitstream Vera) came out unkerned.
GPOS kerning still wins when present, as in HarfBuzz.
Fixed
.win_ansiTrueType fonts dropped composite glyphs: the WinAnsi
subsetter copied a composite (e.g. Inter's i, j, colon = base + dot)
without its components and kept the original component IDs, so those
characters rendered blank. Components are now included and remapped, as
the.identitypath already did.- PDF/A dates:
/Infoand XMP each read the clock, so a second boundary
between them madeCreationDateandxmp:CreateDatediffer and
veraPDF failed the file (6.7.3).output()now reads the clock once.
Explicitmetadata.creation_date/mod_datereached/Infoonly, so
PDF/A with explicit dates was never compliant; they are now converted
to ISO 8601 for XMP (same precision and time zone). Under PDF/A an
explicit date that is not a valid PDF date fails with
error.InvalidDate. - File IDs came from the millisecond clock: the trailer
/ID(PDF/A and
encrypted output) and the XMPDocumentID/InstanceID(one shared,
non-UUID string). Outputs in the same millisecond, e.g. concurrent
documents, got identical IDs, anddoc.rngdid not make output
reproducible as documented. All three now come fromdoc.rng(default:
system CSPRNG); the XMP IDs are distinct version-4 UUIDs. Withdoc.rng
set and explicit dates, PDF/A and encrypted output is byte-identical.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Font collections (.ttc/.otc):
-
released this
2026-10-06 10:02:58 +02:00 | 51 commits to main since this releaseAdded
- Bold and italic per span:
StyledTextspans take.boldand
.italic(st.append("word", .{ .bold = true })), resolved through the
base font's family likeTextStyle.bold/.italic; widths use each
span's variant, so wrapping and justification stay exact. A variant the
family lacks iserror.FontVariantNotLoaded, before anything is drawn.
Fixed
writeBlock, andwriteStyledwith plain spans, ignored the base
style'scoloranddecoration(text came out black, without
underline).TextStyleOptionsfields are now optional overrides:null
inherits from the base style. Existing calls such as
.{ .color = platen.Color.blue }compile unchanged.
Internal
- The PDF comparator behind the reference tests compares fonts whenever
either page has any (a page that lost all its fonts used to pass) and
treats qpdf failures as errors; it has its own tests in the suite. zig build testre-runs when a reference PDF, fixture or comparator
script changes (they are declared inputs); before, a regenerated or even
corrupted reference could be served a cached pass.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Bold and italic per span:
-
released this
2026-10-06 08:41:06 +02:00 | 65 commits to main since this releaseFixed
- Package size: dependents fetch ~0.4 MB instead of 45 MB and cache
~1.8 MB instead of ~100 MB.build.zig.zon.pathslists only the
library (build files,src/, README, LICENSE, CHANGELOG), and
.gitattributeskeeps tests, fixtures, examples and dev scripts out of
the forge's source archives. Clones are unchanged. The package hash
differs from 0.10.0.
Internal
- CI prints the test summary, so runs show test counts and skips.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Package size: dependents fetch ~0.4 MB instead of 45 MB and cache
-
released this
2026-10-06 01:22:39 +02:00 | 70 commits to main since this releaseFirst tagged release; earlier history is in git. Requires Zig 0.17.
Behavior changes — callers may need to adapt
Invalid input is now an error, never silently clamped or converted.
- Numbers: every
Documentdrawing/text/layout call rejects NaN,
infinities and magnitudes aboveplaten.max_value(1,000,000) with
error.InvalidNumber, before changing the document. (A huge
drawCircleradius used to crash.) - Fonts:
unitsPerEmoutside 16–16,384, or metrics that do not fit
once scaled, areerror.InvalidFontat load. Glyph IDs a font does not
have are left out of subsets (they render as .notdef). - SVG: numbers that are not finite or exceed ±1e9 are invalid (treated
like any unparseable value); transforms that compose past that bound are
error.InvalidSvg. - CFF fonts: subroutine nesting over 10, expanded glyphs over 65,535
bytes, or runaway expansion areerror.InvalidCFFwhen subsetting. - PNG: chunk CRCs are verified (
error.PngChecksumMismatch); bit depths
invalid for the color type, zero sizes, and sizes the data cannot hold
are rejected before any allocation. - PDF/A: standard (non-embedded) fonts, and SVGs containing
<text>,
areerror.FontNotEmbeddedat every level. Under PDF/A-1b, images with
transparency areerror.TransparencyNotAllowedand 16-bit images
error.BitDepthNotAllowed(downsample, or use PDF/A-2b). - API types:
opentype.MarkOffsetfields arei32;
CoverageTable.getGlyphCountreturnsusizeandgetGlyphAttakes one. - Link annotations carry
/F 4(print), as PDF/A requires. - The
zig build docsstep (broken since the generated docs were
removed) is gone.
Added
- PNG: Adam7 interlacing, 16-bit images with alpha (16-bit soft mask),
andtRNScolor-key transparency on grayscale and RGB images. All 161
valid PngSuite images now decode to PngSuite's own expected pixels.
Fixed
- Documents with more than 32 images, embedded fonts, SVGs or opacity
states (or more than 16 gradients) produced broken PDFs: resource names
collided. output()could fail withWriteFailedon large page dictionaries.- Crashes on hostile input, found by fuzzing and an audit of ~50 sites:
integer overflows and out-of-bounds reads in the TrueType, OpenType
(GSUB/GPOS), COLR, sbix, CBLC and SVG-table parsers, the PNG/JPEG
decoders and the TrueType/CFF subsetters. - CFF offset sizes were off by one: a 255-byte (or 65,535-byte) item
crashed subsetting — reachable with ordinary fonts. - SVG: non-finite numbers were written into the PDF as
inf; elements
leaked on errors, in<defs>without an id, and on duplicate ids. - PDF/A output failed veraPDF: invalid dates (a Zig 0.17 formatting
change), link annotations without the print flag, and standard fonts. - README examples that no longer compiled.
Internal
- Zig 0.17 migration; the version is single-sourced from
build.zig.zon. - Tests: exact pixel comparison of images (none before), PngSuite against
its own pixel truth, veraPDF validation of PDF/A output, and silent
passing runs. 508 tests. - Releases follow the urverk devkit main/dev model (
.devkit.conf).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
2 downloads
- Numbers: every