All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Hardening release: signature verification is now chain-aware with an
aggregate verdict, the optimizer refuses to silently strip compliance data,
parsers fail closed on malformed input, and multi-page table layout is ~2.3×
faster. Note one behavior change: optimize.Bytes returns ErrLossyInput
for input carrying PDF/A output intents, XMP metadata, embedded files,
tagging, or a document language unless Options{AllowLossy: true} is passed.
sign.SignatureResult.OK()— single aggregate verdict that requires a valid digest and signature, a/ByteRangecovering the whole file, and a trusted chain; closes the incremental-update tamper gap whereDigestValidalone stayed true for a partially covered file.optimize.OptionswithAllowLossyopting in to optimizing documents that carry document-level compliance data (seeErrLossyInput).zugferd.NewAmountChecked/Amount.AddChecked— overflow-checked amount construction and addition for data-driven input.- Third-party encrypted-PDF fixtures — the reader's encryption tests now
parse qpdf-generated AES-256, AES-128, RC4-128, owner-password-only, and
EncryptMetadata falsefiles instead of only round-tripping folio's own writer. - TSA / OCSP / document-timestamp tests — the PAdES B-LT path
(
TSAClient,OCSPClient,AddDocumentTimestamp,AddDSS) gained its first test coverage, driven by local httptest servers.
sign.Verifyparses full certificate chains — the signer certificate is selected by the SignerInfo's issuer-and-serial, remaining certificates feed chain building as intermediates, the RFC 5652contentTypesigned attribute is required,HasTimestampreports only a real RFC 3161 timestamp attribute,/Contentshex strings tolerate whitespace and odd digit counts, and signatures and/DSSare located through the AcroForm object graph (with a raw-scan fallback) so decoy dictionaries in stream payloads are no longer reported and all SignerInfos are examined.optimize.Bytesrefuses compliance-bearing input withErrLossyInputunlessOptions{AllowLossy: true}is passed (behavior change — see above).- AES-256 (R6) decryption validates
/Perms— the encrypted permissions block is decrypted and checked against/P, so a tampered permission word is rejected instead of silently trusted. - CI: fuzz crashes now fail the build (five targets, including the
previously missing
FuzzParsePDF), tests run on an ubuntu/macos/windows matrix, and the wasm binary is build-checked.
-
optimizepreserves inherited page attributes —/MediaBox,/CropBox,/Rotate, and/Resourcesinherited from an ancestor/Pagesnode are materialized onto each rewritten page; inputs relying on inheritance previously came out with no page size or resources. -
zugferdamounts no longer wrap on overflow — parsing bounds the digit count, arithmetic is overflow-checked in validation, andAmount.Stringrendersmath.MinInt64correctly; wrapped values could previously satisfy the totals validation while emitting corrupt XML. -
CSS colors fail closed — malformed hex or component values no longer parse as opaque black, and NaN/Inf color components and lengths are rejected before they can reach the PDF content stream.
-
border-collapseresolves spanned edges per grid segment — acolspan/rowspancell's shared edge is resolved against every neighbor along the span instead of only the first, so mixed neighbor borders no longer erase or misdraw grid-line segments. -
Oversize rowspan groups split across pages — a rowspan group taller than a full page falls back to per-row splitting (continuing the spanning cell as a styled placeholder) instead of silently drawing past the bottom margin.
-
text-overflow: ellipsisworks in rendered documents — truncation now runs in thePlanLayoutrender path used by HTML conversion, not only in directLayoutcalls. -
Long-word breaking accounts for
letter-spacing, and guards against empty runs and negative available widths (previously a potential panic). -
LayoutandmeasureWordsshare one word-flattening path — the measure and render pipelines had diverged copies that could disagree on break points. -
Internal anchor links now register a PDF named destination — a block-level element with an
id(<h2 id="section">,<div id="details">, …) now auto-registers a named destination, so<a href="#section">resolves to a direct/Deston the target's page instead of emitting a dangling/GoTostring action that jumps nowhere. The HTML walker wraps any block element carrying anidin alayoutanchor marker that tags its firstPlacedBlock; the renderer surfaces these onPageResult.Anchors, and the document layer registers them viaAddNamedDestduring layout — no callerdoc.AddNamedDest()needed. Resolved links use an/XYZdestination at the target's y-position with zoom retained (the same shape auto-bookmarks use, so a link and an outline entry to the same target behave identically); the annotation resolver and the/Destswriter share one destination-array builder and agree first-registration-wins on duplicate names. The anchor and bookmark markers now expose anunwrapso the layouter still reaches a decorated element's optional interfaces (CSSclear,page-break-inside: avoid, flex cross-axis stretch) — previously wrapping an element to record itsidsilently disabled those. Inline targets (<span id>, the<a id>/<a name>idiom) are out of scope: the destination is registered only for block-level elements. This restores thelayout.Anchorbehavior documented under 0.8.0 (#223), which was absent from the tree. -
/XYZdestinations emit an explicitTopcoordinate — named destinations and outline entries previously wrotenullfor a zeroTop, which viewers read as "retain current y", so a target aty=0navigated nowhere.Topis now always an explicit real;Leftand zoom keep null-for-zero to preserve the reader's horizontal scroll and zoom. -
letter-spacingapplies to inter-word gaps and list items — the tracking previously stopped at each word boundary, so spaced text showed normal-width spaces between letter-spaced words; the gap now carries the spacing, and<li>content (including markers' text) honors it too. -
Table caption and section styling — a
<caption>now applies its own style rather than inheriting the table's;display: noneon<thead>/<tbody>/<tfoot>hides the section; and a section's own style (color, font, background) cascades to its cells' content as in browsers. -
Reader guards the page tree against cycles — a malformed or hostile PDF whose
/Pagestree contains a reference cycle or pathological depth no longer hangs or recurses unboundedly; the walk detects revisited nodes and depth limits and fails with an error. -
WASM build honors document settings — page orientation, margins, document metadata, and custom fonts passed through the wasm bindings were previously ignored; they now reach the document builder.
- Multi-page table layout ~2.3× faster — paragraph intrinsic widths are memoized and resolved column widths are cached across page-break continuations (~59% fewer allocations on a 200-row table).
- The stray
linksbinary is no longer tracked and is removed bymake clean.
Adds offline signature verification, encrypted-PDF reading, a Factur-X/ZUGFeRD invoice package, and a standalone PDF optimizer, alongside a security-hardening pass across the reader, font, and C ABI layers against malformed input. This release carries breaking changes: font.Face now declares its shaping accessors directly instead of through optional provider interfaces, html no longer fetches remote or absolute-path assets by default, and the C ABI's folio_last_error changes pointer-ownership semantics.
sign.Verify(pdfBytes []byte, opts sign.VerifyOptions) (*sign.Report, error)— offline verification for PDFs signed withsign.SignPDF. For each signature it locates the signature dictionary, recomputes the/ByteRangedigest and checks it against the CMSmessageDigestsigned attribute, verifies the CMS signature (RSA PKCS#1 v1.5 or ECDSA) over the signed attributes, checks that/ByteRangecovers the whole file except the/Contentshex string, and — whenVerifyOptions.Rootsis supplied — builds a certificate chain.sign.Report.Signaturesreports each check independently (DigestValid,SignatureValid,ByteRangeCoversFile,ChainStatus) so partial validity is never conflated with full validity. Revocation fetching, timestamp-token and/DSScontent validation, and PAdES level classification are out of scope; presence of a timestamp token or/DSSis reported but not validated.document.Page.SetDebugMediaBox/document.Document.SetDebugMediaBox— strokes a rectangle around a page's true MediaBox for visual layout debugging (#376). Drawn last, on top of all other page content including the watermark. A page-level call overrides the document-wide default for that page.readercan now open PDFs encrypted with the Standard security handler —reader.ReadOptions.Passwordauthenticates against the document's user or owner password (ISO 32000 §7.6.3-7.6.4);reader.Parseimplicitly tries the empty password, the common case for files "protected" only to restrict permissions. Supports RC4-128 (R3, read-only legacy), AES-128 (R4/AESV2), and AES-256 (R6/AESV3); strings and stream payloads are decrypted transparently at object-resolve time, so every existing reader API (text extraction, merge, redact, page import) works unchanged against the decrypted content.reader.PdfReader.Access()reports whether the user or owner password matched. A wrong password returnsreader.ErrInvalidPassword; unsupported security-handler configurations (public-key handlers, RC4-40, non-standard crypt filters) returnreader.ErrUnsupportedEncryptioninstead of the previous blanket refusal. Decrypted documents are plaintext in memory and are written back out unencrypted unless the caller explicitly re-encrypts viadocument.SetEncryption.zugferdpackage — generates Factur-X/ZUGFeRD hybrid e-invoices: a typedInvoicerenders EN 16931 UN/CEFACT Cross-Industry Invoice (CII) XML andInvoice.Attachembeds it into adocument.Documentas a PDF/A-3B associated file with the matching Factur-X XMP schema, keeping the attachment filename,AFRelationship, and XMP conformance level in sync. Covers the MINIMUM and BASIC Factur-X 1.0 profiles, with hand-written field-presence and totals-arithmetic validation (Invoice.Validate) and a fixed-pointAmounttype so money never touches a float.examples/zugferdis ported onto it. Seedocs/design-zugferd.mdfor profile coverage and known limitations.optimizepackage —optimize.Bytes(data []byte) ([]byte, optimize.Stats, error)parses an existing PDF and re-serializes it through the writer's lossless passes (cross-reference streams, object streams, orphan sweep, stream recompression, object deduplication). The rewrite carries pages and their resources only — outlines, AcroForm fields, attachments, and other document-level structure are dropped — and falls back to the original bytes whenever the rewrite would not shrink the file, so output is never larger than input. Encrypted input returnsoptimize.ErrEncrypted.html.Options.AllowRemoteFetch— opt-in for fetchinghttp(s)assets referenced by the document. Default false.html.Options.AllowAbsolutePaths— opt-in for reading absolute filesystem paths named in document content whenBaseFSis nil. Default false; reads are size-capped like the HTTP path.html.DenyInternalHosts— aURLPolicythat blocks non-http(s) schemes and targets resolving to loopback, private (RFC1918 / ULA), link-local, unspecified, or multicast addresses (plus carrier-grade NAT, and IPv4-compatible / NAT64 IPv6 forms that embed such addresses). Applied by default whenAllowRemoteFetchis true andURLPolicyis nil, and re-checked on every redirect hop.html.Options.MaxTotalAssetBytes— caps the aggregate bytes read across every asset loaded during one conversion (images, fonts, stylesheets — remote,BaseFS-relative, and absolute alike). 0 selects a generous 512 MiB default; loads that cross the cap fail withhtml.ErrAssetBudgetExceeded.html.ErrRemoteFetchDisabled— returned when a document references anhttp(s)asset butAllowRemoteFetchis false. Reported normally (logged, and surfaced underStrictAssets).html.ErrAbsolutePathDenied— returned when a document references an absolute filesystem path butAllowAbsolutePathsis false. Reported normally.html.ErrAssetBudgetExceeded— returned when a conversion's aggregate asset read size crossesOptions.MaxTotalAssetBytes. Reported normally.- C ABI:
folio_buffer_len64(buf)— 64-bit counterpart tofolio_buffer_len, which now saturates atINT32_MAX(and records an error viafolio_last_error) for buffers over 2 GiB instead of silently truncating. Prefer the 64-bit accessor for large buffers. - C ABI:
folio_string_free(s)— releases a string returned byfolio_last_error, which now hands back a caller-owned copy (see Changed (breaking)). Passing NULL is a no-op.
font.Facenow includesGSUB(),GPOS(), andGIDToUnicode()— the optionalfont.GSUBProviderandfont.GPOSProviderinterfaces are removed, and everyFaceimplementation must provide the three methods directly. AFacewith no shaping data for a given table returns nil rather than omitting the method;GIDToUnicodereturns nil when the font has no cmap-derived reverse map. This drops the type-assertion indirection that callers previously used to probe for shaping support (face.(font.GSUBProvider),face.(font.GPOSProvider)) now that the seam is no longer needed pre-1.0 (#404).htmlno longer fetches remote or absolute-path assets by default — a document'shttp(s)references (<img>,background-image: url(), linked stylesheets,@font-face url()) are only fetched whenOptions.AllowRemoteFetchis set, and absolute filesystem paths in document content are only read whenOptions.AllowAbsolutePathsis set (both default false). When remote fetch is enabled with noOptions.URLPolicy, the built-inhtml.DenyInternalHostspolicy blocks loopback, RFC1918, link-local, and non-http(s) targets and is re-checked on every redirect hop; absolute-path reads are size-capped like HTTP reads. SetURLPolicyto a function returning nil to allow every host.Options.FallbackFontPathand the built-in system-font search are unaffected (#391).- C ABI:
folio_last_errornow returns a caller-owned copy — previously the returnedchar*pointed at library-internal storage, valid only until the next C ABI call; callers treated it as borrowed and never freed it. It now returns a fresh copy that must be released with the newfolio_string_free. A caller that keeps the old borrow-and-never-free assumption leaks one short string per call tofolio_last_errorinstead of crashing or corrupting memory, until it adoptsfolio_string_free(#392).
- CSS named colors now resolve to full-precision sRGB values in both
htmlandsvgoutput — the two packages each carried an independent 148-entry named-color table stored as rounded decimal literals (3 decimals inhtml, 4 insvg), so the same keyword could render to different bytes depending on which package handled it. Both now share one table ininternal/csscolorthat computes each component asx/255at fullfloat64precision, which changes the emitted bytes for named colors slightly versus the old decimal literals in both packages.svgcolor parsing also now accepts the full CSS color grammar (4/8-digit hex, CSS Color 4 space-separatedrgb(), andhsl()/hsla()) thathtmlalready supported — previously-validsvginputs are unaffected. As part of sharing the parser, malformedsvgcolor literals (invalid hex digits, non-numericrgb()/alpha components) are now treated leniently — the bad component defaults to 0 rather than rejecting the whole color, matching howhtmlalready handled them. internal/cssunit—htmlandsvgpreviously hand-rolled length/unit parsing ("12px", "10pt", "50%") at five independent call sites, each with its own unit list and quirks. Both now share one tokenizer,internal/cssunit.Parse; each site keeps its own conversion policy (htmlresolves to PDF points,svgtreats every unit as an SVG user unit 1:1). Twosvgparsing corrections fell out of the consolidation — see Fixed.
border-collapse: collapsenow resolves competing borders on shared cell edges per CSS 2.1 §17.6.2 (width, then style priority, then source order;border-style: hiddenalways wins regardless of width) — previously the collapse code only suppressed duplicate borders by cell position, so a narrower or undeclared border on one side of a shared edge could silently win over a wider, explicitly declared border on the other (#378).li::marker { content: counter(list-item) }now numbers list items —<li>implicitly increments the CSSlist-itemcounter and<ul>/<ol>implicitly reset it, per CSS Lists Module 3, so::markercontent driven bycounter(list-item)(orcounters(list-item, sep)for nested lists) resolves correctly instead of always reading0.::marker { font-style: italic }is also now honored.@pagemargin boxes (@top-*/@bottom-*) now honorfont-style,font-weight, andfont-family— previously onlyfont-sizeandcolorwere applied to generated margin-box content;font-style: italicand similar declarations were silently dropped (#378).- Table
rowspangroups stay intact across a page break — a rowspan cell and every row it covers are now kept together as one atomic unit when a table splits across pages; a group taller than a full page falls back to drawing past the page bottom, same as an oversized plain row (#362). svgdimension parsing no longer accepts multi-suffix garbage — attribute values likewidth="10empx"were parsed by stripping every known unit suffix in sequence, so leftover garbage silently fell through to a clean number instead of failing.svglength parsing now consumes exactly one unit suffix; anything left over is rejected.svgfont-sizestyle now acceptspt-suffixed values —font-size: 12ptpreviously failed to parse (onlypxwas recognized) and silently left the inherited font size in place; it is now read as 12 user units, consistent with how thewidth/height/rattributes already treatpt.- Array-form
bfrangedestinations now parse inToUnicodeCMaps — abeginbfrange/endbfrangeblock whose destination is an array of individual code points (rather than a single starting hex value) was silently dropped, so text extraction lost the mapped characters for fonts using that CMap form (#387). - Margin boxes draw in a fixed positional order — the renderer previously iterated margin boxes via Go map iteration, which is intentionally randomized, so content-stream bytes and embedded font-resource ordering varied between otherwise-identical runs. Margin boxes now draw in a fixed top-left/top-center/top-right/bottom-left/bottom-center/bottom-right order, making output byte-reproducible under
document.WriteOptions.Deterministic. - PNG predictor honors
/Colorsand/BitsPerComponent— the reader's PNG-predictor decoder assumed one 8-bit component per sample regardless of the stream's actual/DecodeParms, corrupting decoded row data for multi-component (RGB) or non-8-bit predicted streams. It now computes bytes-per-pixel and row stride from the declared/Colorsand/BitsPerComponent, with hostile values sanitized to safe defaults. - A tokenizer zero-progress case could hang the reader — a stray
),{, or}byte produced an empty keyword token, so a caller looping onNextuntilAtEndnever advanced. Fixed alongside the encrypted-PDF reading work. - Multi-column floats,
float: right,clear, and float containment — floats previously worked only as a single-float width reduction at the top of the page flow. A float now resolves its width against the full containing block rather than the space left by sibling floats (so percentage-width float columns no longer collapse andfloat: rightlands at the right edge), multiple same-side floats stack instead of overlapping, an in-flow block that cannot fit beside the floats drops below them, and a newDiv-level float context handles placement,clearamong children, and growing the container to enclose its floats (the flow-root model) (#367). - Anonymous table-row generation for bare
display: table-cellchildren — adisplay: tableelement whose direct children weredisplay: table-cellboxes with nodisplay: table-rowwrapper treated each bare cell as its own single-cell row and stacked them vertically instead of forming one horizontal row, per CSS table box fixup. Consecutive bare cells are now wrapped in an anonymous row (#367).
- Reject negative or oversized
/Wwidths in xref streams — a crafted/Warray with a negative or very large field width could drive the entry-size computation negative and panic on the resulting allocation; out-of-range widths (outside[0, 8]) now fail with an error before any allocation. - Bound cmap range expansion against hostile overlapping ranges — format 4 and format 12
cmapsubtables now cap the total number of code points expanded across all segments/groups at1<<22(about 4x the legitimate maximum of the full Unicode range), rejecting fonts whose overlapping ranges would otherwise expand into an unbounded map and burn CPU. - Bound WOFF table decompression before
ReadAll— WOFF decoding now caps the declared total uncompressed size of all tables at 256 MB and limits each table'szlibdecompression to its declared size plus one byte, so a decompression bomb fails fast instead of materializing an unbounded buffer. - Validate image dimensions from the header before decoding — PNG, GIF, TIFF, and WebP decoding now reads and checks the image dimensions from the format header (against
MaxDimension/MaxPixels) before allocating the pixel buffer, closing a memory-exhaustion path where a crafted header claimed a small file but an enormous canvas. - C ABI:
unsafe.Slicewith a count cap at the array boundary —(pointer, count)array arguments crossing the C ABI are now validated (count in[0, 1<<20], non-nil pointer for a non-zero count) before being turned into a Go slice viaunsafe.Slice, rejecting a hostile or mistaken count instead of risking an out-of-bounds read or crash. - Xref-stream decompression now honors the caller's
MaxXrefSize—ReadOptions.MemoryLimits.MaxXrefSizewas previously ignored for the xref-stream decompression step, which always used the hardcoded 32 MB default regardless of a caller-configured limit. sfntFacelazy caches are synchronized — the GSUB, GID-to-Unicode, and kern-pair caches on the TrueType/OpenTypeFaceimplementation are now each guarded by async.Once, so a singleFaceshared across goroutines (a common pattern for an embedded font reused by concurrent renders) no longer races on first use.
- Zero-allocation
MeasureString— eliminated the per-call grapheme-cluster allocation in the hot text-measurement path. - Eliminated quadratic prefix re-measurement in word chunking — paragraph word-chunking no longer re-measures the same prefix from scratch for each candidate break point.
- CSS rules bucketed by rightmost simple selector — style resolution indexes rules by the rightmost selector component instead of scanning every rule for every element, cutting match time on stylesheets with many rules.
- Font-descriptor metrics parsed once at face construction — instead of being re-derived on repeated access.
- CMS round-trip and tamper-detection tests for
sign.Verify. svgmalformed-input tests and fuzz targets.- A golden characterization corpus for style resolution and paragraph geometry.
- A text-layout benchmark baseline (
font,layout). - First Go-side tests for the C ABI handle table, plus an error-path stage in the C test harness (#403).
- CI coverage for the remaining runnable examples that previously shipped untested (#231).
- Mechanical split of
layout/paragraph.goand thehtmlconverter into per-seam files, no behavior change (#405). - Consolidated measurer resolution across
paragraph.go/tab.go/table.gointo a singleresolveMeasurerhelper. - Replaced
len([]rune(s))rune-counting withutf8.RuneCountInStringwhere the runes themselves weren't needed.
golang.org/x/image0.41.0 → 0.43.0golang.org/x/net0.55.0 → 0.56.0
A rendering-correctness release across border-radius (#329), CSS paged media
(@page, position: fixed, margin boxes — #327/#328), and lists
(#330/#339/#342/#347/#358), plus a from-scratch fix of the QR barcode encoder
(#341). Adds native multi-level list numbering via CSS counters (#356), table
rowspan geometry (#357), Document.AddConvertResult, and
SetPageSize/PageSize (#338). No breaking changes.
document.Document.AddConvertResult(*html.ConvertResult) error— the wiring half ofAddHTML, exported so callers who need the rawConvertResult(to inspect or transform elements before rendering) can add it back with a single correct call. It forwards everything the converter produces — normal-flow elements, absolutely/position:fixedelements,@pagesize and margins, all four margin-box sets, and<title>/<meta>metadata — and sets the page size from any@pagerule.AddHTMLis now exactlyConvertFull+AddConvertResult, so the two paths cannot diverge. This closes the discoverability trap where a hand-writtenfor _, e := range result.Elements { doc.Add(e) }loop silently dropped absolutes, margin boxes, and@pageconfig with no error (#338)document.Document.SetPageSize(PageSize)andPageSize() PageSize— read and override the page dimensions afterNewDocument. The size was previously fixed at construction even thoughAddHTMLmutates it internally from@pagerules; these expose the missing setter/getter.AddConvertResultderives the size from@pageautomatically, so the pre-NewDocumentgeometry-resolve dance is no longer required (#338)- Code 128 code sets A and C —
barcode.NewCode128now selects sets A, B, and C automatically instead of Code B only. Set C encodes digit runs as pairs (roughly halving the width of numeric payloads) and set A encodes the ASCII control characters (0–31); bytes above 127 still return an error. Round-trip and golden-pattern tests cover every code-set transition (#346) - Native multi-level list numbering via CSS counters and
::marker—li::marker { content: ... }now defines the marker string, socounter-reset/counter-incrementwithcounter()/counters()produce true nested numbering (e.g.1,1.1,1.1.1) instead of a per-level restart. Addslist-style-position(inside/outside):insiderenders the marker inline with the first content line and wraps subsequent lines under it;outside(default) places it in a gutter that auto-grows to fit a wide marker so multi-level ordinals no longer overlap the item text (short-marker lists stay byte-identical). Thelist-styleshorthand now scans all tokens (position / type / image).layout.ListgainsSetMarkerInside. Newexamples/legal-numberingrenders a nested Master Services Agreement with hanging indents. Phase 1 of #356 - Table
rowspanvertical geometry — arowspancell previously reserved grid occupancy (following rows skipped its columns) but was always drawn one row tall. The spanning cell now spans the full height of its rows:buildGridexcludes rowspanning cells from their starting row's natural height, then resolves span heights from the summed spanned-row heights plus inter-row gaps (growing the last spanned row when content needs more room), and vertical alignment centers across the full span. A rowspan straddling a page break is not yet handled (#357)
- Percentage
border-radiusrenders as a true rounded corner —border-radius: 50%was resolving against a zero reference and collapsing to 0 (square corners). It now resolves per-axis: a circle on a square box, an ellipse on a rectangle, per CSS Backgrounds §5.5 corner clamping (#329) border-radiussurvives on a text-bearing box — a box with a direct text run drew its rounded background, then the text's paragraph re-painted the same color as a square rectangle on top, squaring the corners. The redundant fill (and any matchingTextRun.BackgroundColorhighlight) is now cleared, so the common "number inside a colored circle" badge works as a single rounded box (#329)border-radiusapplied across the remaining container paths —flex: 0 0 autochips, grid items,display: blockspans,blockquote,figure, and table wrappers each now round likedisplay: inline-block. Previously only the inline-block path took the rounded-rect draw, so visually identical content-hugging boxes rounded or not depending on how they were sized (#329)
position: fixedelements render on every page — a fixed element (page watermark, pinned footer) was emitted once on the last flowed page instead of repeated on each page. It now draws on every page (#327)@page :first/:left/:rightmargins inherit the base@pagecascade — a partial pseudo-page override (e.g.@page :first { margin: 20px }) now inherits the base@page {}margins for the sides it does not set, and left/right duplex margin boxes resolve correctly (#327)@pageparsing correctness bundle — a batch of fixes to@pagerule parsing across the converter, layout, and document layers (size/orientation resolution, margin percentage/calcresolution, and margin-box cascade), so every document-building entry point applies identical paged-media geometry (#327)@pagemargin boxes embed the default font for PDF/A — running headers/footers declared via@bottom-centeretc. now carry the embedded body font, so a PDF/A document with page numbering no longer fails conformance on an unembedded font in the margin box (#328)
display: inline-blockshrinks to fit and flows inline — an inline-block box now sizes to its content (fit-content width) and participates in inline flow instead of forcing a block break (#330)- Styled
<li>lays out block children and honors its own box — a list item containing block-level children (cards, nested divs) now lays them out correctly, and the<li>'s own background / border /border-radiusis drawn via a per-item box that the list layout previously lacked (#339, #342) inline-block/ explicit-width<li>hugs its content and stops double-painting — follow-up to the per-item box: a badge-style<li>now shrinks to content and clears the square background overpaint that squared its rounded corners (#342)- Ordered lists continue numbering and keep every item across a page break — a long
<ol>spanning pages restarted at1.on the second page and silently dropped the item straddling the boundary. Numbering now continues the sequence (honoring<ol start="N">), and a boundary item is split or deferred so no content is lost. Addslayout.List.SetStart/Start(#347) - Nested list markers indent per level — a sub-list's marker was drawn at the container's left margin regardless of depth, so every nested ordinal sat in the same left column while only the body text indented. Each level's marker now steps right by the accumulated parent indent, landing under its parent's content edge (LTR and RTL;
list-style-position: insidealready drew relative to the content edge). Single-level lists are unaffected (#358) - Margin-box fidelity for
@page :left/:right— fixing the wiring aroundAddConvertResultsurfaced that the left/right margin-box reconstruction dropped theHasColorflag and the embedded font that base and:firstalready carried; all four sets are now consistent (#338) examples/html-to-pdfno longer drops absolutes — the example wired the rawConvertFullresult by hand and forwarded onlyresult.Elements, silently droppingposition: fixed/absolutecontent. It now usesdoc.AddConvertResult(result), which forwards elements, absolutes,@pageconfig, and margin boxes in one call (#338)
- QR codes are now scannable —
barcode.NewQRproduced structurally valid but unscannable symbols due to four independent encoder defects: the Reed-Solomon generator polynomial was built constant-term-first (so every block's error-correction codewords were wrong), one of the two format-information copies was written bit-reversed, the version-information bits were written in reverse (breaking version 7+), and the level-H ECC-per-block table had wrong codeword counts for versions 21–40. All four are fixed and verified by an independent decoder (#341) - EAN-13 quiet zones — corrected the leading/trailing quiet-zone widths so EAN-13 symbols meet the spec and scan reliably (#346)
- Independent barcode decode tests —
qr_decode_test.goreads generated QR symbols back with an independently-built GF(256) field, cross-checks the codeword-capacity tables, and reads format/version info from the matrix; each check was confirmed to fail when its corresponding fix is reverted. Adds Code 128 (A/B/C) and EAN-13 decode round-trips, and anexamples/barcodesdemo rendering QR (all four ECC levels), Code 128, and EAN-13 into one PDF for visual/scanner verification (#341, #346) - List pagination regression tests — multi-page ordered-list numbering continuity,
<ol start>continuation across a break, unordered no-content-loss, a single item taller than a page, andSetStartclamping (#347) AddConvertResultequivalence + fidelity tests — assertAddHTMLandConvertFull+AddConvertResultleave identical document state (page size, all margins, all four margin-box sets, elements, absolutes, metadata), that absolutes and@pageconfig survive, and the:left/:rightfield-fidelity regression (#338)
using-folioClaude Code skill — a thin, version-controlled skill (.claude/skills/using-folio) capturing the stable usage patterns: preferDocument.AddHTML(orConvertFull+AddConvertResult) over hand-wiring, the asset-loading options (BaseFS,FallbackFontPath,StrictAssets), PDF/A setup, and a verification cookbook. Thehtmlpackage docs gain matching guidance steering callers toAddHTML/AddConvertResult(#338)
document.WriteOptions.Deterministic+document.Document.SetFileID([]byte)— opt-in byte-reproducible output. WithDeterministicset, the trailer/IDis derived from an MD5 digest of the serialized object set (the content-based file-identifier scheme of ISO 32000-1 §14.4) instead of random bytes, and the XMPCreateDate/ModifyDateplus embedded-file timestamps fall back to the zero time rather thantime.NowwhenInfo.CreationDateis unset — so identical input documents produce byte-identical PDFs. SetInfo.CreationDate/ModDateso the recorded dates are meaningful rather than the year-zero default.SetFileIDpins the trailer/IDdirectly, overriding both the random PDF/A identifier and the derived digest; a non-PDF/A document that would otherwise carry no/IDgains one. Encrypted documents are excluded — the standard security handler mints a random file identifier and derives the encryption key from it (§7.6.3.3), so their output is never byte-stablehtml.ParseErrorandhtml.AssetErrorerror types — a typed taxonomy so callers (e.g. a service rendering untrusted templates) can map folio failures onto a response status witherrors.As.*ParseErrorwraps a failure to parse the input HTML into a document — an input fault; folio's lenient parser makes it rare, but it is distinct from an internal fault.*AssetErroris the typed form of the failures collected underStrictAssets: it carriesCategory("image","@font-face","background-image","stylesheet","SVG image","FallbackFontPath") andRef(the offendingsrc/href/url/path), andUnwraps to the underlying cause soerrors.Isagainstfs.ErrNotExist,ErrURLPolicyDenied, network errors, etc. keeps working. The joined error returned byConvert/ConvertFullcontains one*AssetErrorper failed reference. Any error that is neither type should be treated as an internal fault.AssetError.Error()is byte-identical to the previous untyped message, so existing log/grep expectations are unchangedhtml.Options.MaxElements/html.Options.MaxDepthresource guards +html.LimitError— bound the HTML→layout conversion so untrusted input cannot exhaust memory or the goroutine stack.MaxElementscaps the number of HTML nodes converted into layout elements;MaxDepthcaps the element nesting depth. Both default to 0 (unlimited), preserving existing behavior. When a ceiling is crossed,Convert/ConvertFullstop walking the tree and return a*html.LimitError(withKind—LimitElementsorLimitDepth— and the configuredLimit), failing closed instead of continuing to allocate. The guard sits at the singleconvertNodechokepoint every element node flows through (the<table>/grid/flex child loops included), so all conversion paths are bounded.LimitErrorjoins theerrors.Astaxonomy alongsideParseError/AssetErroras an input-side fault. Page-count and output-byte ceilings live at the layout and writer layers respectively and are tracked separatelycontext.Contexton the render entry points —html.ConvertWithContext/html.ConvertFullWithContext,(*document.Document).AddHTMLWithContext,(*document.Document).WriteToWithContext, and(*layout.Renderer).RenderContext. Each is the context-aware variant of the existing call; the original (Convert,ConvertFull,AddHTML,WriteToWithOptions,Render) is preserved and now delegates withcontext.Background(), so this addition is non-breaking. The context is checked at element boundaries during the HTML tree walk (theconvertNodechokepoint), at page boundaries during layout (the pagination and emission loops inrenderWithPlans), and at object boundaries during PDF serialization (writeObjectBodies). When the context is done, conversion/layout/serialization stops and returnsctx.Err()(context.Canceledorcontext.DeadlineExceeded); partial output is discarded. This lets a caller bound a render with a real per-render deadline rather than abandoning a goroutine that keeps burning a core on a pathological document. The context is held on the short-lived per-conversion/per-render worker structs (never shared or persisted), avoiding a signature change across every internal recursive call- Root
foliopackage façade — a new root-level package re-exports the common document-construction surface so callers can use a single stable import path:folio.NewDocument,folio.Document,folio.PageSize(and thePageSizeA0…PageSizeExecutivepresets),folio.Info,folio.WriteOptions,folio.PdfAConfig,folio.EncryptionConfig. The types are aliases and the constructor/presets are value re-exports, so they are fully interchangeable with thedocumentpackage identifiers and carry the complete method set. Specialized APIs (HTML conversion, layout, fonts) remain in their own subpackages; the façade covers only document construction
- GPOS cursive horizontal placement —
drawShapedGIDsCursivepreviously applied the entry/exit X delta as aTdshift on top of the glyph's natural advance, landing each joined glyph one extra advance past its predecessor. Per OpenType §6.3, in horizontal text the X component of the cursive join is already encoded by hmtx; the feature only aligns the join in Y. Documents using fonts with cursive joining (Arabic, several Indic scripts) tighten in horizontal direction. Cursive remains LTR-only; the RTL pass is gated on the LookupFlagRIGHT_TO_LEFTbit which is parsed but not yet honoured by the draw path (#220) - CJK paragraphs across page breaks no longer corrupt with spurious spaces —
cloneWithWordsreconstructs the overflow paragraph during page-break splitting and previously joined every same-style word with a literal" "regardless ofSpaceAfter. CJK ideographs (whichbreakCJKWordsemits withSpaceAfter=0) crossing a page break became"中 文 文 本", and the corrupted text re-wrapped to MORE lines than the original because each inserted space became a realspaceW-wide gap on re-measure. Affects any documents with CJK content that paginated in v0.7.x (#246) - Unicode NFC normalization is applied at every
layoutentry point —NewRun/NewRunEmbedded/NewParagraph*/Heading.SetRuns/Row.AddCell*/TabbedLine.SetSegments. Documents that supplied canonically decomposed strings (e.g.e+ combining acuteU+0301instead ofé) now measure and render against the precomposed form. Most input is already NFC, so for the majority of documents output is byte-identical; affected documents see corrected widths and shaping (font cmap tables that only cover precomposed codepoints stop falling through to.notdef) (#217) - Numeric
font-weightagainst standard PDF-14 fonts now rounds at 600 — pre-fix,parseFontWeightcollapsed every numeric value to the binary string"normal"/"bold"with the boundary at 700, sofont-weight: 600against Helvetica picked Helvetica (Regular). Post-fix, weights ≥ 600 select the Bold variant per CSS Fonts L4 §5.2's synthetic-bolding guidance for missing weights. Sofont-weight: 600against Helvetica now picks Helvetica-Bold;font-weight: 800continues to pick Helvetica-Bold. Documents that wrotefont-weight: 600against the standard families and expected Regular weight will see headings tighten visually. The fix also unblocks proper SemiBold/Medium rendering when the family has matching@font-facedeclarations — see Added (#286, #287) <th>honours explicittext-align: left/startinstead of silently re-centering — pre-fix, the table-cell default-center heuristic gated oncellStyle.TextAlign == AlignLeft, so an author who wroteth { text-align: left }got their explicit choice overridden because the resolved alignment matched the re-center sentinel. Post-fix, the gate is!cellStyle.TextAlignSet && resolveTextAlign(cellStyle) == AlignLeft, which preserves explicit author intent (left, orstart/endresolving to left under direction). Default<th>still centers when no author choice is set. The new semantic is more spec-correct (UA stylesheet center is a default, not an override) and matches every browser. Documents that wroteth { text-align: left }and expected the silent override will see headers shift from center to left (#288)
html.Options.BasePathremoved;BaseFSis the single way to resolve local assets — the v0.7.x compromise that kept both fields is gone. Callers pass anyfs.FS(embed.FS,os.DirFS(dir),(*os.Root).FS(),fstest.MapFS) and every local reference —<img src>,<link href>,@font-face url(),background-image: url(), andOptions.FallbackFontPath— flows through it. Paths are normalised tofs.FSconventions before the read: forward slashes, no leading/, no..traversal. A leading/in documentsrc/hrefis treated as web-style root-of-BaseFS(matching how<base href="/">works in browsers) instead of an absolute filesystem path. WithBaseFSnil, every local-asset reference fails — the document is expected to inline its assets viadata:URIs. The C ABI signature is unchanged:folio_document_add_html_with_optionsstill takes abasePathC string and wraps it asos.DirFS(basePath)internally (#85)@font-faceURLs resolve relative to their containing stylesheet — a linked<link rel="stylesheet" href="css/site.css">containing@font-face { src: url(../fonts/Inter.ttf); }now resolves tofonts/Inter.ttffrom theBaseFSroot, not from the document root. HTTP-origin stylesheets resolve relative URLs as HTTP, FS-origin stylesheets resolve them throughBaseFS. Inline<style>blocks continue to resolve relative URLs fromBaseFSroot. Documents that previously relied on the root-anchored behavior need to use root-relative/fonts/Inter.ttfor move the@font-facerule into an inline<style>layout.UnitValueis no longer comparable with==— the struct now holds a func field for the newUnitCalcvariant, so equality comparisons no longer compile. No in-tree consumer comparedUnitValuefor equality; out-of-tree consumers that did will see a compile error and need to switch to comparing the variant tag and resolved value separately (#236)layout.BackgroundImage.Positionis now[2]layout.ResolvableLength— pre-change the field was[2]float64, eagerly resolved at parse time, which dropped plain lengths and mixed-unit calc onbackground-positionto 0 because the background box dimensions are only known at draw time. Post-change the field carries aResolvableLengthper axis whoseResolve(container, fontSize float64) float64method evaluates at draw time. A new siblingFontSize float64field on the struct feeds em / rem resolution. Out-of-tree code that constructsBackgroundImagedirectly must implement the one-methodResolvableLengthinterface for each axis (see MIGRATING) (#266, #319)
html.Options.Logger(*slog.Logger) — receives warn-level events when a local or remote asset fails to load: missing fonts in@font-face, unreadable linked stylesheets, image fetch errors that fall back to alt text. Defaults to nil (silent). Pair withslog.NewTextHandler(os.Stderr, nil)during development to surface what would otherwise be swallowed (#85)html.Options.Client(*http.Client) — HTTP client used for remote fetches (<img>, linked stylesheets,@font-face url(http://...)). Lets callers configure timeouts, transport, and proxies; mock the network in tests viahttptest.NewServer; or share connection pools with surrounding code. Defaults tohttp.DefaultClient(#85)tmpl.RenderFile/tmpl.RenderFileToauto-populateBaseFS— when the caller'sOptions.BaseFSis nil, the helpers default it toos.DirFS(filepath.Dir(templatePath))so that a template referencing<img src="logo.png">next to itself resolves without extra wiringhtml.Options.StrictAssets(bool) — promotes asset-load failures from warn-and-continue to returned errors. When true,ConvertandConvertFullcollect every failed@font-face url(),<img>,background-image: url(), linked stylesheet, SVG load, andFallbackFontPath, then return them joined viaerrors.Joinat the end of the conversion. The partial result (the elements that did render) is returned alongside the error so callers can inspect both. Errors are returned in document order — linked stylesheets,@font-facerules, then asset references in tree-walk order — and are byte-stable across runs given byte-identical input. Defaults to false — production keeps the warn-and-continue behavior. Use it in development and CI to surface broken asset paths in the local feedback loop instead of letting them silently degrade the output (#232)font.ParseFontForLanguage(data, lang string) (Face, error)— TTC face selection by BCP-47 language tag. Pan-CJK font collections (NotoSansCJK, Source Han Sans, Hiragino, PingFang, msgothic.ttc) ship with separate faces for Japanese, Korean, Simplified Chinese, and Traditional Chinese variants encoded in each face's NameID 1 FontFamily ("Noto Sans CJK JP", "Noto Sans CJK SC", etc.). The new entry point picks the face whose family name best matches the requested language:"zh-CN"/"zh-Hans"→ SC,"zh-TW"/"zh-Hant"→ TC,"ja"→ JP,"ko"→ KR. Emptylang(or any unrecognised hint) falls back to face 0, soParseFontis now a thin wrapper aroundParseFontForLanguage(data, "")and back-compat is preserved. Pinned by tests against synthetic 4-face TTCs and a real-system round-trip.examples/cjkupdated to prefer SC-specific standalone fonts (NotoSansSC-Regular.otf,simsun.ttc) ahead of the pan-CJK TTC bundles whose default face is JPhtml.ErrURLPolicyDenied— sentinel returned when aURLPolicycallback rejects a fetch. The denial wraps it withfmt.Errorf("%w: %w", ErrURLPolicyDenied, policyErr)so callers can branch onerrors.Is(err, html.ErrURLPolicyDenied)to distinguish "I told it to block" from "the asset broke." UnderStrictAssets,ErrURLPolicyDeniedis logged throughOptions.Loggerbut excluded from the joined return error — the caller already received the signal they wired the policy to produce (#232)font.Fallbackandlayout.NewParagraphFallback— Phase 1 of #192.font.Fallbackwraps an ordered list of*EmbeddedFontand dispatches per script run viaPickFace(firstBaseRune).NewParagraphFallback(text, fb, size)walksSegmentByScript, picks one face per script run, coalesces same-face neighbours, and emits NTextRuns. Lets a single string mixing Latin / Hebrew / Arabic / CJK render without the caller pre-splitting into per-font runs. Within-script coverage gaps still render as.notdef; per-cluster intra-script dispatch is Phase 2. Pointer identity is preserved through the fallback chain so subsetting and the page-level font-resource map keep deduping by pointer — one Type0 dict per face per document regardless of how many paragraphs share the fallback (#225)layout.Paragraph.MeasureLines(maxWidth) intandMeasureHeight(maxWidth) float64— wrapped line count and rendered height (sum of line heights, excludingSpaceBefore/SpaceAfterso callers compose with their own pagination math) at a given width. Both are thin wrappers over the existingLayout()path, so the values always agree with what the renderer will produce. Unblocks clamp/truncate decisions before render (#238)layout.Paragraph.SplitAfterLine(n int, maxWidth float64) (head, tail *Paragraph)— splits a paragraph after the firstnrendered lines at a given width. Both returned paragraphs are clones; the receiver is unchanged.n <= 0returns(nil, full),n >= total linesreturns(full, nil). Spacing ownership: the half that owns the original boundary keeps that spacing;FirstLineIndentis dropped from both halves so re-laying re-applies it correctly. Designed for first-N-lines-plus-appendix flows. ThecloneWithWordsaudit chain (#238/#241/#246/#250) shipped first so the resulting clones reproduce correctly across all script families, inline elements, links, and forced line breaks (#253)layout.Anchorelement + auto-registered PDF named destinations —<a href="#anchor">now emits a real PDF link annotation with an internal/Destrather than a/URIaction containing the literal"#anchor". The HTML walker prepends a zero-heightlayout.Anchorfor any element with anidattribute; the renderer surfaces these ontoPageResult.Anchors, and the document layer registers them as PDF named destinations automatically — no callerdoc.AddNamedDest()calls required.TextRun/Wordlink plumbing splits intoLinkURIvsLinkDestNameso a single line can carry both kinds of links (#223)layout.UnitCalcvariant +layout.CalcUnit(fn)constructor — aUnitValuewhose value is computed lazily at layout time via aCalc func(available float64) float64closure. Introduced so CSS lengths that depend on a percentage in a calc expression (e.g.flex: 0 0 calc(50% - 8px)) can resolve against the actual layout area at render time rather than against the pre-margin container width. Pure absolute lengths still resolve eagerly viaPt— the new variant only kicks in when the parser detects percentage participation throughcssLength.dependsOnPercent()/calcExpr.dependsOnPercent()(#236)document.Info.Language(BCP-47 / RFC 3066) — writes catalog/Langper ISO 32000-2 §14.9.2 anddc:languagein XMP. Required for any PDF/A Level A variant per ISO 19005-2/3 §6.7.2 (#214)- PDF/A-3a and PDF/A-4 family —
PdfA3a(ISO 19005-3:2012, Level A) for accessibility-conformance with attachments;PdfA4(base, PDF 2.0),PdfA4F(embedded files, Factur-X successor),PdfA4E(engineering, replaces PDF/E-1) for ISO 19005-4:2020.pdfaid:rev = "2020"emitted for part 4;pdfaid:conformanceomitted for plain A-4 and present (F/E) for A-4f / A-4e. Embedded files now permitted in A-3 (a/b) and A-4 (f/e); thepdfafextension schema is declared for every level allowing associated files (#214) - C ABI:
folio_document_set_language(doc, lang)+FOLIO_PDFA_3A/FOLIO_PDFA_4/FOLIO_PDFA_4F/FOLIO_PDFA_4E— companions to the new Go-side PDF/A surface. ExistingFOLIO_PDFA_*numeric values are preserved (#214) - C ABI:
folio_paragraph_measure_lines/folio_paragraph_measure_height/folio_paragraph_split_after_line— surface the newParagraphmeasurement and split helpers (#238, #253) to non-Go callers.measure_linesreturnsint32_t(-1on bad handle);measure_heightreturnsdouble(-1.0on bad handle);split_after_linereturns two paragraph handles via out-pointers, either of which may be0for the no-op halves (n <= 0→(0, full),n >= total→(full, 0)). Receiver unchanged; caller frees non-zero halves (#283) - C ABI:
folio_font_parse_for_language(data, length, lang)— wrapsfont.ParseFontForLanguageso C consumers can pick the right TTC face by BCP-47 tag ("zh-CN","ja", etc.) for pan-CJK font collections. NULLlangfalls back to face 0, matchingfolio_font_parse_ttfsemantics. Thefont.Fallback/layout.NewParagraphFallbacksurface (#225) is intentionally NOT exposed in v0.8.0 — per its PR description, C ABI exposure is Phase 2 work tied to promoting*EmbeddedFontto an interface (#283) - C ABI exports grow from 388 (v0.7.1) to 393 (+5) —
scripts/audit-cabi.shreports Go//exportdirectives,export/folio.hdeclarations, and built dylib symbols all in sync at 393. The header is updated in lockstep with each addition - WASM CLI exposes
pdfa3a/pdfa4/pdfa4f/pdfa4e— and fills in profile keys the Go API already supported but the playground did not (pdfa1a,pdfa2u,pdfa2a,pdfa3b) (#214) - CSS
:emptypseudo-class (Selectors L3 §9) — matches an element with no element children and no non-empty text children. Comments do not count as children, per spec (#215) - CSS
::placeholderpseudo-element (Pseudo-Elements L4) — applies to<input>and<textarea>carrying aplaceholderattribute, only when the field has novalue(input) or body text (textarea). Supported sub-properties:color,font-style,font-weight,font-size. Numeric font-weight values route throughparseFontWeightsofont-weight: 700/bolderresolve to the bold font variant (#215) - Indic shaping for eight Brahmic scripts — Bengali, Gujarati, Gurmukhi, Kannada, Malayalam, Oriya, Tamil, Telugu. Generalises the previously Devanagari-only five-phase shaper around a per-script
indicScriptConfig. Existing Devanagari behaviour and tests are preserved.ScriptOf/Scriptis extended to recognise the six new scripts (Bengali and Tamil were already mapped). Paragraph layout dispatches viaindicConfigFor(Script). Gurmukhi and Tamil userephPosNoneso a leading Ra + virama + consonant in those scripts is left as a plain halant-joined cluster (#216) - GPOS LookupType 6 (Mark-to-Mark Positioning) — combining marks anchor to the previously placed mark instead of stacking on the same base anchor, unblocking Arabic shadda + harakat stacks, Hebrew dagesh + niqqud, Vietnamese tone-on-diacritic vowels (
ế), and stacked IPA tone marks. NewMarkMarkOffset(mark1GID, mark2GID, feature)accessor; parameter order follows the spec's mark1/mark2 (attaching/receiving) convention (#213) - GPOS LookupType 3 (Cursive Attachment) — parses CursivePosFormat1 entry/exit anchors and applies them at draw time on shaper-produced GID streams. Each glyph emits its own
Tj; the second through Nth are bracketed byTdshifts that align the entry anchor onto the previous exit. NewCursiveOffset(prevGID, currGID, feature)accessor returningprev.Exit - curr.Entry. LTR-only: RTL handling is gated on the LookupFlagRIGHT_TO_LEFTbit, which is parsed and stored on theCursiveRecordbut not yet honoured by the draw path (#218) - GPOS LookupType 5 (Mark-to-Ligature) — parses MarkLigPosFormat1 with per-(component, mark-class) anchor grids and a parallel
Presentgrid. NewMarkLigatureOffset(ligGID, componentIdx, markGID, feature)accessor. Draw integration tries Type 5 before falling back to Type 4 mark-to-base when the cluster base is a ligature carrier. Component attribution uses a stop-gap heuristic (first Extend/ZWJ mark to component 0, subsequent marks to the last component) because the layout pipeline does not yet plumb cluster→component IDs from the shaper; ligatures with three or more components fall back to Type 4 to avoid silent middle-component misplacement.TODO(#218)left inlayout/draw.gofor the follow-up (#218) - CSS
counter(page)andcounter(pages)resolve in body-flow::before/::aftercontent— previously these only worked inside@pagemargin boxes. Two-pass emission: pagination is decided once, then a second pass draws each page with the final page total available, socounter(pages)is substituted directly during draw rather than via a post-stream byte replace. Layout-time width reservation for counter placeholders keeps line breaks stable across digit-count transitions (page 9 → 10, 99 → 100).target-counter(url(#anchor), page)for cross-references remains tracked as #222 (#221) - CSS GCPM bookmark properties for PDF outlines —
bookmark-labelresolvescontent(),attr(NAME), and literal strings during HTML conversion (missing attributes fall back to the element's text content);bookmark-level: noneexcludes an element from the outline (cover pages, suppressed headings);bookmark-state: closedmarks the subtree collapsed in viewers via the negative/Countsign convention (ISO 32000 §12.3.3). Non-heading elements with an explicitbookmark-leveljoin the outline through a newBookmarkAnchorwrapper that decorates the firstPlacedBlockwithout retagging the structure tree (a<figure>stays a Figure in the structure tree). Level-skip rule documented: an entry whose level skips a parent (h1 → h3) nests under the nearest preceding lower-level entry (#224) - CSS shorthand parsers preserve
calc()/min()/max()/clamp()values — every shorthand below previously usedstrings.Fieldson values that may legally contain functional values, which split on the function's internal whitespace and silently dropped the declaration. A new paren-awaresplitTopLevelFields(sibling ofsplitTopLevelCommas) replaces it across:flex(#236),margin/padding(#237),font(#240),border(#242),background-size(#244),@page size(#247, plus the page-local length parser is now calc-aware viaparseLengthPt),gap/grid-gap(#249),border-radius(#252),box-shadow(#254),transform-origin(#257),border-spacing(#258),columns(#259), andcolumn-rule(#261).borderandcolumn-ruleadditionally regainrgb()/rgba()/hsl()color parsing (which had been shredded by the same tokenizer). Forflexonly: percentages inside calc resolve lazily through the newlayout.UnitCalcvariant against the actual layout area at render time (#236) <html lang>propagated to@font-faceTTC face selection —html.ConvertFullnow reads the document root'slangattribute and routes it throughfont.ParseFontForLanguage, so<html lang="zh-CN">+@font-face url('NotoSansCJK-Regular.ttc')picks the SC face instead of the JP face-0 default. The same hint also flows toOptions.FallbackFontPathand the built-in system-font candidate list (loaded lazily, so the lang is available by the time the loader runs): a doc declaringlang="zh-CN"whose<p>falls back to the CJK system font gets the SC face there too. The extracted tag is also exposed onConvertResult.Metadata.Languagefor callers that want to feed it into the PDF catalog's/Langentry or other downstream consumers. Per-elementlangoverrides (<p lang="ja">) are NOT yet honoured —@font-faceis loaded once at converter setup, so element-level overrides would need either eager multi-face loading or a shape-time face selector. Deferred as Phase 2 of #280 (#280)font.LoadFontForLanguage(path, lang string) (Face, error)— disk sibling toParseFontForLanguage. The same BCP-47 face-selection rules apply; emptylangfalls through to face-0 / single-face semantics identical toLoadFont- Numeric
font-weightladder + nearest-weight@font-facematching (CSS Fonts L4 §3.1 + §5.2) — pre-fixparseFontWeightreturned the binary string"normal"/"bold"and the@font-faceresolver looked up faces by that string. Documents declaring four Inter weights at 400/500/600/700 and writingfont-weight: 600silently picked Inter-Regular instead of Inter-SemiBold; same for 500 (Medium) collapsing to Regular. Post-fixcomputedStyle.FontWeightisintover the 100..900 ladder,parseFontWeightreturns the spec ladder (withbolder/lighterresolving relative to the inherited weight), andresolveFontPairdoes §5.2 nearest-weight matching: exact match wins, then walks the ladder per the spec's per-window algorithm ([400, 500]ascending toward 500 first,< 400descending then ascending,> 500ascending then descending). Plus an italic-vs-normal style fallback so an author asking for italic Inter at a registered weight gets the regular-style face instead of falling through to Helvetica.computedStyle.FontWeightis package-internal so this is not a public API break (#286, #287) - CSS
text-align: start | enddirection-relative keywords (CSS Text L4 §7.1) — pre-fixparseTextAlignreturned(AlignLeft, ok=false)for both, so the registry's Apply silently dropped the declaration:<p style="direction: rtl; text-align: start">שלום</p>rendered left-aligned despite the author asking for the RTL start edge. Post-fix the parser preserves the keyword oncomputedStyle.TextAlignKeywordand a newresolveTextAlign(style)helper late-binds the resolution againststyle.Directionat consumer time (so source-order interactions liketext-align: start; direction: rtlresolve correctly regardless of declaration ordering). Same treatment fortext-align-lastviaresolveTextAlignLast. New publiclayout.Paragraph.Align()getter exposed for test inspection (#288) - CSS
text-decoration-line: overline(CSS Text Decoration L4 §3.1) — pre-fixlayout.TextDecorationdefinedUnderline (1<<0)andStrikethrough (1<<1)only; the third spec line type was silently dropped at parse time.<p style="text-decoration: overline">x</p>rendered plain. Post-fixDecorationOverline = 1 << 2joins the bitset,parseTextDecorationORs it in for theoverlinekeyword (combinations liketext-decoration: underline overline line-throughproduce all three flags), anddrawDecorationsrenders the stroke atbaselineY + ascent * 0.95withtext-decoration-styledispatch (solid / double / wavy / dashed / dotted) honoured. Thedoublesecondary stroke offsets DOWNWARD (toward text) so both lines stay inside the line box even atline-height: 1. Adjacent runs with mismatched decoration sets correctly mask the trailing-space extension to only their shared subset, fixing a latent bug surfaced by the new flag (#289) - CSS border styles
groove | ridge | inset | outset(CSS Backgrounds L3 §4.1) — pre-fix the parser stored the keyword string butbuildBorder's switch matched 3 keywords + default-solid, so<div style="border: 4px ridge silver">parsed and stored "ridge" but rendered as a flat solid line. Post-fix the parser whitelist accepts the four 3D bevel styles andbuildBorderForSide(side, ...)dispatches per-side via newbeveledColor/lightenColor/darkenColorhelpers (fixed 30% sRGB shift; CMYK adjusts K). Per CSS §4.1: groove/inset → top+left dark, bottom+right light (sunken); ridge/outset → opposite (raised). Folio renders a single solid stroke per side with side-modulated color rather than the strict spec's two-half-width split bevel — visually indistinguishable for thin borders, less pronounced bevel for thick ones (#290) - CSS
max-width/max-height/min-width/min-heighton<img>(CSS 2.1 §10.7) — pre-fix the CSS parser stored these properties oncomputedStylebuthtml/converter_image.go::convertImagenever threaded them tolayout.ImageElement, andImageElementhad no fields to receive them. So a 2000×500 source logo withmax-width: 180pxin a 540pt-wide page bled the entire header instead of clamping. Post-fixlayout.ImageElementgainscssMaxWidth/cssMaxHeight/cssMinWidth/cssMinHeightfields plusSetMaxSize(maxW, maxH)andSetMinSize(minW, minH)setters;resolveSizeapplies the clamps in spec order (max first, then min, with min winning over max per the conflict rule), aspect-preserving on each axis. Theobject-fitbranch's early-return path now applies the clamps before fitting, so<img width=200 height=200 style="object-fit: cover; max-width: 50px">correctly produces a 50×50 box with cover-fit content. The container width remains the absolute outer bound for replaced elements per §10.4 — amin-heightthat would push the image past the canvas is silently dropped to keep the image inside its containing block, matching browser behaviour. Percentagemax-width: 50%still resolves to 0pt because the converter's CSS-to-points conversion passesrelativeTo=0; pre-existing limitation tracked separately (#291, #292) - CFF v1 subsetter for CID-keyed OTF fonts — Adobe-Japan1 / Adobe-GB1 / Adobe-Korea1 / Adobe-CNS1 fonts embedded through the new CFF read path (#260) previously shipped the full CFF Top DICT, CharStrings INDEX, and FDArray/FDSelect into the PDF, producing multi-megabyte output for documents that only used a handful of glyphs. The new subsetter walks the used-GID set from the layout pass, rewrites CharStrings, prunes FDArray entries, and emits a minimal CID-keyed CFF stream through
/FontFile3+/CIDFontType0. On the test corpus a 1-page document quoting a single CJK ideograph from Source Han Sans dropped from 11.1 MB to 173 KB end-to-end. Pure-Type 1 (non-CID) CFF fonts continue to ship un-subset because the layout pipeline emits CIDs unconditionally; gating on CID-keyed-v1 keeps the subsetter conservative. Sharing of embedded and standard fonts across pages also lands in the same change, closing the duplicate-FontDescriptor accounting that #295 originally surfaced (#295, #299, #300) background-positionand gradient color stops accept percent-onlycalc()/min()/max()/clamp()— pre-change the parsers only recognised bare percent / length leaves; expressions likebackground-position: calc(50% + 10px) 0orlinear-gradient(red, blue calc(50% + 1px))silently fell back to 0. Post-changeparseBgPositionand the gradient-stop parser accept any calc tree whose participation includes percent (per the rules in #236). The bg-position half then resolves lazily via the newlayout.ResolvableLengthmachinery added in #319; gradient-stop calc remains eagerly resolved against the box width at parse time. Followup tests and asplitTopLevelFieldsdoc gap close out in #314, and aparseCalcExprdepth-reset fix between operator scan passes lands in #315 (#266, #312, #314, #315)
- Microsoft Symbol fonts (Wingdings, Symbol, dingbat) now load and render correctly — the cmap parser accepts the (platformID=3, encodingID=0) Symbol-encoding fallback when no Unicode subtable is present (matching HarfBuzz / go-text / pdf.js), and additionally mirrors PUA-keyed entries (0xF020..0xF0FF) to the corresponding ASCII codepoints (0x0020..0x00FF). Without the mirror these fonts loaded but
<p>A</p>rendered as.notdefbecause the html-shaping path sends U+0041, not U+F041; HarfBuzz applies the same alias automatically. Existing ASCII-range entries in the source cmap are NOT overwritten — explicit values from the font win over synthesized aliases (#262) fontpackage no longer depends ongolang.org/x/image/font/sfntfor metric reads —head,hhea,maxp,hmtx,OS/2,name, andcmapare now parsed directly from raw font bytes via new filesfont/{head,hhea,maxp,hmtx,os2,name,cmap}.go. Closes #248 (sfnt's hardcodedmaxCmapSegments = 20000blocked Microsoft YaHei, Noto Sans CJK, STHeiti) and fixes #227 (the CJK rendering bug those fonts caused) without the architectural debt of the closed PR #251 (recovery-path heuristic that substring-matched sfnt's unexported error message).Ascent/Descentfollow the OpenType USE_TYPO_METRICS convention (OS/2 fsSelection bit 7): when set, the foundry has explicitly requested the typo metrics, so we usesTypoAscender/sTypoDescender; otherwise we use the hhea ascender/descender. This preserves byte-identical FontDescriptor/Ascentand/Descentoutput for every font where USE_TYPO_METRICS is unset (the majority — and every font sfnt v0.39.0 produced metrics for, since sfnt unconditionally used hhea). Fonts that opt into the bit see their values shift to match the foundry's intent. CFF/OTF outline support is unchanged: Folio's subset code already only handled TrueTypeglyf/loca, so CFF fonts continue to load and embed (raw CFF bytes opaque) but cannot be subset (#260)htmlasset resolution centralized behind a singleresolveLocalAssetcontract —<img src>, inline SVG,<link rel="stylesheet" href>,@font-face url(),background-image: url(), andFallbackFontPathpreviously each carried their own routing logic that disagreed on absolute paths, root-anchored paths, and HTTP origins. They now route through one method documented inARCHITECTURE.md"Asset resolution". Behavior-preserving for every existing test (go test ./...green); the visible side-effects are that<img src="https://...svg">and inline-SVG<img>URLs now flow throughURLPolicyenforcement uniformly, and@font-face url('/abs/system/font.ttf')succeeds withBaseFS: nil(closes the workaround originally proposed in PR #228).FallbackFontPathretains a documented programmatic-only carve-out: an absolute path always bypassesBaseFSto the OS, and a relative path that missesBaseFSretries against the OS — neither extends to document-supplied references because the trust boundary is different (#229)- HTML CSS parser refactored into a declarative property registry — replaces the ~700-line
applyPropertyswitch inhtml/converter_style.gowith onecssPropertyentry per CSS property (138 entries total) carrying its ownApplyclosure plus documentation fields.applyPropertyshrinks to 7 lines: pre-dispatch logic preserved verbatim (custom-property--storage, thenvar()resolution, then registry dispatch). The same registry is the source of truth fordocs/CSS_SUPPORT.md, auto-generated bygo generate ./html/....applyTagDefaults(browser HTML element defaults) is untouched. No public API change; behaviour-preserving across the full suite (#271) - Error message prefixes standardized to
<pkg>:/<pkg>: <subdomain>:/<pkg>.<Func>:— top-level errors get<pkg>:, sub-feature errors insidefont/andreader/get<pkg>: <subdomain>:, and panics naming a public function get<pkg>.<Func>:. Adds package prefixes to ~130 previously unprefixed internal helper errors. Renames the message ofhtml.ErrURLPolicyDeniedfrom"folio/html: ..."to"html: ..."; the exportedvaridentity is unchanged, soerrors.Is(err, html.ErrURLPolicyDenied)still works. Callers that string-comparederr.Error()against the old verbatim message — including thefolio/html:prefix — must update their assertions (#313) computedStylemargin/padding storage migrates to lazy*cssLengthsiblings — the eight legacyfloat64fields (MarginTop/Right/Bottom/Left,PaddingTop/Right/Bottom/Left) are removed fromcomputedStyle; every read goes throughMarginTopAt(width)/PaddingTopAt(width)helpers backed by sibling*cssLengthfields. Tag-default setters write&cssLength{Value: N, Unit: "pt"}so heading / paragraph margins are no longer baked-in floats.computedStyleis package-internal so this is not a public API break; thePageConfig.MarginTopfield used bypage.gois a separate type and stays asfloat64. ATestHeadingDefaultMarginsSurviveMigrationregression pins the documented per-heading top/bottom margin values (16.08, 14.94, 14.04, 16.02, 16.60, 18.62) (#269, #309, #310, #311)
- SVG
preserveAspectRatioslice viewport clip — when an SVG is drawn withslicemeet-or-slice, the renderer now emits a PDF clip path on the target rectangle before the viewport transform. Previously the uniform scale was applied correctly but content outside the target rectangle leaked onto the page. Callers that already clipped externally will continue to work (#196) - TrueType Collection (
.ttc) fonts now load —font.ParseFontandfont.LoadFontadvertised TTC support but routed the bytes tosfnt.Parse, which rejects collections withinvalid single font (data is a font collection). The dispatch now extracts face 0 from the collection into a standalone single-font TTF (table directory rewritten to point at the new offsets) and parses that. Selection of face 0 matches browser behavior forurl()references without a#fragment. Very large CJK collections may still hitgolang.org/x/image/font/sfnt's hardcodedmaxCmapSegmentslimit — that is an upstream parser limit, orthogonal to TTC dispatch (#227) font.ParseFontno longer falsely advertises PostScript Type 1 (typ1) support — the magic was listed in the dispatch alongside TrueType variants, butParseTTFroutes it tosfnt.Parsewhich rejects Type 1 with a confusingsfnt: invalid fonterror. The entry is gone; callers feedingtyp1bytes now receive a clearErrUnknownFormat. Type 1 has been deprecated since PDF 2.0 and no modern font foundry ships in this format, so re-introducing support would require a full Type 1 parser, not just adding the magic back. NewTestParseFontDispatchSurfaceaudits every magic the dispatch claims to support to prevent the same false-advertisement shape recurring (#230)- GPOS Type 5 mark-to-ligature now falls back to Type 4 mark-to-base for ligatures with three or more components — without per-cluster component attribution, middle-component anchors silently misplaced marks. The bail-out preserves correct rendering for the common two-component cases (Arabic lam-alef, Latin "fi" / "ffl") while avoiding the misplacement on longer ligatures (#220)
- Inline elements and Arabic
OriginalTextsurvive paragraph page splits —cloneWithWords'swordToRunhelper was droppingWord.InlineBlock(inline image / SVG / Div) so any inline element falling into the overflow half of a paragraph was silently lost, and was copying post-shapeWord.Textinstead of pre-shapeWord.OriginalTextso re-laying skipped repopulatingOriginalTextand broke/ActualTextmarked-content recovery (ISO 32000-2 §14.9.4) for Arabic.wordToRunnow copiesInlineBlock→InlineElementand usesOriginalText(when set) asText;cloneWithWords'ssameRunguard rejects merges across inline elements (#241) font:shorthand handles a/insidecalc()correctly — the size/line-height detector usedstrings.IndexByte(sizeStr, '/')which found any/including ones insidecalc(2em / 2)and split mid-expression. Replaced with a paren-awareindexByteAtTopLevelhelper. Same fix surface as the calc-tokenization series (#270)column-rulewidth zeroes whencolumn-rule-styleisnoneorhidden— matching CSS Multi-column Layout L1 (same rule asborder-style: none).parseBorderFullalready implemented this;parseColumnRuledid not, socolumn-rule: 4px none redreturned width = 3pt instead of 0pt.hiddenis now also accepted as a style keyword (parity withparseBorderFull) (#270)transform()parsescalc()/min()/max()arguments correctly — two compounding paren-blind bugs inparseTransform. The outer function-call extractor usedstrings.Index(val[parenIdx:], ")")and so truncatedtranslate(calc(50% - 10px), 0)mid-calc; the inner argument splitter usedstrings.ReplaceAll(args, ",", " ")which destructively flattened nested commas (min(10px, 20px)→min(10px 20px)). The outer is now depth-tracking; the inner usessplitTopLevelCommasfirst with asplitTopLevelFieldsfallback for the legacy SVG space-separated form. Calc insiderotate/scale/skewis fixed in a follow-up via #284 (#272)parseAngleandparseNumericValunderstandcalc()/min()/max()/clamp()— closes the per-arg gap left after #272 surfaced calc to transform args.transform: rotate(calc(45deg + 45deg))now resolves to 90deg (was 0),scale(calc(0.5 + 0.5))to 1.0 (was 0, a degenerate zero-scale transform),skew(calc(10deg * 2))to 20deg.parseAnglegains an angle-native calc evaluator (resolveAngleCalc) that mirrorsparseCalcExpr's grammar but reads each leaf as an angle in degrees (or a dimensionless multiplier).parseNumericValhandles min/max/clamp directly and delegates calc to the existing dimensionless-leaf path.parseAngleLeaf's suffix-detection order is fixed in passing — pre-fix100gradmatched theradsuffix beforegradand resolved to 0; the new ordering checks turn/grad/rad/deg (#274, #284)parseLineHeightdistinguishes dimensionless calc from length-form calc —font: 12px/calc(1.2 * 1.5) sansnow resolves to multiplier 1.8 as written. Pre-fix the dimensionless calc result (1.8) was passed throughpts := l.toPoints(fontSize, fontSize)and then divided by fontSize=9pt, yielding 0.2 — a 9× compression of line spacing. New helperscssLength.isDimensionless()andcalcExpr.isDimensionless()walk the calc tree and report true iff every leaf is a bare number;parseLineHeightshort-circuits via that check before the existing length-form path. The length-form calc (calc(1.5em),calc(1em + 4px)) is unchanged (#275, #284)margin: <calc> autocorrectly flags both auto-sides — the auxiliary auto-flag scan inside themargincase ofapplyPropertystill usedstrings.Fieldsafter #237 migratedparseMarginShorthanditself. The two tokenizers diverged on functional values, somargin: calc(10px + 20px) autocorrectly resolved top/bottom = 22.5pt and left/right = auto, but onlyMarginLeftAutowas flagged —MarginRightAutowas missed. Both tokenizers now usesplitTopLevelFields(#263)- Paragraph background stays pinned to the content box under
text-align: center/right— pre-fix the background rectangle was drawn from the line's measured start X, so a centred or right-aligned paragraph withbackground-colorpainted a stripe under each centred line rather than under the paragraph's content box. The renderer now anchors the background to the content box bounds regardless of per-line alignment, matching browser behaviour (#293) word-break: break-allhonoured in the second paragraph measurement pass —measureWordswas consulted twice during paragraph layout but only the first pass observedword-break: break-all, so a paragraph that needed re-measurement (page splits, container resizes, line-height re-evaluation) regressed to the default no-break behaviour mid-layout. The second pass now reads the same word-break state as the first, so long-word breaks remain stable across re-measurement boundaries (#294)breakLongWordspreservesOriginalTextand shaped GIDs across chunk boundaries — when a single word overflowed the available width and was split into multiple chunks, the helper kept the raw substring but dropped the pre-shapeOriginalTextand the post-shapeGIDsslice. The downstream/ActualTextmarked-content emission (ISO 32000-2 §14.9.4) then lost the recovery hint for Arabic and complex-script words split across line boundaries, and the draw path re-shaped from the chunk's runes instead of reusing the already-shaped GIDs. Both fields are now propagated through every chunk, so accessibility recovery and shape-cache reuse survive the split (#255, #307)
The four direct-field accesses deprecated in v0.7.0 remain fully functional in v0.8.0 and continue to work identically to their accessor-method equivalents. Removal target is v1.0, when the stable public API will be declared. Migrate before then to avoid a compile-error churn at the v1.0 boundary.
core.PdfDictionary.Entriesdirect field access — useAll,Get,Set,Remove. Direct slice mutation bypasses the lazy key index and can desync the dictionary's lookup table.core.PdfArray.Elementsdirect field access — useAll,At,Len,Add,Set,RemoveAt,Replace.core.PdfIndirectReference.ObjectNumber— useNumfor reads andSetNumfor writes.core.PdfIndirectReference.GenerationNumber— useGen.
core.RevisionRC4128 (RC4 encryption) remains formally discouraged for new documents — RC4 is cryptographically broken — but is NOT scheduled for removal. It is kept indefinitely to support reading and writing legacy RC4-encrypted PDFs. Use RevisionAES256 for new encrypted documents.
examples/cjkis now CI-tested end-to-end — addsexamples/cjk/main_test.gothat builds the example's HTML against a synthetic TTC fixture (constructed from any system TTF at test time), runshtml.ConvertFullanddocument.Save, and asserts the resulting bytes start with%PDF-and embed the requested font's PostScript name (matching the+<name>subset prefix). Issue #227 — broken TTC dispatch on Windows / Linux — was the kind of regression this would have caught: the example documented workingmsyh.ttcandNotoSansCJK-Regular.ttcpaths but neither was ever exercised end-to-end because nothing in CI compiled or ran the example. A second test (TestCJKExampleFindCJKFontReturnsExistingPath) verifies the example'sfindCJKFontcandidate list does not drift away from on-disk reality. Phase 1 of #231; subsequent PRs roll the same pattern out to other examples (#231)- Hyphenation contract guard for
cloneWithWords— locks in the dormant-but-correct behaviour:hyphenateWordproduces a part-and-rest pair wherepart.Textends in-andSpaceAfter=0, andcloneWithWordsjoins same-style words. A naive join would emit"linguis-tic"instead of the original"linguistic". Today the path is unreachable becausewrapWords(whichPlanLayoutuses) does not callhyphenateWord— onlyParagraph.Layoutdoes. The new test exercises the page-split path with a long alpha word eligible for hyphenation under both Liang-Knuth and character-boundary fallback paths and asserts no-leaks into the overflow runs. If a future change wireshyphenateWordintowrapWords, this fails immediately and signals thatcloneWithWordsneeds hyphen-aware join logic before that change can land. Closes the audit chain that produced #241 / #246 (#250) - Phase 2 of
#231— end-to-end CI tests for the remainingexamples/directory — extends the same pattern Phase 1 of #231 introduced forexamples/cjkto the rest of the example tree, so every documented example now compiles and runs in CI rather than rotting silently. Newmain_test.gofiles build the example's PDF and assert%PDF-prefix + key invariants per example:examples/html-to-pdfreads back throughpdf.Readerand checks rendered text plus image presence (with cached bytes to amortize the build cost across assertions);examples/reportbuilds with reader-based content assertions;examples/{hello, links, template}cover the small-surface examples in one batch;examples/{merge, import-page, redact}cover the mid-complexity reader-write round-trip examples in one batch (#231, #301, #302, #303, #304) - Synthetic CJK font fixture for integration tests — replaces the previous "skip if NotoSansCJK is not on the test box" gate with a deterministic in-memory CJK font built at test setup time, so the CJK shaping and TTC dispatch tests now run on every CI machine regardless of installed fonts. The fixture builds a minimal CFF + cmap pair sufficient for the parser and shaper to traverse end-to-end (#281, #305)
- Direct unit test for
parseLineHeightdimensionless / length-form calc paths — covers the helper introduced by #275 in isolation rather than only via thefont:shorthand integration test. Locks in thecssLength.isDimensionless()/calcExpr.isDimensionless()walk so a future change to the calc grammar can't regress the 9× compression bug that #275 originally fixed (#275, #306)
docs/CSS_SUPPORT.mdintroduced — auto-generated from the CSS property registry bygo generate ./html/.... Covers all 138 supported CSS properties grouped by category (Typography / Color / Backgrounds / BoxModel / Borders / Layout / Flexbox / Grid / MultiColumn / Tables / Pagination / Lists / Effects / PDF), with per-property aliases / accepted values / notes. Adds a value-form glossary, a box-alignment cross-context callout, a "Known unsupported features" table with concrete workarounds (oklch/color-mix→ precompute or use a CSS variable;line-clamp→layout.Paragraph.SplitAfterLine;filter/backdrop-filter→ pre-bake into images; etc.), and a contributor guide for adding a new property. Linked from the README.TestCSSDocsInSyncenforces drift detection between the registry and the on-disk file (#271, #273)docs/CSS_SUPPORT.mdextended with Selectors / At-rules / Functions — three hand-written sections covering the surface area the property registry doesn't reach. Selectors: 4 combinators, 5 simple selectors, 7 attribute operators, 13 pseudo-classes, 4 pseudo-elements, with non-supported sets called out in each subsection (interaction-state pseudo-classes, single-colon pseudo-element legacy forms, attribute case-sensitivity flags). At-rules:@font-face,@page+ selectors,@pagemargin boxes,@supports,@media print, plus the silently-ignored set (@medianon-print,@import,@keyframes,@counter-style,@namespace,@charset,@layer/@scope/@container/@property). Functions: ~30 entries across math (calc/min/max/clamp), color (rgb/rgba/hsl/hsla/cmyk), gradients, content/counters, transforms, andurl, with related issue numbers (#222, #265, #266, #274, #275) called out for known gaps. Three drift guards:TestAtRulesDocCoverageAST-grepsparseCSSfor@-prefixed string literals;TestFunctionsDocCoverageandTestSelectorsDocCoverageuse static expected lists with behavioural smoke checks against the parsers (#162, #282)
C ABI follow-up to v0.7.0. No Go-side behavior changes; every addition is in export/. C ABI exports grow from 372 to 388 (+16). The header export/folio.h is updated in lockstep — scripts/audit-cabi.sh reports Go and header in sync.
Handle-based builder so future toggles compose without renaming existing calls. The save and buffer entry points accept a zero handle as "use defaults", so callers that only want the optimizer for a single write do not need to allocate and free an options object.
folio_write_options_new/folio_write_options_freefolio_write_options_set_use_xref_stream— ISO 32000-1 §7.5.8folio_write_options_set_use_object_streams— §7.5.7folio_write_options_set_object_stream_capacityfolio_write_options_set_orphan_sweepfolio_write_options_set_clean_content_streams— §7.8folio_write_options_set_deduplicate_objectsfolio_write_options_set_recompress_streams— §7.4.4folio_document_save_with_optionsfolio_document_write_to_buffer_with_options
folio_document_set_actual_text— toggles the/Span /ActualTextemission for shaped Arabic words (ISO 32000-2 §14.9.4)folio_paragraph_set_direction—FOLIO_DIR_AUTO(0) /FOLIO_DIR_LTR(1) /FOLIO_DIR_RTL(2). Out-of-range values normalize to autofolio_list_set_directionfolio_table_set_directionfolio_columns_set_balanced
FOLIO_DIR_AUTO,FOLIO_DIR_LTR,FOLIO_DIR_RTLpreprocessor constants inexport/folio.hso C consumers do not encode magic numbers for the direction setters
export/testdata/test_cabi.c gains 21 new assertions covering: WriteOptions lifecycle, all seven setters (happy path and bad-handle rejection), zero-options default path, an end-to-end document write with every optimizer toggle on that confirms the output starts with %PDF-, folio_document_set_actual_text happy path plus bad-handle, direction setters on Paragraph / List / Table (happy path, normalization of out-of-range codes, bad handle), folio_columns_set_balanced happy path and bad-handle. Total test_cabi assertions: 390 (384 pre-existing + 6 new blocks).
Internal-only v0.7.0 additions remain unexported from the C ABI: core.PdfIndirectReference.SetNum and core.PdfStream.WillCompress (writer-internal); core.DeflateStreamData / core.InflateStreamData (callers can use any host-language zlib); core.PdfArray / PdfDictionary / PdfNumber / PdfBoolean / PdfString Go-style accessors; font.ParseGPOS / ParseGSUB / ParseKern and face.GSUB / GPOS / GIDToUnicode (font parser internals); font.CanEncodeWinAnsiRune, EmbeddedFont.EncodeGIDs / MeasureGIDs (shaper-internal); layout.ShapeArabic / ShapeArabicWithFont / ShapeDevanagari*, ScriptOf / SegmentByScript, GraphemeBreaks / NextGraphemeBreak / GraphemeCount, FindKashidaCandidates / InsertKashidas (run inside the layout pipeline); layout.GSUBProvider / GPOSProvider (Go interfaces); tmpl package (Go-specific templating). RTL and shaping for HTML-driven workflows continue to flow through folio_document_add_html unchanged.
No breaking API changes. Every new field, method, and package is additive; zero-value WriteOptions and existing constructors produce byte-identical output to v0.6.2. Several bug fixes change the visible output of affected documents — see Visual changes before regression-diffing PDFs.
- CSS multi-column fill order — children are distributed sequentially with height-balanced packing instead of round-robin by index. Matches CSS Multi-column Layout
column-fill: balance(the spec default). Documents usingcolumn-countwill reflow (#145) - Arabic text shaping — the layout engine uses the font's OpenType GSUB contextual substitutions (init/medi/fina/isol) when present and falls back to the legacy Arabic Presentation Forms-B substitutions only when the font lacks GSUB. v0.6.x rendered Arabic as disconnected isolated forms; v0.7.0 renders connected (#160)
- TrueType kerning — fonts whose
kerntable uses Apple-format v0 coverage (Arial and many others) previously returned zero kern for every pair due to a flipped coverage-byte decode. Pairs are now applied; spacing in affected documents tightens (#172) - CJK line-breaking — Japanese, Chinese, and Korean paragraphs break per JIS X 4051 kinsoku shori rules instead of arbitrary character boundaries (#157)
- Per-glyph font fallback — runes outside the primary font's coverage previously rendered as
.notdefboxes; the layout engine selects a fallback face per glyph cluster when one is configured - CSS
!importantcascade at the inline/stylesheet boundary is now honored (#137) /ActualTextmarkers around shaped Arabic words are emitted by default. Adds a small per-word byte cost and improves copy/paste fidelity in PDF readers. Opt out withDocument.SetActualText(false)
These remain fully functional in v0.7.0. Plan to migrate before v1.0, when the stable API will be declared and deprecated symbols removed.
core.PdfDictionary.Entriesdirect field access — useAll,Get,Set,Remove. Direct slice mutation bypasses the lazy key indexcore.PdfArray.Elementsdirect field access — useAll,At,Len,Add,Set,RemoveAt,Replacecore.PdfIndirectReference.ObjectNumber— useNumfor reads andSetNumfor writescore.PdfIndirectReference.GenerationNumber— useGen
core.RevisionRC4128— RC4 is cryptographically broken; useRevisionAES256for new documents. The constant remains supported for reading and writing legacy RC4-encrypted PDFs and is not scheduled for removal
- Right-to-left text — bidi paragraph layout per UAX #9 via
golang.org/x/text/unicode/bidi, character-level bidi splitting, RTL list support (markers on right, text indented from right), HTMLdirattribute and CSSdirectionproperty wiring (#37) - Arabic shaping — presentation-forms shaper with full GSUB pipeline for init/medi/fina/isol features, GSUB ligature substitutions (
rlig,liga), kashida (tatweel) justification,/ActualTextmarkers (default on) for round-trip-safe copy/paste of shaped words (#37, #160, #179, #180) - Devanagari shaping — Indic shaper for Hindi, Sanskrit, Marathi, and Nepali. Five-phase OpenType pipeline with reordering, half-form substitution, and conjunct formation (#186)
- CJK line-breaking — kinsoku shori rules per JIS X 4051 with leading and trailing prohibition sets (#157)
- Unicode infrastructure — UAX #29 grapheme clusters (
unicode/graphemepackage), UAX #24 script segmentation, cluster-awarefont.MeasureString(#170, #176, #183)
- GSUB LookupType 4 ligature substitutions (#171)
- GSUB LookupType 6 chaining contextual substitution with depth-bounded action recursion (#174, #184)
- GPOS LookupType 2 pair adjustment (Format 1 explicit pairs and Format 2 class-based pairs);
face.Kernconsults GPOS first and falls back to the legacykerntable (#175) - GPOS LookupType 4 mark-to-base anchoring with draw-pipeline wiring; correct diacritic placement for Arabic and Devanagari (#175, #185)
- TrueType
kerntable parser hardening: correct v0 / v1 coverage decode, per-face cache, kerning-awareMeasureStringfor bothStandardandEmbeddedFont(#172)
The WriteOptions struct is the single extension point for opt-in writer behavior. Zero value preserves byte-identical output to v0.6.2. Pass dispatch order: orphan sweep → content-stream cleanup → object dedup → stream recompression → encryption → serialization.
document.WriteOptionswithWriter.WriteToWithOptions,Document.WriteToWithOptions,Document.SaveWithOptions,Document.ToBytesWithOptionsUseXRefStream— cross-reference stream object (ISO 32000-1 §7.5.8) replacing the traditional xref table and trailer. The xref stream is always written as the last indirect object so its own offset is known before serializationUseObjectStreams— packs eligible indirect objects into compressed object streams (§7.5.7). ImpliesUseXRefStream. Refused on encrypted documentsObjectStreamCapacity— cap on objects packed per/ObjStm(default 100)OrphanSweep— drops indirect objects unreachable from/Root,/Info,/Encrypt; renumbers survivors contiguouslyRecompressStreams— re-Flates eligible payloads atzlib.BestCompression, gated by a size-regression guard. Skips DCT/JPX/CCITT/JBIG2 leaf filters, multi-filter chains, and FlateDecode streams carrying/DecodeParms(predictor handling per §7.4.4.4)DeduplicateObjects— merges byte-identical indirect objects via SHA-256 of canonical serialization; rewrites references to the canonical survivor; renumbers contiguously. Excludes catalog,/Info,/EncryptCleanContentStreams— removes emptyq...Qsave/restore pairs and identity1 0 0 1 0 0 cmoperators (§7.8) from page content streams. Includes a byte-level lexer respecting strings, hex strings, comments, names, arrays, dictionaries, and inline images (BI/ID/EI per §8.9.7)core.PdfIndirectReference.SetNum— object-number setter used by writer-side renumbering passescore.PdfStream.WillCompress— getter matching the existingSetCompresssetter; lets writer-side passes skip streams the writer will already deflatecore.DeflateStreamData/core.InflateStreamData— exported zlib codec helpers (formerly the unexporteddeflate)examples/optimize— runnable demo with four fixtures (text-heavy, many empty pages, table-heavy, imported text-heavy) reporting byte-size deltas across the optimizer toggles. Imported fixture: 40,569 → 6,071 bytes (85.0% saved) with the full stack
tmplpackage —html/templateintegration: parse a template, execute against caller data, feed the result to the HTML converter (#155)@font-facewithsrc: url(data:font/...)base64 data URI fonts (#159)
examples/indic— Hindi, Sanskrit, Marathi, Nepali samples (#191)examples/rtl— expanded with full Arabic shaping showcase (#191)examples/optimize— multi-fixture optimizer comparison (#177, #181)- Stress tests for column, grid, flexbox, and SVG layouts (#126, #153)
<br>inside<strong>,<em>,<a>, and other inline elements — previously panicked. All 14 inline tags now route line breaks correctly through theTextRun.IsLineBreakfield (#147, #150)- Multi-line headings no longer overprint wrapped lines (#132)
column-span: allinside multi-column containers — correct break handling at leading, trailing, consecutive, and column-boundary positions (#127)- Heading overflow tag and
Consumedaccounting on page break (#139) - Gradient
stop-opacityplumbed throughlayout.GradientStopto the rasterizer (#146) - JPEG parser out-of-bounds found by fuzz testing (#169)
- CSS Custom property
var()references inalign-items(#128)
corepackage — correctness fixes, hardening, gradual API cleanup; sentinel errors for parser failures (#165)contentpackage — operator validation against ISO 32000 operator set; coverage to 100% on operator emission paths (#166)imagepackage — limits on dimension and component count; fuzz suite for JPEG, PNG, TIFF; CMYK JPEG via Adobe APP14 (#167, #169, #173)fontpackage — sentinel errors, documented concurrency contract, coverage on subset and cmap paths (#168, #173)- SVG
preserveAspectRatioparsed and honored; barcode fuzz suite (#178)
- Go module dependencies:
golang.org/x/image0.38.0 → 0.39.0golang.org/x/net0.52.0 → 0.53.0- Added
golang.org/x/text/unicode/bidifor UAX #9 bidirectional text
softprops/action-gh-releasev2 → v3 (release CI)- Internal:
ARCHITECTURE.mdupdated with the bidi dependency entry;font/kern.goextracted fromtruetype.go; GSUB extracted from theFaceinterface into an optionalGSUBProvider;unicode/graphemeextracted as a leaf package - READMEs: end-to-end HTML-to-PDF benchmarks and performance section; Language SDKs section pointing at Java and WASM ports
- David Richardson (@enquora) — stress-test contributions for column, grid, flexbox, SVG layouts (#126)
No breaking API changes — all additions below are additive. layout.Grid.SetAlignContent keeps its signature; it now also records that the value was explicitly set so implicit "normal" stretching is preserved. The C ABI grows from 348 to 372 exports, all additive.
- SVG
<image>elements with data-URI raster sources —<image href="data:image/png;base64,...">inside an<svg>now decodes and draws the embedded PNG/JPEG/WebP/GIF. Missingwidth/heightfall back to the raster's intrinsic pixel dimensions (#130) - Real SVG gradients —
linearGradientandradialGradientreferenced viafill="url(#id)"are now rasterized and drawn clipped to the shape instead of collapsing to the first stop color (#130) svg.RenderOptions.RegisterImage— new callback for wiring an external image decoder and XObject registrar from SVG<image>elements, keeping the svg package free of image-format dependencies (#130)svg.RenderOptions.RegisterGradient— new callback for rasterizing SVG gradients into PDF image XObjects; nil callback preserves the legacy first-stop fallback (#130)svg.Node.LinearGradient()/svg.Node.RadialGradient()— accessors returning parsed gradient definitions (LinearGradientInfo/RadialGradientInfo) with resolvedStopvalues, so callers can drive an external rasterizer without reparsing SVG syntax (#130)svg.BBox— new exported type representing an axis-aligned bounding box in SVG local coordinates, used as the shape bounding box passed toRegisterGradient(#130)- 24 new C ABI exports —
folio_document_to_bytes,folio_document_validate_pdfa, Div extras (set_aspect_ratio,set_keep_together,set_border_radius_per_corner,set_width_percent,set_hcenter,set_hright,set_clear,set_outline,add_box_shadow), Cell extras (set_border_radius,set_border_radius_per_corner), Grid extras (set_border,set_borders,set_template_areas), Flex extras (set_align_content,set_borders),paragraph_set_text_align_last, Image extras (set_object_fit,set_object_position),run_list_last_set_background_color,signer_new_pkcs12,parse_css_length. Total C ABI: 372 exports, up from 348 (#143)
- Inline
<svg>/<img>dropped from paragraph flow — replaced elements inside a<p>inheriteddisplay: blockfrom the parent and were silently skipped by the inline collector. They now default todisplay: inlineinapplyTagDefaults, matching browser replaced-element behavior. Affects any<p>containing a bare<svg>or<img>without an explicit inlinedisplayoverride (#130) - Inline
<strong>/<em>/<span>/<a>split paragraphs into three —<div>text <strong>bold</strong> more text</div>produced three stacked paragraphs with the trailing punctuation orphaned on a new line. Two interacting bugs: text-emphasis tags (strong,em,span,b,i,u,s,del,mark,small,sub,sup,code,a) inheriteddisplay: block, andwalkChildrendidn't group consecutive inline siblings into anonymous block boxes per CSS 2.1 §9.2.1.1.walkChildrennow buffers consecutive inline flow children and flushes them as a single paragraph (#142) <td style="width:50%">overflow in narrow flex columns — cell width percentages were resolved against the converter's outer container width instead of the table's actual layout width, producing absolute hints much larger than the table could hold and cells that ran off the page. Percentage widths are now deferred to layout time where the real table width is known (#142)- CSS Grid
align-items,justify-items, and container height — explicitheighton a grid container is now honored soalign-items/align-contenthave room to distribute;justify-itemsvalues (start/end/center/stretch) are applied; explicitalign-content: flex-startno longer triggers row stretching (#129) - Flexbox
orderproperty — children are stable-sorted by their CSSordervalue before layout; ties preserve DOM order per Flexbox spec.align-itemsnow resolvesvar(--custom-property)references through the cascade (#128) - Table sizing preserved across page-break continuations —
min-width, auto-column widths with cell hints, andborder-collapse/border-spacingnow survive when a table splits across pages (#134) - Multi-line headings no longer overprint wrapped lines —
Heading.PlanLayoutallocates per-line boxes so long headings that wrap at narrow widths render cleanly without glyph overlap (#132) column-span: allinside multi-column containers — elements withcolumn-span: allnow break the column flow correctly at leading, trailing, consecutive, and column-boundary positions, including whencolumn-ruleis set (#127)- Heading overflow tag +
Consumedaccounting on page break — headings that split across pages carry the correct structure tag on continuation and no longer over-advance the available height (#139)
- CSS
aspect-ratioproperty on Div elements (CSS Sizing Level 4 §5.1) — derives height from width when no explicit height is set; supports16 / 9,auto 16/9, single number forms (#112) - CSS Color Level 4 space-separated
rgb()/hsl()syntax —rgb(255 0 0 / 0.5), percentage alpha, applies torgba()/hsla()too (#108) html.ParseCSSLengthpublic utility — converts CSS length strings ("1in","16px","2em","50%",calc()) to PDF points (#109)Document.ToBytesconvenience method — returns serialized PDF as[]bytefor HTTP responses, base64 encoding, in-memory processing (#66)- Per-side border-radius on table cells —
drawCellBordersRoundednow draws each border side independently with corner arcs, instead of requiring all four borders to be identical (#115) - WASM header/footer —
folioRenderacceptsheaderHtml/footerHtmlin settings JSON, rendered viaSetHeaderElement/SetFooterElement(#102) - Invoice example (
examples/invoice/) — professional invoice PDF demonstrating rounded table headers, CSS Grid, Flexbox, and optional Tailwind CSS v2
- Nil font panic in
runMeasurer— falls back to Helvetica whenTextRunhas no font set (#98) - Table default
border-collapsechanged fromcollapsetoseparateper CSS 2.1 §17.6 — previously prevented cell border-radius from rendering (#114) - Table default margins removed — browsers set zero margins on
<table>; addedborder-spacing: 2pxdefault per browser UA stylesheets (#117) - Div
drawRoundedBordersnow uses per-corner radii (RoundedRectPerCorner) instead of uniform radius; previously onlyr[0]was used (#104) :not([hidden])selector — attribute selectors inside pseudo-class parens were incorrectly extracted, leaving empty:not()that always returned false; enables CSS frameworkspace-y-*utilities (#101)remunit parsing —parsePlainLengthchecked"em"suffix before"rem", so"1rem"failed to parse (#111)- Table cell border-radius in HTML — converter now skips radius wiring in
border-collapse: collapsemode per CSS Backgrounds Level 3 §5.3 (#100) - README Go version corrected from 1.21+ to 1.25+ to match go.mod (#124)
- Table borders now render in
separatemode by default (previouslycollapse). Tables without explicitborder-collapse: collapsein CSS will show individual cell borders instead of shared borders. This matches browser behavior per CSS 2.1 §17.6. To restore the old behavior, addtable { border-collapse: collapse; }to your CSS.
- Layout test coverage 70% → 77.9% — 40 new integration tests for draw functions, table rendering, Div features, Grid layout, Flex column, paragraph indent/ellipsis/orphans
- Playground URL updated to
playground.foliopdf.dev
Breaking changes — see MIGRATING.md for upgrade steps.
- Renamed constructors to
New*/Load*/Parse*acrossreader,barcode,layout,sign,forms sign.LoadPKCS12renamed tosign.ParsePKCS12(same signature, name-only change)Document.Page(index)returns(*Page, error)instead of panicking- Unexported internal symbols in
readerandsvg - Baseline positioning uses CSS half-leading with actual font metrics (visual change — text shifts up ~4pt for 12pt Helvetica)
vertical-alignaccepts length/percentage values (previously ignored)
- Element-based headers/footers:
SetHeaderElement,SetFooterElement,SetHeaderText,SetFooterText AddHTMLTemplate/AddHTMLTemplateFuncsfor Go template → PDFValidatePdfAfor early PDF/A validation- Per-run text highlight (
WithBackgroundColor,<mark>in HTML) - Inline elements in paragraphs (
<img>,<svg>,display:inline-block) <sub>and<sup>rendering with baseline shift and correct spacingbaseline-shiftCSS property (keywords and lengths/percentages)vertical-alignextended with length/percentage values per CSS 2.1- Empty lines from consecutive
\n\nin paragraphs RunInlinefor inline layout elements within paragraphs- CSS:
text-align-last,::marker,cmyk(),object-fit,@supports,min()/max()/clamp(),:is()/:where(), repeating gradients,column-width/column-rule,string-set/string(),page-break-inside: avoid, escape sequences in selectors, multiplebox-shadow - WebP and GIF image formats
<sub>/<sup>baseline shift — previously only reduced font size (#86)- Adjacent styled runs no longer insert spurious spaces; inline whitespace collapsing per CSS Text Level 3 §4.1.1 (#86)
- Punctuation after
</sup>/</sub>keeps correct styling (#86) - Punctuation at font boundaries keeps its own font (#30)
- Consecutive
\n\nproduce visible empty lines (#91) - Blank lines preserved across page splits (#95)
- Paragraph baseline uses CSS half-leading
(lineH + ascent - descent) / 2(#90) cloneWithWordspreserves all Word styling + line breaks on page-split paragraphs- Style changes at line break boundaries preserved during page splits
- Overflow handling includes following siblings in Div layout (#13)
- Table layout handles zero/negative height without panicking
- Inline-block SVG/IMG dispatch to correct converters (#71)
- Inline element alignment: line-relative child positions (#71)
buildParagraphFromRunsalways usesNewStyledParagraph(was droppingBaselineShift/BackgroundColor)- RGB color components clamped to 0–1
- Case-insensitive attribute selector matching
- Alpha premultiplication fix for PNG
- Font descriptor flags from actual metadata
- Kern format 0 nPairs validated
- Encrypted PDFs detected with clear error
- Signatures preserved on multi-sign PDFs
- Highlight/underline/strikethrough use actual font metrics (#73)
- Predictor column count bounded to prevent allocation DoS
- Ben Davidson (@bendavidsonku) — inline elements in paragraphs, per-run text highlight background (#71, #72)
- Jason Kulatunga (@AnalogJ) — table zero-height fix, overflow sibling handling (#13)
golang.org/x/imagev0.37.0 → v0.38.0- Internal:
html/converter.gosplit into focused modules - Internal:
ARCHITECTURE.mdwith design principles, layering rules, naming conventions
- PDF redaction —
RedactText,RedactPattern,RedactRegionspermanently remove text from content streams with character-level TJ splitting precision; configurable fill color, overlay text, and metadata stripping (#59) - Page import —
Page.ImportPageandPage.ImportPageWithOptsload existing PDF pages as Form XObjects (ISO 32000 §8.10) for template workflows;reader.ExtractPageImportconvenience API with full indirect-ref resolution (#47) - Drawing primitives —
Page.AddLine,Page.AddRect,Page.AddRectFilledfor low-level graphics on pages - PDF/UA accessibility — alt text for images, custom structure tags, structure tree reading from existing PDFs (#60)
- Paragraph
\nline breaks —\n,\r\n, and\rnow produce forced line breaks in paragraphs and table cells (#61, #63) - C ABI expanded to 346 functions (up from 330) — adds redaction, page import, drawing, digital signatures, encryption permissions, page manipulation, content extraction, form flattening, merge, TextRun builder, styled list/heading exports
- Examples —
merge/(parse, merge, extract text),sign/(PAdES B-B digital signature),report/(multi-page layout API),import-page/(external PDF template filling),redact/(sensitive text removal)
- Import page blank output —
resolveDeeprecursively resolves all indirect references in imported resources;hoistStreamsconverts nested PdfStream objects to indirect refs, fixing blank/partial output with real-world PDFs - Paragraph
\ncollapsed to space —splitWordsnow splits on newlines first and inserts forced line break markers
- Release workflow — replaced deprecated
macos-13runner withmacos-latestfor x86_64 builds - Fuzz test regex — anchored
-fuzz='^FuzzParse$'to avoid matchingFuzzParsePDF
- Marc Ole Bulling —
<br>nil pointer fix (#10) - Moritz (@FrauElster) — PDF/A-3b file attachments (#17)
- Piotr Pawlak (@piotrxp) — SSRF prevention for remote resources (#39)
- C ABI expanded to 281 functions (up from 115) — covers nearly all Go engine features
- Barcode C ABI —
folio_barcode_qr,qr_ecc,code128,ean13+ layout elements - SVG C ABI —
folio_svg_parse,parse_bytes+ layout elements with size/align - Link C ABI — hyperlink, embedded font, and internal link layout elements
- Flex C ABI — full flexbox container with items, direction, justify, align, wrap, gap, borders
- Grid C ABI — CSS Grid with template columns/rows, auto-rows, placement, justify/align items/content
- Columns C ABI — multi-column layout with gap and custom widths
- Float C ABI — left/right floating elements with margin
- TabbedLine C ABI — tab-stop text with dot leaders for TOC-style layouts
- Form filling C ABI —
folio_form_filler_new,set_value,set_checkbox,field_names,get_value - Form field builder C ABI —
folio_form_create_text_field,create_checkbox+set_value,set_read_only,set_required,set_background_color,set_border_color, thenadd_field - Additional form fields — multiline text, password, listbox, radio group
- Document watermark —
folio_document_set_watermarkandset_watermark_config - Outlines/bookmarks C ABI —
folio_document_add_outline,add_outline_xyz,outline_add_child - Named destinations —
folio_document_add_named_dest - Viewer preferences —
folio_document_set_viewer_preferences - Page labels —
folio_document_add_page_label - File attachments —
folio_document_attach_filefor PDF/A-3b compliance - Inline HTML —
folio_document_add_htmlandadd_html_with_options - Page-specific margins —
folio_document_set_first_margins,set_left_margins,set_right_margins - Absolute positioning —
folio_document_add_absolute - Page extensions — art box, page size override, page-to-page links, text annotations, text markup annotations (highlight, underline, squiggly, strikeout), separate fill/stroke opacity
- All 14 standard font accessors — added
helvetica_oblique,helvetica_bold_oblique,times_italic,times_bold_italic,courier_bold,courier_oblique,courier_bold_oblique,symbol,zapf_dingbats - Paragraph extensions — orphans, widows, ellipsis, word-break, hyphens
- Table extensions — footer rows, cell spacing, auto column widths, min width
- Cell extensions — per-side padding, vertical alignment, borders, width hints
- Div extensions — border radius, opacity, overflow, max/min width, box shadow, max height
- List extensions — leading, nested sub-lists
- Image extensions — TIFF loading, element alignment
- C ABI audit script (
scripts/audit-cabi.sh) — detects drift between Go exports, folio.h, and built symbols - C integration tests — 258 tests covering all exported functions
- Library version injected at build time via
-ldflags "-X main.version=..."—folio_version()returns the git tag in releases,git describein dev builds - CI — added C ABI audit, shared library build, and C integration test steps
- Release workflow — builds native shared libraries for 5 platforms (linux-x86_64, linux-aarch64, macos-x86_64, macos-aarch64, windows-x86_64) with SHA256 checksums
- Makefile — added
audit,audit-build, cross-compilation targets (cross-linux-amd64, etc.), OS-aware shared library extension detection
<br>nil pointer — fixed nil pointer exception with<br>tags (#10)- PDF/A-3b file attachments — proper embedded file streams with
/AF,/Names, MIME types (#17) - ZUGFeRD lint — deterministic timestamps, fixed example (#41)
- Content stream compression — FlateDecode for content streams, merge stream dict bug (#44)
- SSRF prevention — URL policy interceptor for blocking/modifying remote resource requests (#39)
- Radio button / checkbox appearance — fixed appearance stream generation for form fields
:rootselector — now correctly matches the<html>element- Gradient rendering — fixed CSS gradient parsing and rendering
- Page number CSS counter —
counter(page)andcounter(pages)now work in margin boxes - MarginBox API — exposed
MarginBoxes/FirstMarginBoxesonConvertResultfor simpler programmatic access (#54)
- Clickable links in PDFs —
<a href="...">inside paragraphs, headings, and list items now produce PDF link annotations (#23, #26, #27) - Multiple links per line — paragraphs with several inline links each get their own precise annotation rectangle
- Internal document links —
layout.NewInternalLinkresolves to direct page references for macOS Preview compatibility - Layout API link support —
TextRun.WithLinkURI()andWithDecoration()for building linked text programmatically;List.AddItemRuns()for linked list items - Links example (
examples/links/) showcasing external, inline, multi-line, styled, heading, and list item links plus bookmarks and internal navigation - Fonts example (
examples/fonts/) demonstrating custom@font-facewith Unicode (CJK, Cyrillic, Japanese)
- Custom
@font-facefamily names ignored —parseFontFamilywas mapping all names to standard fonts; now preserves custom names for embedded font matching (#16) page-break-afterignored when body haswidth: 100%—AreaBreakelements trapped inside Div wrappers are now hoisted out so the renderer can act on them (#21)- CSS class selectors case-insensitive —
.myClassnow matchesclass="myClass"regardless of case (#28) - Punctuation spacing at run boundaries — period/comma after a styled span (e.g.
<b>word</b>.) no longer gets an extra inter-word space (#25) - Underline continuous across multi-word links — decoration extends through trailing spaces between consecutive linked/decorated words
@font-facefamily name case mismatch — font-face names are now lowercased consistently so CSS lookup matches
- Split
html/converter.gointo 11 focused files by responsibility (paragraph, table, block, flex, forms, image, list, heading, link, style, helpers) — no behavior changes (#34)
- Emrecan BATI — Apache 2.0 license text cleanup
- Comprehensive GoDoc comments across all 14 packages — every exported and unexported symbol now has an accurate doc comment following Go conventions
- Package-level doc comments added to
layout,html,svg, and consolidated incore(had two conflicting comments) ARCHITECTURE.mddocumenting design principles, package responsibilities, layering rules, dependency policy, and non-goals- Examples directory with hello-world sample
- Stale/inaccurate doc comments: watermark "prepends" → "appends",
dss.Buildreturn description,PdfObjects→BuildObjects, merged interface docs in layout, and others cmd/folio printUsagereferenced nonexistent "region" extraction strategysvg/doc.golisted nonexistentclipPathsupport, missingradialGradientlayout/doc.goreferenced wrong type names (Tab→TabbedLine, nonexistentTransformelement)font/standard.gopackage doc was stale ("and later font parsing" — parsing is fully implemented)
- Removed committed
folio.wasmbinary (7.2MB) from the repository; the release workflow already builds it fresh per tagged version - Added
*.wasmto.gitignore - golangci-lint issues fixed and linter added to CI
- Apache 2.0 license replaced with verbatim text; missing license headers added
- WOFF1 font decoding —
@font-facenow supports.wofffiles via automatic format detection (font.LoadFont) - CSS custom properties (variables) —
--name: valuedeclarations withvar(--name, fallback)resolution, inheritance, and nesting - CSS counters —
counter-reset,counter-increment,counter()andcounters()in::before/::aftercontent - CSS
clearproperty —clear: left/right/bothadvances past active floats before placing elements - CSS
border-spacing— horizontal and vertical cell spacing for tables in separate border model - HTTP background images —
background-image: url('https://...')fetches remote images in non-WASM builds - Inline-block in text flow —
display: inline-blockelements flow within paragraphs as "big words" with correct line-breaking and height expansion - Containing-block absolute positioning —
position: absoluteresolves against nearest positioned ancestor via overlay children, not just the page - Liang-Knuth hyphenation — 4938 TeX US English patterns for linguistically correct syllable breaks, replacing geometric character splitting
- C ABI export layer —
export/package for FFI from Python, Ruby, Swift, etc. - Full sRGB ICC profile and PDF/A-1b compliance support
- QR code v1-40 with numeric/alphanumeric encoding modes
- Symbol and ZapfDingbats font width tables
font.LoadTTFcalls in HTML converter replaced withfont.LoadFont(auto-detects TTF/OTF/WOFF)hyphenateWord()uses pattern-based breaks first, falls back to character splitting
- CSS Grid layout —
display: gridwithgrid-template-columns,grid-template-rows,grid-template-areas, named areas,auto-rows, alignment (justify-items,align-items), and page break support - Absolute positioning with z-index —
position: absoluteelements ordered byz-index margin-left: autoright-alignment for inline-block SVGs in flex containers
- Flex width double-resolution when both
widthUnitand percentage were set - Inline-block SVGs disappearing due to missing width propagation
- Auto-height pages via CSS
@page { size: 80mm 0; }— page height sizes to content (receipts, flyers) - Negative margin support for flex column children — enables CSS patterns like
margin: -10px -14pxto break out of parent padding margin-left: autoon flex row items — pushes items to the right edge (e.g., seat box alignment)margin-top: autoon flex column items — pushes items to the bottom (e.g., footer positioning)- Cross-axis stretch (W3C Flexbox §9.4) — flex row items stretch to match tallest sibling, with or without definite container height
- Flex column
flex-grow— items withflex: 1now grow to fill remaining space in column direction - Flex column
justify-content— space-between, center, flex-end, space-around, space-evenly in column direction hasDefiniteCrossSizeflag on Flex — enables stretch when Flex is wrapped in a height-constrained Div- Watermark support in WASM render API via
watermarkparameter - Automatic Unicode font embedding for non-WinAnsi characters (CIDFont with embedded cmap)
- CIDFont fallback decoding from embedded font cmap tables
- Font caching for repeated font resolution
- Form XObject resolution in PDF reader
- Tagged PDF extraction improvements
- Full text matrix tracking and font-aware space detection in reader
- Xref cycle detection, hybrid xref support, stream length correction
- SVG enhancements: text-anchor, tspan, defs/use, gradient support
- Percentage heights now resolve against parent container's explicit height, not the page — fixes vertical bar charts overflowing their containers
box-sizing: border-boxno longer double-subtracts padding from width/height — only border is subtracted since the Div handles padding internally- Double-padding on wrapped flex containers — when a Flex has CSS width/height, visual properties (padding, borders, margins) are cleared from the Flex and applied only to the wrapper Div
letter-spacingin width measurement —Paragraph.MinWidth()andMaxWidth()now include letter-spacing, preventing flex items from being measured too narrow- Floating-point overflow in
margin-top: auto— added 0.01pt epsilon tolerance to prevent items from silently overflowing due to float rounding margin-top: autophase consistency —neededBelowcalculation now includesmarginBottomof subsequent items in both phase 1 and phase 3- SpaceBefore/SpaceAfter doubling on flex items — element margins are cleared when FlexItem margins take over (Div, Flex, and Paragraph)
- Background preserved on wrapped Flex —
min-heightbackgrounds now fill the full height (kept on both Div wrapper and inner Flex) parseFloatnegative numbers — CSS parser now correctly handles negative values like-10px- Flex children splitting into separate items instead of grouping per HTML child
- SVG shapes invisible and text mirrored
- Sequential elements overlapping by tracking cumulative Y offset in renderer
<br>tags in paragraphs and CSS width as flex-basis- WASM binary size halved by excluding
net/httpfrom js builds
- Cross-axis stretch now fires for all flex row items (not just when container has definite height)
planColumnrefactored into 3-phase layout: measure, grow, position
- CLI
extractcommand with pluggable strategies (simple, location) - CLI
signcommand for PAdES digital signatures - Table
SetBorderCollapse(true)for CSS-style collapsed borders - CSS
calc()support in HTML-to-PDF (e.g.,width: calc(100% - 40px)) - CSS
@pagerule parsing (size, margins) in HTML-to-PDF - CSS
orphans/widowsproperties in HTML-to-PDF - CSS
break-before/break-after/break-insidemodern syntax - Remote image loading (
<img src="https://...">) in HTML-to-PDF - Data URI image support (
<img src="data:image/png;base64,...">) - PDF metadata extraction from HTML
<title>and<meta>tags - Content stream processor with full graphics state (CTM, color, font)
- Pluggable text extraction strategies (Simple, Location, Region)
- Path and image extraction from content streams
- Per-glyph span extraction (opt-in)
- Text rendering mode awareness (invisible text filtering)
- Marked content tag tracking (BMC/BDC/EMC)
- Form XObject recursion in content processing
- Actual glyph widths from font metrics (replaces estimation)
- Auto-bookmarks from layout headings
- Viewer preferences (page layout, mode, UI options)
- Page labels (decimal, Roman, alpha)
- Page geometry boxes (CropBox, BleedBox, TrimBox, ArtBox)
- SVG package in README
- CLI version bumped to 0.1.1
- README updated with extract and sign commands, border-collapse, SVG package
- Table border-collapse: adjacent cells no longer draw double borders
- Tables section in README had undefined variable
- Initial release
- PDF generation with layout engine (Paragraph, Heading, Table, List, Div, Image, Float, Flex, Columns)
- PDF reader with tokenizer, parser, xref streams, object streams
- PDF merge and modify
- HTML-to-PDF conversion with CSS support
- Digital signatures (PAdES B-B, B-T, B-LT)
- Interactive forms (AcroForms)
- Barcodes (Code128, QR, EAN-13)
- Tagged PDF and PDF/A compliance
- SVG rendering
- CLI tool (merge, info, pages, text, create, blank)
- Font embedding and subsetting (TrueType)
- JPEG, PNG, TIFF image support
- Encryption (AES-256, AES-128, RC4)