PeachPDF 0.9.5 Release Notes
Release date: July 21, 2026
Full changelog: v0.9.4...v0.9.5
Release tag: v0.9.5
PeachPDF 0.9.5 centers on CSS Paged Media correctness and font capability: per-page @page geometry with true full-bleed pages, spec-correct px resolution (1px = 0.75pt), relative units in @page rules, @font-face unicode-range with per-character font matching, and emoji/astral codepoint rendering — plus a batch of pagination, table, and Acid2 paint fixes.
Highlights
CSS Paged Media: per-page geometry and full-bleed pages (#142)
@page { margin: 0 }was silently ignored (treated as unset), making full-bleed pages impossible — fixed (closes #125), and the shared length parser now delegates to the CSS-OMLengthstruct instead of a duplicate hand-rolled one- Implemented the CSS Paged Media 3 page-box model for per-page margin overrides: when
@page :first,:left/:right, or named-page rules override top/bottom margins, each pagination slot gets its own content-band height and cumulative top — enabling true four-edge full-bleed pages and variable-height content bands that genuinely affect where text breaks - New
PageGeometryTable(per-slot band geometry) andPageRuleResolver(unified@pagecascade shared by layout and paint) make paint unable to disagree with layout about a page's geometry - Exact-boundary forced breaks per CSS Fragmentation 3 §4.4: a sibling ending flush on a page boundary satisfies the break, so an exact-fit cover page no longer manufactures a blank page
- Link annotations now use the shifted page grid and map to materialized pages (they were silently dropped after any skipped blank page)
Spec-correct CSS px resolution (#160)
pxnow resolves at 1px = 1/96in = 0.75pt engine-wide per CSS Values & Units §6.2, replacing the old dual convention (1px = 1pt for layout but 0.75pt for fonts) — output now matches browser print behavior (closes #150)- The original symptom:
@page { margin: 96px }and@page :first { margin: 96px }resolved through different paths and produced different band heights for textually identical margins - A single
Length.PointsPerPxconstant is now the only place the ratio lives, covering body layout, fonts, borders, backgrounds, image/SVG intrinsic sizes,@pagegeometry, andcalc() - See the new "Length units" section in docs
Relative units in @page margins, size, and margin boxes (#164)
%,em,ex, andremin@pagemargins,@page { size }, and margin-box dimensions now resolve against the correct context per CSS Paged Media 3 §7–8 (root font forem/rem, margin-area dimensions for%), instead of being silently ignored or mis-resolved (closes #154, #155, #156)- Units invalid in a page context (viewport units,
ch) are dropped as invalid declarations per CSS error handling, leaving the configured value in place rather than resolving to zero
Named-page @page styles no longer leak onto later default pages (#167)
- After a
page: <name>element ended, content reverting to the default page kept the named page's margins and margin-box suppression — the used value ofpagewas resolved flow-based and forward-sticky - Now computed tree-based per CSS Paged Media 3 §3 (own
pageunlessauto, else the parent's used value), with reversion entries registered so geometry and margin boxes revert correctly (closes #126); flex/table/multicol reversion is tracked separately in #166
@font-face unicode-range and per-character font matching (#174)
- The
unicode-rangedescriptor was parsed but never honored — font matching is now per-character (codepoint-aware) following the CSS Fonts 4 matching algorithm, and the same mechanism gives glyph-coverage fallback across thefont-familystack (a codepoint one family lacks falls through to the next, including system fonts via their actualcmapcoverage) (closes #158) - Emoji and other supplementary-plane (astral) codepoints now render: the CID text pipeline moved from UTF-16
chartoSystem.Text.Rune, cmap format-12 subtables are read, and a word-break bug that split every astral character's surrogate pair into two U+FFFD words is fixed - Custom fonts are now content-addressed, so two different files sharing one internal name (a common webfont-subset pattern) no longer collide
- New public API:
AddFontFromStream(stream, RuneRange[])restricts a registered font to codepoint ranges — the programmaticunicode-range - Known limits, documented: color emoji renders as monochrome outlines (tracked in #175), and there is no text-shaping stage (no
GSUB/GPOS, Bidi, or Arabic joining)
Table pagination fixes (#153, #161)
- Headings with
page-break-after: avoidare no longer stranded when the following table is relocated to the next page — keep-with-next run pulling now applies in the margin-crossing relocation path, and repeating-header tables where the header fits but the first body row doesn't are now relocated too - A repeating
<tfoot>row-group had a zero-width bounding box (ActualRightwas never assigned, unlike<thead>'s), breaking its per-page repeat rendering (closes #124)
Replaced elements in flex containers vs. page breaks (#170)
- An
<svg>/<img>that was the sole child of adisplay: flexcontainer could paint its content well below its own box after an ancestor table relocated the row to the next page — flex's throwaway measurement layout passes ran at provisional positions and could trigger word-level page-break jumps that stuck (closes #171) - Fixed by suppressing word page-break decisions during measurement-only layout passes; the documented trade-off is that word-level page-break avoidance is unavailable inside
display: flexcontainers - Two pre-existing SVG
clipPathspec gaps found during the investigation are filed as #168 and #169
Acid2 paint visibility and background tiling fixes (#177)
CssBox.Paint()'s visibility pre-check used the containing block's client rectangle instead of the box's own line rectangles, so an inline replaced element inside a deliberately zero-height container (Acid2's "eyes") was wrongly skipped entirely — background and all- Independently,
background-attachment: fixedrepeat loops tiled across the full page viewport instead of the visible clip area, emitting ~200k+ draw operators for one small clipped element — now bounded by the actual visible intersection
counter(name, style) rendering (#165)
- The two-argument
counter()form incremented the counter but emitted no text at all; the style argument was never read (closes #128) - A shared counter-style formatter now serves both list markers and
content: counter(), withdecimal-leading-zerosupport and spec-correct fallback todecimalfor unknown styles (CSS Counter Styles 3 §2)
Documentation and support policy (#173, #179)
- Documented the .NET support policy — PeachPDF supports .NET 8 and every newer currently-supported .NET version, aligned with Microsoft's lifecycle — in the README and getting-started guide; CI now skips heavy test steps for doc-only changes
- Major documentation expansion: architecture.md now covers per-page content bands, blank-page skipping/margin truncation/keep-with-next, CSS2.1 Appendix E stacking order, and the full font subsystem (codepoint-aware resolution,
Rune-based CID pipeline, content-addressed font identity); a new testing.md documents the test suite, CI pipeline, diff-coverage gate, and the two-renderer (PDFium + MuPDF) rasterization verification practice
All Merged Pull Requests
- CSS Paged Media: per-page geometry and full-bleed pages — #142
- Fix keep-with-next when tables page-break — #153
- Fix CSS px unit to resolve spec-correctly at 0.75pt — #160
- Fix repeating tfoot row-group having a zero-width bounding box — #161
- Resolve relative units in @page margins, size, and margin boxes — #164
- Fix counter(name, style) rendering nothing — #165
- Fix named-page @page styles leaking onto later default pages — #167
- Fix SVG content painting outside its box after page-break relocation — #170
- Document .NET support policy and compare it across libraries — #173
- Honor @font-face unicode-range (per-character matching) and render astral/emoji codepoints — #174
- Fix Acid2 eyes disappearing behind an un-interlocked checkerboard — #177
- Bump package version from 0.9.4 to 0.9.5 — #178
- Document architecture, testing, and font subsystem; add testing.md — #179