v0.9.16
PeachPDF 0.9.16 Release Notes
Release date: September 5, 2026
Full changelog: v0.9.15...v0.9.16
Release tag: v0.9.16
PeachPDF 0.9.16 finishes the mixed-page-orientation/size work that's been landing in layers: @page rules can now give individual pages their own physical size, and position: fixed content, the initial containing block, and per-page text reflow all resolve correctly against that per-page geometry. It also closes out this repo's OpenType text-shaping arc — GPOS/GDEF/GSUB completion, real Arabic/Syriac joining and Devanagari/Bengali/Gujarati/Tamil Universal Shaping Engine support (in both HTML and SVG text), and a bidi nesting-order fix — plus three new CSS features (quotes, :lang(), legacy clip: rect()) and tab-size. Finally, it adds an opt-in PDF/A (ISO 19005) conformance mode covering parts 1–3 at levels B/U/A.
Highlights
Mixed page orientation and size support is complete (#869, #870, #871, #872, #873, #874, #875, #877, #878, #879, #881, #882, #883)
- A named or pseudo-selector
@pagerule'ssize(e.g.@page landscape-table { size: A4 landscape; }combined withpage: landscape-tableon an element) now produces a genuinely independent PDF page — its own/MediaBox— instead of being silently ignored. Content reflows to match: text re-wraps line-by-line to each page's own measure as it crosses a page boundary (including a document that only varies physicalsize, with no margin override at all), an auto-width block's own border/background box resizes to match, and a named page's own opening content is measured against its real (not stale) geometry from the moment it opens position: fixedcontent now tracks each page's own area throughout: percentageleft/top/width/heightresolve against the page the fixed copy is actually being drawn on rather than one global base area, and fixed content positioned inside the page margins (a corner badge, watermark, or running header near the physical edge) now paints at all — it previously vanished on every page as soon as any@pagerule existed, regardless of margin overrides- The initial containing block's height now tracks
@page :first's own margin override, so a root-relative percentage-height element sizes against the first page's real content band instead of the document's base page size - Two convergence bugs in the per-page reflow loop are fixed alongside this work, and a mixed-page-orientation showcase was added to
PeachPDF.TestHarness
OpenType text shaping: GPOS/GDEF/GSUB completion, Arabic/Indic complex-script support, and SVG parity (#887, #888, #889, #890, #891)
- GPOS, GDEF mark filtering, and GSUB multiple/contextual/chaining substitution now work, along with per-element language selection for shaping (an element's own
lang— not just<html lang>— now selects the font'sLangSys). Fixed alongside: three CSS bidi bugs where generated content (::before/::after/::marker, footnotes) never participated in bidi resolution at all - Real Arabic/Syriac joining (a ported HarfBuzz state machine covering seven joining forms, including Syriac's
ALAPH/DALATH_RISHalternates) and Universal Shaping Engine support for Devanagari, Bengali, Gujarati, and Tamil (syllable classification, conjunct/reph formation, pre-base matra reordering) now render real complex-script text correctly instead of falling back to unshaped/isolated glyph forms — verified glyph-for-glyph against real HarfBuzz output through real Noto Sans fonts - All of the above now works in SVG
<text>/<tspan>as well as HTML, via a new "shaping run" concept that lets a maximal same-script stretch of characters shape and reorder as a unit the way an HTML word already does - Fixed alongside: copying or searching a mirrored right-to-left word (e.g. a parenthesized Hebrew phrase) out of the generated PDF previously extracted the wrong (visually-mirrored) characters instead of the true logical text; this is now correct in both HTML and SVG. Also fixed: a crash on a malformed font whose GSUB/GPOS contextual rule declares a zero glyph count
Three new CSS 2.1/Selectors features, plus tab-size (#866, #867, #868, #886)
- The
quotesproperty and theopen-quote/close-quote/no-open-quote/no-close-quotecontentkeywords are implemented, so<q>and custom quotation styling viacontent: open-quotenow render real, author- or locale-appropriate quotation marks instead of being inert - The
:lang()pseudo-class now does real matching (againstlangattributes andContent-Language-style inheritance) instead of never matching - The legacy CSS 2.1
clip: rect(...)property is implemented (superseded byclip-pathfor new content, but still authored in the wild) tab-size(CSS Text 4 §3.6) is implemented forwhite-space: pre/pre-wrapcontent, expanding a literal tab character to the correct stop width, including mid-line and across sibling inline elements
A nested bidi override's stacking order is fixed (#884)
- Two
unicode-bidiboxes sharing the same start index (the common shape for adir-bearing wrapper around anotherdir-bearing element with no text of its own before the child) previously had their overrides pushed in the wrong order, silently collapsing what should have been two distinct bidi embedding levels onto one and erasing the isolate boundary between them. Nesting order is now preserved regardless of whether a box has its own leading text
Opt-in PDF/A (ISO 19005) conformance (#892)
- A new
PdfGenerateConfig.PdfAConformancesetting (default off, byte-for-byte unchanged output) covers PDF/A parts 1, 2, and 3, each at level B (visual-only), U (guaranteed Unicode text-extraction), and A (accessible). Parts 2 and 3 add an embedded sRGB ICC output intent, an XMP metadata stream, and a PDF 1.7 version bump; the accessible ("A") levels require a resolvable document language and force on tagged-PDF structure, with every tagged image guaranteed an/Altentry - PDF/A-1 forbids PDF transparency groups outright, and PeachPDF has no engine to flatten opacity/masks/blend modes into a conformant form — so generation now throws rather than silently emitting a non-conformant file whenever a document uses opacity, fill/stroke-opacity, an alpha gradient stop, an SVG
<mask>, an image's own alpha soft mask, or a non-default blend mode underPdfA1B/PdfA1A. A transparency-free document generates correctly under PDF/A-1 - Also new: an independent
EnableXmpMetadataopt-in (an XMP stream without full PDF/A conformance),PdfDocumentMetadata.CreationDate/CustomXmpPropertiesfor a caller-supplied creation date and arbitrary additional XMP metadata, and font descriptorSerif/FixedPitch/Italicflags now derived from real OpenType metadata instead of being left unset
Breaking changes & migration notes
Mixed page orientation/size support changes several previously-documented boundaries. Each of the following was explicitly called out in docs/html-css-support.md as not working as of v0.9.15 and now does, so a document that happens to rely on the old (documented) limitation will see a visual change:
- A box spanning a page boundary no longer keeps its start-page measure across its fragments — its text, and (for a straddling main-column block) its own border/background box, now re-wrap/resize to each page's own content width. A block with a background color spanning pages of different widths will now show that background at the correct width instead of the original page's width.
position: fixedpercentageleft/top/width/heightnow resolve per page instead of against one global base area, on any document that mixes physical page sizes via@page. An absolute-length offset/size is unaffected.position: fixedcontent positioned inside the page margins now actually paints, on every page, instead of silently never painting as soon as any@pagerule exists.- The initial containing block's height now resolves against
@page :first's own margin override rather than the document's base page area (width, viewport units, and the fixed-position containing block are unchanged). - A named/pseudo
@pagerule'ssizenow produces a real, independently-sized PDF page, and a document that mixes only physical sizes (no margin override) now reflows its content to match. Horizontal reflow onto such a page still uses the document's base measure unless the content has an explicit width — full auto-reflow onto a differently-sized named page remains tracked as a follow-on (issue #202).
hyphens: auto now follows the nearest ancestor lang, not only <html lang>. Previously the whole document was hyphenated using one language, resolved once from <html lang> (or the config default). A document that mixes languages via lang attributes on nested elements (e.g. a French quotation inside an English article) now hyphenates each part with its own language's patterns — matching real browsers. A document with a single lang is unaffected.
A PDF page's /Group transparency dictionary is no longer added unconditionally. Every generated page previously carried a /Group << /S /Transparency /CS /DeviceRGB >> dictionary regardless of content; it's now added only when a page actually uses opacity, a semi-transparent gradient stop, an SVG <mask>, or a non-default blend mode. This is a pure size/cleanliness difference in ordinary (non-PDF/A) output — no PDF viewer renders affected pages differently — but it is a genuine, observable difference in the raw PDF bytes that a byte-for-byte comparison against prior output would catch.
A GSUB contextual lookup can now target another contextual lookup as a nested substitution. Previously a font whose feature resolved a glyph's final form through a chain of contextual lookups (found in a real Noto Sans Gujarati font) left the glyph at its default, un-substituted form; PeachPDF now recurses into the nested lookup and renders the font's intended glyph. This is a rendering-correctness fix, script-agnostic, and only affects fonts authored with this pattern.
See Forward compatibility in the HTML/CSS support docs for how PeachPDF treats this class of change going forward.
All Merged Pull Requests
- Implement the CSS 2.1 quotes property and open/close-quote content keywords — #866
- Implement real matching for the :lang() pseudo-class — #867
- Implement the legacy CSS 2.1 clip: rect() property — #868
- Resolve @page size per matching rule; fix bare orientation keyword — #869
- Resolve per-slot physical sheet size in PageGeometryTable — #870
- Resolve position:fixed percentage width/height against the page area — #871
- Narrow the ICB height to the first page's own band; document the pins — #873
- Make float displacement page-width-aware — #872
- Give each PDF page its own independent /MediaBox — #874
- Re-wrap main-column block text to each fragmentainer's own measure — #877
- Fix two per-page reflow convergence bugs; widen the reflow gate to size-only overrides — #878
- Resolve position:fixed percentage offsets against each page's own area — #875
- Resize a position:fixed box's percentage width/height per page — #879
- Fix position:fixed content inside page margins silently not painting — #881
- Add mixed page orientation showcase — #882
- Resize a straddling main-column block's own border box per fragment — #883
- Fix nested bidi override nesting order for boxes sharing a start index — #884
- Add CSS tab-size property (CSS Text 4 §3.6) — #886
- Add GPOS, GDEF mark filtering, GSUB multiple/contextual substitution, per-element language shaping — #887
- Add Arabic/Indic complex-script joining, GSUB/GPOS extensions, SVG text parity, and RTL ToUnicode fix — #888
- Extend Universal Shaping Engine support to Bengali, Gujarati, and Tamil — #889
- Port Arabic-family joining and Devanagari USE reordering to SVG text — #890
- Fix a crash on a malformed GSUB/GPOS contextual rule with glyphCount 0 — #891
- Add opt-in PDF/A conformance (ISO 19005 parts 1/2/3, levels B/U/A) — #892