NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Go modules · #2123 by repository stars
Last release 18 days ago
20 Sep 2026
Ships fairly regularly
a new release about every 2 weeks
Nearly every release is documented
notes for 20 of 20 stable releases
Nothing withdrawn
no release was ever pulled
6 months old
49 releases · first in 2026
One column per month.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
reader: guard page tree walk against cycles and excessive depth by @carlos7ags in #431
Full Changelog: v0.10.0...v0.10.1
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 /ByteRange covering the whole file, and a
trusted chain; closes the incremental-update tamper gap where DigestValid
alone stayed true for a partially covered file.optimize.Options with AllowLossy opting in to optimizing documents
that carry document-level compliance data (see ErrLossyInput).zugferd.NewAmountChecked / Amount.AddChecked — overflow-checked
amount construction and addition for data-driven input.EncryptMetadata false files instead of only round-tripping folio's own
writer.TSAClient, OCSPClient, AddDocumentTimestamp, AddDSS) gained its
first test coverage, driven by local httptest servers.sign.Verify parses full certificate chains — the signer certificate
is selected by the SignerInfo's issuer-and-serial, remaining certificates
feed chain building as intermediates, the RFC 5652 contentType signed
attribute is required, HasTimestamp reports only a real RFC 3161
timestamp attribute, /Contents hex strings tolerate whitespace and odd
digit counts, and signatures and /DSS are 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.Bytes refuses compliance-bearing input with ErrLossyInput
unless Options{AllowLossy: true} is passed (behavior change — see above)./Perms — the encrypted permissions
block is decrypted and checked against /P, so a tampered permission word
is rejected instead of silently trusted.FuzzParsePDF), tests run on an ubuntu/macos/windows
matrix, and the wasm binary is build-checked.optimize preserves inherited page attributes — /MediaBox,
/CropBox, /Rotate, and /Resources inherited from an ancestor
/Pages node are materialized onto each rewritten page; inputs relying on
inheritance previously came out with no page size or resources.
zugferd amounts no longer wrap on overflow — parsing bounds the
digit count, arithmetic is overflow-checked in validation, and
Amount.String renders math.MinInt64 correctly; 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-collapse resolves spanned edges per grid segment — a
colspan/rowspan cell'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: ellipsis works in rendered documents — truncation now
runs in the PlanLayout render path used by HTML conversion, not only in
direct Layout calls.
Long-word breaking accounts for letter-spacing, and guards against
empty runs and negative available widths (previously a potential panic).
Layout and measureWords share 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 /Dest on the target's page instead of emitting a dangling /GoTo string action that jumps nowhere. The HTML walker wraps any block element carrying an id in a layout anchor marker that tags its first PlacedBlock; the renderer surfaces these on PageResult.Anchors, and the document layer registers them via AddNamedDest during layout — no caller doc.AddNamedDest() needed. Resolved links use an /XYZ destination 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 /Dests writer share one destination-array builder and agree first-registration-wins on duplicate names. The anchor and bookmark markers now expose an unwrap so the layouter still reaches a decorated element's optional interfaces (CSS clear, page-break-inside: avoid, flex cross-axis stretch) — previously wrapping an element to record its id silently 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 the layout.Anchor behavior documented under 0.8.0 (#223), which was absent from the tree.
/XYZ destinations emit an explicit Top coordinate — named destinations and outline entries previously wrote null for a zero Top, which viewers read as "retain current y", so a target at y=0 navigated nowhere. Top is now always an explicit real; Left and zoom keep null-for-zero to preserve the reader's horizontal scroll and zoom.
letter-spacing applies 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: none on <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 /Pages tree 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.
links binary is no longer tracked and is removed by
make clean.build(deps): bump golang.org/x/net from 0.55.0 to 0.56.0 by @dependabot [bot] in #366
Full Changelog: v0.9.1...v0.10.0
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 with sign.SignPDF. For each signature it locates the signature dictionary, recomputes the /ByteRange digest and checks it against the CMS messageDigest signed attribute, verifies the CMS signature (RSA PKCS#1 v1.5 or ECDSA) over the signed attributes, checks that /ByteRange covers the whole file except the /Contents hex string, and — when VerifyOptions.Roots is supplied — builds a certificate chain. sign.Report.Signatures reports each check independently (DigestValid, SignatureValid, ByteRangeCoversFile, ChainStatus) so partial validity is never conflated with full validity. Revocation fetching, timestamp-token and /DSS content validation, and PAdES level classification are out of scope; presence of a timestamp token or /DSS is 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.reader can now open PDFs encrypted with the Standard security handler — reader.ReadOptions.Password authenticates against the document's user or owner password (ISO 32000 §7.6.3-7.6.4); reader.Parse implicitly 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 returns reader.ErrInvalidPassword; unsupported security-handler configurations (public-key handlers, RC4-40, non-standard crypt filters) return reader.ErrUnsupportedEncryption instead of the previous blanket refusal. Decrypted documents are plaintext in memory and are written back out unencrypted unless the caller explicitly re-encrypts via document.SetEncryption.zugferd package — generates Factur-X/ZUGFeRD hybrid e-invoices: a typed Invoice renders EN 16931 UN/CEFACT Cross-Industry Invoice (CII) XML and Invoice.Attach embeds it into a document.Document as 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-point Amount type so money never touches a float. examples/zugferd is ported onto it. See docs/design-zugferd.md for profile coverage and known limitations.optimize package — 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 returns optimize.ErrEncrypted.html.Options.AllowRemoteFetch — opt-in for fetching http(s) assets referenced by the document. Default false.html.Options.AllowAbsolutePaths — opt-in for reading absolute filesystem paths named in document content when BaseFS is nil. Default false; reads are size-capped like the HTTP path.html.DenyInternalHosts — a URLPolicy that 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 when AllowRemoteFetch is true and URLPolicy is 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 with html.ErrAssetBudgetExceeded.html.ErrRemoteFetchDisabled — returned when a document references an http(s) asset but AllowRemoteFetch is false. Reported normally (logged, and surfaced under StrictAssets).html.ErrAbsolutePathDenied — returned when a document references an absolute filesystem path but AllowAbsolutePaths is false. Reported normally.html.ErrAssetBudgetExceeded — returned when a conversion's aggregate asset read size crosses Options.MaxTotalAssetBytes. Reported normally.folio_buffer_len64(buf) — 64-bit counterpart to folio_buffer_len, which now saturates at INT32_MAX (and records an error via folio_last_error) for buffers over 2 GiB instead of silently truncating. Prefer the 64-bit accessor for large buffers.folio_string_free(s) — releases a string returned by folio_last_error, which now hands back a caller-owned copy (see Changed (breaking)). Passing NULL is a no-op.font.Face now includes GSUB(), GPOS(), and GIDToUnicode() — the optional font.GSUBProvider and font.GPOSProvider interfaces are removed, and every Face implementation must provide the three methods directly. A Face with no shaping data for a given table returns nil rather than omitting the method; GIDToUnicode returns 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).html no longer fetches remote or absolute-path assets by default — a document's http(s) references (<img>, background-image: url(), linked stylesheets, @font-face url()) are only fetched when Options.AllowRemoteFetch is set, and absolute filesystem paths in document content are only read when Options.AllowAbsolutePaths is set (both default false). When remote fetch is enabled with no Options.URLPolicy, the built-in html.DenyInternalHosts policy 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. Set URLPolicy to a function returning nil to allow every host. Options.FallbackFontPath and the built-in system-font search are unaffected (#391).folio_last_error now returns a caller-owned copy — previously the returned char* 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 new folio_string_free. A caller that keeps the old borrow-and-never-free assumption leaks one short string per call to folio_last_error instead of crashing or corrupting memory, until it adopts folio_string_free (#392).html and svg output — the two packages each carried an independent 148-entry named-color table stored as rounded decimal literals (3 decimals in html, 4 in svg), so the same keyword could render to different bytes depending on which package handled it. Both now share one table in internal/csscolor that computes each component as x/255 at full float64 precision, which changes the emitted bytes for named colors slightly versus the old decimal literals in both packages. svg color parsing also now accepts the full CSS color grammar (4/8-digit hex, CSS Color 4 space-separated rgb(), and hsl()/hsla()) that html already supported — previously-valid svg inputs are unaffected. As part of sharing the parser, malformed svg color literals (invalid hex digits, non-numeric rgb()/alpha components) are now treated leniently — the bad component defaults to 0 rather than rejecting the whole color, matching how html already handled them.internal/cssunit — html and svg previously 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 (html resolves to PDF points, svg treats every unit as an SVG user unit 1:1). Two svg parsing corrections fell out of the consolidation — see Fixed.border-collapse: collapse now resolves competing borders on shared cell edges per CSS 2.1 §17.6.2 (width, then style priority, then source order; border-style: hidden always 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 CSS list-item counter and <ul>/<ol> implicitly reset it, per CSS Lists Module 3, so ::marker content driven by counter(list-item) (or counters(list-item, sep) for nested lists) resolves correctly instead of always reading 0. ::marker { font-style: italic } is also now honored.@page margin boxes (@top-*/@bottom-*) now honor font-style, font-weight, and font-family — previously only font-size and color were applied to generated margin-box content; font-style: italic and similar declarations were silently dropped (#378).rowspan groups 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).svg dimension parsing no longer accepts multi-suffix garbage — attribute values like width="10empx" were parsed by stripping every known unit suffix in sequence, so leftover garbage silently fell through to a clean number instead of failing. svg length parsing now consumes exactly one unit suffix; anything left over is rejected.svg font-size style now accepts pt-suffixed values — font-size: 12pt previously failed to parse (only px was recognized) and silently left the inherited font size in place; it is now read as 12 user units, consistent with how the width/height/r attributes already treat pt.bfrange destinations now parse in ToUnicode CMaps — a beginbfrange/endbfrange block 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).document.WriteOptions.Deterministic./Colors and /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 /Colors and /BitsPerComponent, with hostile values sanitized to safe defaults.), {, or } byte produced an empty keyword token, so a caller looping on Next until AtEnd never advanced. Fixed alongside the encrypted-PDF reading work.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 and float: right lands 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 new Div-level float context handles placement, clear among children, and growing the container to enclose its floats (the flow-root model) (#367).display: table-cell children — a display: table element whose direct children were display: table-cell boxes with no display: table-row wrapper 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)./W widths in xref streams — a crafted /W array 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.cmap subtables now cap the total number of code points expanded across all segments/groups at 1<<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.ReadAll — WOFF decoding now caps the declared total uncompressed size of all tables at 256 MB and limits each table's zlib decompression to its declared size plus one byte, so a decompression bomb fails fast instead of materializing an unbounded buffer.MaxDimension/MaxPixels) before allocating the pixel buffer, closing a memory-exhaustion path where a crafted header claimed a small file but an enormous canvas.unsafe.Slice with 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 via unsafe.Slice, rejecting a hostile or mistaken count instead of risking an out-of-bounds read or crash.MaxXrefSize — ReadOptions.MemoryLimits.MaxXrefSize was previously ignored for the xref-stream decompression step, which always used the hardcoded 32 MB default regardless of a caller-configured limit.sfntFace lazy caches are synchronized — the GSUB, GID-to-Unicode, and kern-pair caches on the TrueType/OpenType Face implementation are now each guarded by a sync.Once, so a single Face shared across goroutines (a common pattern for an embedded font reused by concurrent renders) no longer races on first use.MeasureString — eliminated the per-call grapheme-cluster allocation in the hot text-measurement path.sign.Verify.svg malformed-input tests and fuzz targets.font, layout).layout/paragraph.go and the html converter into per-seam files, no behavior change (#405).paragraph.go/tab.go/table.go into a single resolveMeasurer helper.len([]rune(s)) rune-counting with utf8.RuneCountInString where the runes themselves weren't needed.golang.org/x/image 0.41.0 → 0.43.0golang.org/x/net 0.55.0 → 0.56.0…, and SetPageSize / PageSize ( #338 ). No breaking changes.
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 of AddHTML, exported so callers who need the raw ConvertResult (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:fixed elements, @page size and margins, all four margin-box sets, and <title>/<meta> metadata — and sets the page size from any @page rule. AddHTML is now exactly ConvertFull + AddConvertResult, so the two paths cannot diverge. This closes the discoverability trap where a hand-written for _, e := range result.Elements { doc.Add(e) } loop silently dropped absolutes, margin boxes, and @page config with no error (#338)document.Document.SetPageSize(PageSize) and PageSize() PageSize — read and override the page dimensions after NewDocument. The size was previously fixed at construction even though AddHTML mutates it internally from @page rules; these expose the missing setter/getter. AddConvertResult derives the size from @page automatically, so the pre-NewDocument geometry-resolve dance is no longer required (#338)barcode.NewCode128 now 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)::marker — li::marker { content: ... } now defines the marker string, so counter-reset / counter-increment with counter() / counters() produce true nested numbering (e.g. 1, 1.1, 1.1.1) instead of a per-level restart. Adds list-style-position (inside / outside): inside renders 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). The list-style shorthand now scans all tokens (position / type / image). layout.List gains SetMarkerInside. New examples/legal-numbering renders a nested Master Services Agreement with hanging indents. Phase 1 of #356rowspan vertical geometry — a rowspan cell 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: buildGrid excludes 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)border-radius (#329)border-radius renders 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-radius survives 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 matching TextRun.BackgroundColor highlight) is now cleared, so the common "number inside a colored circle" badge works as a single rounded box (#329)border-radius applied across the remaining container paths — flex: 0 0 auto chips, grid items, display: block spans, blockquote, figure, and table wrappers each now round like display: 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)@page, position: fixed, margin boxes (#327, #328)position: fixed elements 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 / :right margins inherit the base @page cascade — 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)@page parsing correctness bundle — a batch of fixes to @page rule parsing across the converter, layout, and document layers (size/orientation resolution, margin percentage/calc resolution, and margin-box cascade), so every document-building entry point applies identical paged-media geometry (#327)@page margin boxes embed the default font for PDF/A — running headers/footers declared via @bottom-center etc. 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-block shrinks 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)<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-radius is 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)<ol> spanning pages restarted at 1. 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. Adds layout.List.SetStart/Start (#347)list-style-position: inside already drew relative to the content edge). Single-level lists are unaffected (#358)@page :left / :right — fixing the wiring around AddConvertResult surfaced that the left/right margin-box reconstruction dropped the HasColor flag and the embedded font that base and :first already carried; all four sets are now consistent (#338)examples/html-to-pdf no longer drops absolutes — the example wired the raw ConvertFull result by hand and forwarded only result.Elements, silently dropping position: fixed/absolute content. It now uses doc.AddConvertResult(result), which forwards elements, absolutes, @page config, and margin boxes in one call (#338)barcode.NewQR produced 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)qr_decode_test.go reads 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 an examples/barcodes demo rendering QR (all four ECC levels), Code 128, and EAN-13 into one PDF for visual/scanner verification (#341, #346)<ol start> continuation across a break, unordered no-content-loss, a single item taller than a page, and SetStart clamping (#347)AddConvertResult equivalence + fidelity tests — assert AddHTML and ConvertFull + AddConvertResult leave identical document state (page size, all margins, all four margin-box sets, elements, absolutes, metadata), that absolutes and @page config survive, and the :left/:right field-fidelity regression (#338)using-folio Claude Code skill — a thin, version-controlled skill (.claude/skills/using-folio) capturing the stable usage patterns: prefer Document.AddHTML (or ConvertFull + AddConvertResult) over hand-wiring, the asset-loading options (BaseFS, FallbackFontPath, StrictAssets), PDF/A setup, and a verification cookbook. The html package docs gain matching guidance steering callers to AddHTML/AddConvertResult (#338)Full Changelog: v0.9.0...v0.9.1
Nothing published for this version
Nothing published for this version
Nothing published for this version
document: deterministic output mode (WriteOptions.Deterministic + SetFileID) by @carlos7ags in #320
Full Changelog: v0.8.0...v0.9.0
document.WriteOptions.Deterministic + document.Document.SetFileID([]byte) — opt-in byte-reproducible output. With Deterministic set, the trailer /ID is 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 XMP CreateDate/ModifyDate plus embedded-file timestamps fall back to the zero time rather than time.Now when Info.CreationDate is unset — so identical input documents produce byte-identical PDFs. Set Info.CreationDate/ModDate so the recorded dates are meaningful rather than the year-zero default. SetFileID pins the trailer /ID directly, overriding both the random PDF/A identifier and the derived digest; a non-PDF/A document that would otherwise carry no /ID gains 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.ParseError and html.AssetError error types — a typed taxonomy so callers (e.g. a service rendering untrusted templates) can map folio failures onto a response status with errors.As. *ParseError wraps 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. *AssetError is the typed form of the failures collected under StrictAssets: it carries Category ("image", "@font-face", "background-image", "stylesheet", "SVG image", "FallbackFontPath") and Ref (the offending src/href/url/path), and Unwraps to the underlying cause so errors.Is against fs.ErrNotExist, ErrURLPolicyDenied, network errors, etc. keeps working. The joined error returned by Convert/ConvertFull contains one *AssetError per 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.MaxDepth resource guards + html.LimitError — bound the HTML→layout conversion so untrusted input cannot exhaust memory or the goroutine stack. MaxElements caps the number of HTML nodes converted into layout elements; MaxDepth caps the element nesting depth. Both default to 0 (unlimited), preserving existing behavior. When a ceiling is crossed, Convert/ConvertFull stop walking the tree and return a *html.LimitError (with Kind — LimitElements or LimitDepth — and the configured Limit), failing closed instead of continuing to allocate. The guard sits at the single convertNode chokepoint every element node flows through (the <table>/grid/flex child loops included), so all conversion paths are bounded. LimitError joins the errors.As taxonomy alongside ParseError/AssetError as an input-side fault. Page-count and output-byte ceilings live at the layout and writer layers respectively and are tracked separatelycontext.Context on 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 with context.Background(), so this addition is non-breaking. The context is checked at element boundaries during the HTML tree walk (the convertNode chokepoint), at page boundaries during layout (the pagination and emission loops in renderWithPlans), and at object boundaries during PDF serialization (writeObjectBodies). When the context is done, conversion/layout/serialization stops and returns ctx.Err() (context.Canceled or context.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 callfolio package 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 the PageSizeA0…PageSizeExecutive presets), 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 the document package identifiers and carry the complete method set. Specialized APIs (HTML conversion, layout, fonts) remain in their own subpackages; the façade covers only document constructionsvg: clip to viewport under preserveAspectRatio slice by @carlos7ags in #211
Full Changelog: v0.7.1...v0.8.0
drawShapedGIDsCursive previously applied the entry/exit X delta as a Td shift 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 LookupFlag RIGHT_TO_LEFT bit which is parsed but not yet honoured by the draw path (#220)cloneWithWords reconstructs the overflow paragraph during page-break splitting and previously joined every same-style word with a literal " " regardless of SpaceAfter. CJK ideographs (which breakCJKWords emits with SpaceAfter=0) crossing a page break became "中 文 文 本", and the corrupted text re-wrapped to MORE lines than the original because each inserted space became a real spaceW-wide gap on re-measure. Affects any documents with CJK content that paginated in v0.7.x (#246)layout entry point — NewRun/NewRunEmbedded/NewParagraph*/Heading.SetRuns/Row.AddCell*/TabbedLine.SetSegments. Documents that supplied canonically decomposed strings (e.g. e + combining acute U+0301 instead 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)font-weight against standard PDF-14 fonts now rounds at 600 — pre-fix, parseFontWeight collapsed every numeric value to the binary string "normal"/"bold" with the boundary at 700, so font-weight: 600 against 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. So font-weight: 600 against Helvetica now picks Helvetica-Bold; font-weight: 800 continues to pick Helvetica-Bold. Documents that wrote font-weight: 600 against 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-face declarations — see Added (#286, #287)<th> honours explicit text-align: left / start instead of silently re-centering — pre-fix, the table-cell default-center heuristic gated on cellStyle.TextAlign == AlignLeft, so an author who wrote th { 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, or start/end resolving 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 wrote th { text-align: left } and expected the silent override will see headers shift from center to left (#288)html.Options.BasePath removed; BaseFS is the single way to resolve local assets — the v0.7.x compromise that kept both fields is gone. Callers pass any fs.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(), and Options.FallbackFontPath — flows through it. Paths are normalised to fs.FS conventions before the read: forward slashes, no leading /, no .. traversal. A leading / in document src/href is treated as web-style root-of-BaseFS (matching how <base href="/"> works in browsers) instead of an absolute filesystem path. With BaseFS nil, every local-asset reference fails — the document is expected to inline its assets via data: URIs. The C ABI signature is unchanged: folio_document_add_html_with_options still takes a basePath C string and wraps it as os.DirFS(basePath) internally (#85)@font-face URLs 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 to fonts/Inter.ttf from the BaseFS root, not from the document root. HTTP-origin stylesheets resolve relative URLs as HTTP, FS-origin stylesheets resolve them through BaseFS. Inline <style> blocks continue to resolve relative URLs from BaseFS root. Documents that previously relied on the root-anchored behavior need to use root-relative /fonts/Inter.ttf or move the @font-face rule into an inline <style>layout.UnitValue is no longer comparable with == — the struct now holds a func field for the new UnitCalc variant, so equality comparisons no longer compile. No in-tree consumer compared UnitValue for 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.Position is now [2]layout.ResolvableLength — pre-change the field was [2]float64, eagerly resolved at parse time, which dropped plain lengths and mixed-unit calc on background-position to 0 because the background box dimensions are only known at draw time. Post-change the field carries a ResolvableLength per axis whose Resolve(container, fontSize float64) float64 method evaluates at draw time. A new sibling FontSize float64 field on the struct feeds em / rem resolution. Out-of-tree code that constructs BackgroundImage directly must implement the one-method ResolvableLength interface 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 with slog.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 via httptest.NewServer; or share connection pools with surrounding code. Defaults to http.DefaultClient (#85)tmpl.RenderFile / tmpl.RenderFileTo auto-populate BaseFS — when the caller's Options.BaseFS is nil, the helpers default it to os.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, Convert and ConvertFull collect every failed @font-face url(), <img>, background-image: url(), linked stylesheet, SVG load, and FallbackFontPath, then return them joined via errors.Join at 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-face rules, 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. Empty lang (or any unrecognised hint) falls back to face 0, so ParseFont is now a thin wrapper around ParseFontForLanguage(data, "") and back-compat is preserved. Pinned by tests against synthetic 4-face TTCs and a real-system round-trip. examples/cjk updated 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 a URLPolicy callback rejects a fetch. The denial wraps it with fmt.Errorf("%w: %w", ErrURLPolicyDenied, policyErr) so callers can branch on errors.Is(err, html.ErrURLPolicyDenied) to distinguish "I told it to block" from "the asset broke." Under StrictAssets, ErrURLPolicyDenied is logged through Options.Logger but excluded from the joined return error — the caller already received the signal they wired the policy to produce (#232)font.Fallback and layout.NewParagraphFallback — Phase 1 of #192. font.Fallback wraps an ordered list of *EmbeddedFont and dispatches per script run via PickFace(firstBaseRune). NewParagraphFallback(text, fb, size) walks SegmentByScript, picks one face per script run, coalesces same-face neighbours, and emits N TextRuns. 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) int and MeasureHeight(maxWidth) float64 — wrapped line count and rendered height (sum of line heights, excluding SpaceBefore/SpaceAfter so callers compose with their own pagination math) at a given width. Both are thin wrappers over the existing Layout() 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 first n rendered lines at a given width. Both returned paragraphs are clones; the receiver is unchanged. n <= 0 returns (nil, full), n >= total lines returns (full, nil). Spacing ownership: the half that owns the original boundary keeps that spacing; FirstLineIndent is dropped from both halves so re-laying re-applies it correctly. Designed for first-N-lines-plus-appendix flows. The cloneWithWords audit 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.Anchor element + auto-registered PDF named destinations — <a href="#anchor"> now emits a real PDF link annotation with an internal /Dest rather than a /URI action containing the literal "#anchor". The HTML walker prepends a zero-height layout.Anchor for any element with an id attribute; the renderer surfaces these onto PageResult.Anchors, and the document layer registers them as PDF named destinations automatically — no caller doc.AddNamedDest() calls required. TextRun/Word link plumbing splits into LinkURI vs LinkDestName so a single line can carry both kinds of links (#223)layout.UnitCalc variant + layout.CalcUnit(fn) constructor — a UnitValue whose value is computed lazily at layout time via a Calc func(available float64) float64 closure. 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 via Pt — the new variant only kicks in when the parser detects percentage participation through cssLength.dependsOnPercent() / calcExpr.dependsOnPercent() (#236)document.Info.Language (BCP-47 / RFC 3066) — writes catalog /Lang per ISO 32000-2 §14.9.2 and dc:language in XMP. Required for any PDF/A Level A variant per ISO 19005-2/3 §6.7.2 (#214)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:conformance omitted 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); the pdfaf extension schema is declared for every level allowing associated files (#214)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. Existing FOLIO_PDFA_* numeric values are preserved (#214)folio_paragraph_measure_lines / folio_paragraph_measure_height / folio_paragraph_split_after_line — surface the new Paragraph measurement and split helpers (#238, #253) to non-Go callers. measure_lines returns int32_t (-1 on bad handle); measure_height returns double (-1.0 on bad handle); split_after_line returns two paragraph handles via out-pointers, either of which may be 0 for the no-op halves (n <= 0 → (0, full), n >= total → (full, 0)). Receiver unchanged; caller frees non-zero halves (#283)folio_font_parse_for_language(data, length, lang) — wraps font.ParseFontForLanguage so C consumers can pick the right TTC face by BCP-47 tag ("zh-CN", "ja", etc.) for pan-CJK font collections. NULL lang falls back to face 0, matching folio_font_parse_ttf semantics. The font.Fallback / layout.NewParagraphFallback surface (#225) is intentionally NOT exposed in v0.8.0 — per its PR description, C ABI exposure is Phase 2 work tied to promoting *EmbeddedFont to an interface (#283)scripts/audit-cabi.sh reports Go //export directives, export/folio.h declarations, and built dylib symbols all in sync at 393. The header is updated in lockstep with each additionpdfa3a / pdfa4 / pdfa4f / pdfa4e — and fills in profile keys the Go API already supported but the playground did not (pdfa1a, pdfa2u, pdfa2a, pdfa3b) (#214):empty pseudo-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)::placeholder pseudo-element (Pseudo-Elements L4) — applies to <input> and <textarea> carrying a placeholder attribute, only when the field has no value (input) or body text (textarea). Supported sub-properties: color, font-style, font-weight, font-size. Numeric font-weight values route through parseFontWeight so font-weight: 700 / bolder resolve to the bold font variant (#215)indicScriptConfig. Existing Devanagari behaviour and tests are preserved. ScriptOf / Script is extended to recognise the six new scripts (Bengali and Tamil were already mapped). Paragraph layout dispatches via indicConfigFor(Script). Gurmukhi and Tamil use rephPosNone so a leading Ra + virama + consonant in those scripts is left as a plain halant-joined cluster (#216)ế), and stacked IPA tone marks. New MarkMarkOffset(mark1GID, mark2GID, feature) accessor; parameter order follows the spec's mark1/mark2 (attaching/receiving) convention (#213)Tj; the second through Nth are bracketed by Td shifts that align the entry anchor onto the previous exit. New CursiveOffset(prevGID, currGID, feature) accessor returning prev.Exit - curr.Entry. LTR-only: RTL handling is gated on the LookupFlag RIGHT_TO_LEFT bit, which is parsed and stored on the CursiveRecord but not yet honoured by the draw path (#218)Present grid. New MarkLigatureOffset(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 in layout/draw.go for the follow-up (#218)counter(page) and counter(pages) resolve in body-flow ::before / ::after content — previously these only worked inside @page margin boxes. Two-pass emission: pagination is decided once, then a second pass draws each page with the final page total available, so counter(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)bookmark-label resolves content(), attr(NAME), and literal strings during HTML conversion (missing attributes fall back to the element's text content); bookmark-level: none excludes an element from the outline (cover pages, suppressed headings); bookmark-state: closed marks the subtree collapsed in viewers via the negative /Count sign convention (ISO 32000 §12.3.3). Non-heading elements with an explicit bookmark-level join the outline through a new BookmarkAnchor wrapper that decorates the first PlacedBlock without 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)calc() / min() / max() / clamp() values — every shorthand below previously used strings.Fields on values that may legally contain functional values, which split on the function's internal whitespace and silently dropped the declaration. A new paren-aware splitTopLevelFields (sibling of splitTopLevelCommas) 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 via parseLengthPt), gap/grid-gap (#249), border-radius (#252), box-shadow (#254), transform-origin (#257), border-spacing (#258), columns (#259), and column-rule (#261). border and column-rule additionally regain rgb() / rgba() / hsl() color parsing (which had been shredded by the same tokenizer). For flex only: percentages inside calc resolve lazily through the new layout.UnitCalc variant against the actual layout area at render time (#236)<html lang> propagated to @font-face TTC face selection — html.ConvertFull now reads the document root's lang attribute and routes it through font.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 to Options.FallbackFontPath and the built-in system-font candidate list (loaded lazily, so the lang is available by the time the loader runs): a doc declaring lang="zh-CN" whose <p> falls back to the CJK system font gets the SC face there too. The extracted tag is also exposed on ConvertResult.Metadata.Language for callers that want to feed it into the PDF catalog's /Lang entry or other downstream consumers. Per-element lang overrides (<p lang="ja">) are NOT yet honoured — @font-face is 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 to ParseFontForLanguage. The same BCP-47 face-selection rules apply; empty lang falls through to face-0 / single-face semantics identical to LoadFontfont-weight ladder + nearest-weight @font-face matching (CSS Fonts L4 §3.1 + §5.2) — pre-fix parseFontWeight returned the binary string "normal"/"bold" and the @font-face resolver looked up faces by that string. Documents declaring four Inter weights at 400/500/600/700 and writing font-weight: 600 silently picked Inter-Regular instead of Inter-SemiBold; same for 500 (Medium) collapsing to Regular. Post-fix computedStyle.FontWeight is int over the 100..900 ladder, parseFontWeight returns the spec ladder (with bolder/lighter resolving relative to the inherited weight), and resolveFontPair does §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, < 400 descending then ascending, > 500 ascending 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.FontWeight is package-internal so this is not a public API break (#286, #287)text-align: start | end direction-relative keywords (CSS Text L4 §7.1) — pre-fix parseTextAlign returned (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 on computedStyle.TextAlignKeyword and a new resolveTextAlign(style) helper late-binds the resolution against style.Direction at consumer time (so source-order interactions like text-align: start; direction: rtl resolve correctly regardless of declaration ordering). Same treatment for text-align-last via resolveTextAlignLast. New public layout.Paragraph.Align() getter exposed for test inspection (#288)text-decoration-line: overline (CSS Text Decoration L4 §3.1) — pre-fix layout.TextDecoration defined Underline (1<<0) and Strikethrough (1<<1) only; the third spec line type was silently dropped at parse time. <p style="text-decoration: overline">x</p> rendered plain. Post-fix DecorationOverline = 1 << 2 joins the bitset, parseTextDecoration ORs it in for the overline keyword (combinations like text-decoration: underline overline line-through produce all three flags), and drawDecorations renders the stroke at baselineY + ascent * 0.95 with text-decoration-style dispatch (solid / double / wavy / dashed / dotted) honoured. The double secondary stroke offsets DOWNWARD (toward text) so both lines stay inside the line box even at line-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)groove | ridge | inset | outset (CSS Backgrounds L3 §4.1) — pre-fix the parser stored the keyword string but buildBorder'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 and buildBorderForSide(side, ...) dispatches per-side via new beveledColor / lightenColor / darkenColor helpers (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)max-width / max-height / min-width / min-height on <img> (CSS 2.1 §10.7) — pre-fix the CSS parser stored these properties on computedStyle but html/converter_image.go::convertImage never threaded them to layout.ImageElement, and ImageElement had no fields to receive them. So a 2000×500 source logo with max-width: 180px in a 540pt-wide page bled the entire header instead of clamping. Post-fix layout.ImageElement gains cssMaxWidth / cssMaxHeight / cssMinWidth / cssMinHeight fields plus SetMaxSize(maxW, maxH) and SetMinSize(minW, minH) setters; resolveSize applies the clamps in spec order (max first, then min, with min winning over max per the conflict rule), aspect-preserving on each axis. The object-fit branch'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 — a min-height that would push the image past the canvas is silently dropped to keep the image inside its containing block, matching browser behaviour. Percentage max-width: 50% still resolves to 0pt because the converter's CSS-to-points conversion passes relativeTo=0; pre-existing limitation tracked separately (#291, #292)/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-position and gradient color stops accept percent-only calc() / min() / max() / clamp() — pre-change the parsers only recognised bare percent / length leaves; expressions like background-position: calc(50% + 10px) 0 or linear-gradient(red, blue calc(50% + 1px)) silently fell back to 0. Post-change parseBgPosition and 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 new layout.ResolvableLength machinery added in #319; gradient-stop calc remains eagerly resolved against the box width at parse time. Followup tests and a splitTopLevelFields doc gap close out in #314, and a parseCalcExpr depth-reset fix between operator scan passes lands in #315 (#266, #312, #314, #315)<p>A</p> rendered as .notdef because 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)font package no longer depends on golang.org/x/image/font/sfnt for metric reads — head, hhea, maxp, hmtx, OS/2, name, and cmap are now parsed directly from raw font bytes via new files font/{head,hhea,maxp,hmtx,os2,name,cmap}.go. Closes #248 (sfnt's hardcoded maxCmapSegments = 20000 blocked 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/Descent follow the OpenType USE_TYPO_METRICS convention (OS/2 fsSelection bit 7): when set, the foundry has explicitly requested the typo metrics, so we use sTypoAscender/sTypoDescender; otherwise we use the hhea ascender/descender. This preserves byte-identical FontDescriptor /Ascent and /Descent output 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 TrueType glyf/loca, so CFF fonts continue to load and embed (raw CFF bytes opaque) but cannot be subset (#260)html asset resolution centralized behind a single resolveLocalAsset contract — <img src>, inline SVG, <link rel="stylesheet" href>, @font-face url(), background-image: url(), and FallbackFontPath previously each carried their own routing logic that disagreed on absolute paths, root-anchored paths, and HTTP origins. They now route through one method documented in ARCHITECTURE.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 through URLPolicy enforcement uniformly, and @font-face url('/abs/system/font.ttf') succeeds with BaseFS: nil (closes the workaround originally proposed in PR #228). FallbackFontPath retains a documented programmatic-only carve-out: an absolute path always bypasses BaseFS to the OS, and a relative path that misses BaseFS retries against the OS — neither extends to document-supplied references because the trust boundary is different (#229)applyProperty switch in html/converter_style.go with one cssProperty entry per CSS property (138 entries total) carrying its own Apply closure plus documentation fields. applyProperty shrinks to 7 lines: pre-dispatch logic preserved verbatim (custom-property -- storage, then var() resolution, then registry dispatch). The same registry is the source of truth for docs/CSS_SUPPORT.md, auto-generated by go generate ./html/.... applyTagDefaults (browser HTML element defaults) is untouched. No public API change; behaviour-preserving across the full suite (#271)<pkg>: / <pkg>: <subdomain>: / <pkg>.<Func>: — top-level errors get <pkg>:, sub-feature errors inside font/ and reader/ 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 of html.ErrURLPolicyDenied from "folio/html: ..." to "html: ..."; the exported var identity is unchanged, so errors.Is(err, html.ErrURLPolicyDenied) still works. Callers that string-compared err.Error() against the old verbatim message — including the folio/html: prefix — must update their assertions (#313)computedStyle margin/padding storage migrates to lazy *cssLength siblings — the eight legacy float64 fields (MarginTop/Right/Bottom/Left, PaddingTop/Right/Bottom/Left) are removed from computedStyle; every read goes through MarginTopAt(width) / PaddingTopAt(width) helpers backed by sibling *cssLength fields. Tag-default setters write &cssLength{Value: N, Unit: "pt"} so heading / paragraph margins are no longer baked-in floats. computedStyle is package-internal so this is not a public API break; the PageConfig.MarginTop field used by page.go is a separate type and stays as float64. A TestHeadingDefaultMarginsSurviveMigration regression 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)preserveAspectRatio slice viewport clip — when an SVG is drawn with slice meet-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).ttc) fonts now load — font.ParseFont and font.LoadFont advertised TTC support but routed the bytes to sfnt.Parse, which rejects collections with invalid 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 for url() references without a # fragment. Very large CJK collections may still hit golang.org/x/image/font/sfnt's hardcoded maxCmapSegments limit — that is an upstream parser limit, orthogonal to TTC dispatch (#227)font.ParseFont no longer falsely advertises PostScript Type 1 (typ1) support — the magic was listed in the dispatch alongside TrueType variants, but ParseTTF routes it to sfnt.Parse which rejects Type 1 with a confusing sfnt: invalid font error. The entry is gone; callers feeding typ1 bytes now receive a clear ErrUnknownFormat. 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. New TestParseFontDispatchSurface audits every magic the dispatch claims to support to prevent the same false-advertisement shape recurring (#230)OriginalText survive paragraph page splits — cloneWithWords's wordToRun helper was dropping Word.InlineBlock (inline image / SVG / Div) so any inline element falling into the overflow half of a paragraph was silently lost, and was copying post-shape Word.Text instead of pre-shape Word.OriginalText so re-laying skipped repopulating OriginalText and broke /ActualText marked-content recovery (ISO 32000-2 §14.9.4) for Arabic. wordToRun now copies InlineBlock → InlineElement and uses OriginalText (when set) as Text; cloneWithWords's sameRun guard rejects merges across inline elements (#241)font: shorthand handles a / inside calc() correctly — the size/line-height detector used strings.IndexByte(sizeStr, '/') which found any / including ones inside calc(2em / 2) and split mid-expression. Replaced with a paren-aware indexByteAtTopLevel helper. Same fix surface as the calc-tokenization series (#270)column-rule width zeroes when column-rule-style is none or hidden — matching CSS Multi-column Layout L1 (same rule as border-style: none). parseBorderFull already implemented this; parseColumnRule did not, so column-rule: 4px none red returned width = 3pt instead of 0pt. hidden is now also accepted as a style keyword (parity with parseBorderFull) (#270)transform() parses calc() / min() / max() arguments correctly — two compounding paren-blind bugs in parseTransform. The outer function-call extractor used strings.Index(val[parenIdx:], ")") and so truncated translate(calc(50% - 10px), 0) mid-calc; the inner argument splitter used strings.ReplaceAll(args, ",", " ") which destructively flattened nested commas (min(10px, 20px) → min(10px 20px)). The outer is now depth-tracking; the inner uses splitTopLevelCommas first with a splitTopLevelFields fallback for the legacy SVG space-separated form. Calc inside rotate / scale / skew is fixed in a follow-up via #284 (#272)parseAngle and parseNumericVal understand calc() / 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. parseAngle gains an angle-native calc evaluator (resolveAngleCalc) that mirrors parseCalcExpr's grammar but reads each leaf as an angle in degrees (or a dimensionless multiplier). parseNumericVal handles min/max/clamp directly and delegates calc to the existing dimensionless-leaf path. parseAngleLeaf's suffix-detection order is fixed in passing — pre-fix 100grad matched the rad suffix before grad and resolved to 0; the new ordering checks turn/grad/rad/deg (#274, #284)parseLineHeight distinguishes dimensionless calc from length-form calc — font: 12px/calc(1.2 * 1.5) sans now resolves to multiplier 1.8 as written. Pre-fix the dimensionless calc result (1.8) was passed through pts := l.toPoints(fontSize, fontSize) and then divided by fontSize=9pt, yielding 0.2 — a 9× compression of line spacing. New helpers cssLength.isDimensionless() and calcExpr.isDimensionless() walk the calc tree and report true iff every leaf is a bare number; parseLineHeight short-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> auto correctly flags both auto-sides — the auxiliary auto-flag scan inside the margin case of applyProperty still used strings.Fields after #237 migrated parseMarginShorthand itself. The two tokenizers diverged on functional values, so margin: calc(10px + 20px) auto correctly resolved top/bottom = 22.5pt and left/right = auto, but only MarginLeftAuto was flagged — MarginRightAuto was missed. Both tokenizers now use splitTopLevelFields (#263)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 with background-color painted 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-all honoured in the second paragraph measurement pass — measureWords was consulted twice during paragraph layout but only the first pass observed word-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)breakLongWords preserves OriginalText and 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-shape OriginalText and the post-shape GIDs slice. The downstream /ActualText marked-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.Entries direct field access — use All, Get, Set, Remove. Direct slice mutation bypasses the lazy key index and can desync the dictionary's lookup table.core.PdfArray.Elements direct field access — use All, At, Len, Add, Set, RemoveAt, Replace.core.PdfIndirectReference.ObjectNumber — use Num for reads and SetNum for writes.core.PdfIndirectReference.GenerationNumber — use Gen.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/cjk is now CI-tested end-to-end — adds examples/cjk/main_test.go that builds the example's HTML against a synthetic TTC fixture (constructed from any system TTF at test time), runs html.ConvertFull and document.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 working msyh.ttc and NotoSansCJK-Regular.ttc paths but neither was ever exercised end-to-end because nothing in CI compiled or ran the example. A second test (TestCJKExampleFindCJKFontReturnsExistingPath) verifies the example's findCJKFont candidate list does not drift away from on-disk reality. Phase 1 of #231; subsequent PRs roll the same pattern out to other examples (#231)cloneWithWords — locks in the dormant-but-correct behaviour: hyphenateWord produces a part-and-rest pair where part.Text ends in - and SpaceAfter=0, and cloneWithWords joins same-style words. A naive join would emit "linguis-tic" instead of the original "linguistic". Today the path is unreachable because wrapWords (which PlanLayout uses) does not call hyphenateWord — only Paragraph.Layout does. 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 wires hyphenateWord into wrapWords, this fails immediately and signals that cloneWithWords needs hyphen-aware join logic before that change can land. Closes the audit chain that produced #241 / #246 (#250)#231 — end-to-end CI tests for the remaining examples/ directory — extends the same pattern Phase 1 of #231 introduced for examples/cjk to the rest of the example tree, so every documented example now compiles and runs in CI rather than rotting silently. New main_test.go files build the example's PDF and assert %PDF- prefix + key invariants per example: examples/html-to-pdf reads back through pdf.Reader and checks rendered text plus image presence (with cached bytes to amortize the build cost across assertions); examples/report builds 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)parseLineHeight dimensionless / length-form calc paths — covers the helper introduced by #275 in isolation rather than only via the font: shorthand integration test. Locks in the cssLength.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.md introduced — auto-generated from the CSS property registry by go 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. TestCSSDocsInSync enforces drift detection between the registry and the on-disk file (#271, #273)docs/CSS_SUPPORT.md extended 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, @page margin boxes, @supports, @media print, plus the silently-ignored set (@media non-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, and url, with related issue numbers (#222, #265, #266, #274, #275) called out for known gaps. Three drift guards: TestAtRulesDocCoverage AST-greps parseCSS for @-prefixed string literals; TestFunctionsDocCoverage and TestSelectorsDocCoverage use static expected lists with behavioural smoke checks against the parsers (#162, #282)Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
export: C ABI bindings for v0.7.0 additions by @carlos7ags in #208
Full Changelog: v0.7.0...v0.7.1
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_optionsfolio_document_set_actual_text — toggles the /Span /ActualText emission 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_balancedFOLIO_DIR_AUTO, FOLIO_DIR_LTR, FOLIO_DIR_RTL preprocessor constants in export/folio.h so C consumers do not encode magic numbers for the direction settersexport/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.
Plumb stop-opacity through layout.GradientStop by @carlos7ags in #146
Full Changelog: v0.6.2...v0.7.0
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.
column-fill: balance (the spec default). Documents using column-count will reflow (#145)kern table 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).notdef boxes; the layout engine selects a fallback face per glyph cluster when one is configured!important cascade at the inline/stylesheet boundary is now honored (#137)/ActualText markers 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 with Document.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.Entries direct field access — use All, Get, Set, Remove. Direct slice mutation bypasses the lazy key indexcore.PdfArray.Elements direct field access — use All, At, Len, Add, Set, RemoveAt, Replacecore.PdfIndirectReference.ObjectNumber — use Num for reads and SetNum for writescore.PdfIndirectReference.GenerationNumber — use Gencore.RevisionRC4128 — RC4 is cryptographically broken; use RevisionAES256 for new documents. The constant remains supported for reading and writing legacy RC4-encrypted PDFs and is not scheduled for removalgolang.org/x/text/unicode/bidi, character-level bidi splitting, RTL list support (markers on right, text indented from right), HTML dir attribute and CSS direction property wiring (#37)rlig, liga), kashida (tatweel) justification, /ActualText markers (default on) for round-trip-safe copy/paste of shaped words (#37, #160, #179, #180)unicode/grapheme package), UAX #24 script segmentation, cluster-aware font.MeasureString (#170, #176, #183)face.Kern consults GPOS first and falls back to the legacy kern table (#175)kern table parser hardening: correct v0 / v1 coverage decode, per-face cache, kerning-aware MeasureString for both Standard and EmbeddedFont (#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.WriteOptions with Writer.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). Implies UseXRefStream. 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 at zlib.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 empty q...Q save/restore pairs and identity 1 0 0 1 0 0 cm operators (§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 existing SetCompress setter; lets writer-side passes skip streams the writer will already deflatecore.DeflateStreamData / core.InflateStreamData — exported zlib codec helpers (formerly the unexported deflate)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 stacktmpl package — html/template integration: parse a template, execute against caller data, feed the result to the HTML converter (#155)@font-face with src: 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)<br> inside <strong>, <em>, <a>, and other inline elements — previously panicked. All 14 inline tags now route line breaks correctly through the TextRun.IsLineBreak field (#147, #150)column-span: all inside multi-column containers — correct break handling at leading, trailing, consecutive, and column-boundary positions (#127)Consumed accounting on page break (#139)stop-opacity plumbed through layout.GradientStop to the rasterizer (#146)var() references in align-items (#128)core package — correctness fixes, hardening, gradual API cleanup; sentinel errors for parser failures (#165)content package — operator validation against ISO 32000 operator set; coverage to 100% on operator emission paths (#166)image package — limits on dimension and component count; fuzz suite for JPEG, PNG, TIFF; CMYK JPEG via Adobe APP14 (#167, #169, #173)font package — sentinel errors, documented concurrency contract, coverage on subset and cmap paths (#168, #173)preserveAspectRatio parsed and honored; barcode fuzz suite (#178)golang.org/x/image 0.38.0 → 0.39.0golang.org/x/net 0.52.0 → 0.53.0golang.org/x/text/unicode/bidi for UAX #9 bidirectional textsoftprops/action-gh-release v2 → v3 (release CI)ARCHITECTURE.md updated with the bidi dependency entry; font/kern.go extracted from truetype.go; GSUB extracted from the Face interface into an optional GSUBProvider; unicode/grapheme extracted as a leaf packageNothing published for this version
Nothing published for this version
Nothing published for this version
Honor column-span: all inside multi-column containers by @carlos7ags in #131
column-span: all inside multi-column containers by @carlos7ags in #131order and align-items via CSS custom properties by @carlos7ags in #138align-items, justify-items, and container height by @carlos7ags in #140Full Changelog: v0.6.1...v0.6.2
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.
<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. Missing width/height fall back to the raster's intrinsic pixel dimensions (#130)linearGradient and radialGradient referenced via fill="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 resolved Stop values, 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 to RegisterGradient (#130)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)<svg> / <img> dropped from paragraph flow — replaced elements inside a <p> inherited display: block from the parent and were silently skipped by the inline collector. They now default to display: inline in applyTagDefaults, matching browser replaced-element behavior. Affects any <p> containing a bare <svg> or <img> without an explicit inline display override (#130)<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) inherited display: block, and walkChildren didn't group consecutive inline siblings into anonymous block boxes per CSS 2.1 §9.2.1.1. walkChildren now 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)align-items, justify-items, and container height — explicit height on a grid container is now honored so align-items / align-content have room to distribute; justify-items values (start/end/center/stretch) are applied; explicit align-content: flex-start no longer triggers row stretching (#129)order property — children are stable-sorted by their CSS order value before layout; ties preserve DOM order per Flexbox spec. align-items now resolves var(--custom-property) references through the cascade (#128)min-width, auto-column widths with cell hints, and border-collapse/border-spacing now survive when a table splits across pages (#134)Heading.PlanLayout allocates per-line boxes so long headings that wrap at narrow widths render cleanly without glyph overlap (#132)column-span: all inside multi-column containers — elements with column-span: all now break the column flow correctly at leading, trailing, consecutive, and column-boundary positions, including when column-rule is set (#127)Consumed accounting on page break — headings that split across pages carry the correct structure tag on continuation and no longer over-advance the available height (#139)Nothing published for this version
Fix nil font panic in paragraph measureWords ( #98 ) by @carlos7ags in #99
Full Changelog: v0.6.0...v0.6.1
aspect-ratio property on Div elements (CSS Sizing Level 4 §5.1) — derives height from width when no explicit height is set; supports 16 / 9, auto 16/9, single number forms (#112)rgb()/hsl() syntax — rgb(255 0 0 / 0.5), percentage alpha, applies to rgba()/hsla() too (#108)html.ParseCSSLength public utility — converts CSS length strings ("1in", "16px", "2em", "50%", calc()) to PDF points (#109)Document.ToBytes convenience method — returns serialized PDF as []byte for HTTP responses, base64 encoding, in-memory processing (#66)drawCellBordersRounded now draws each border side independently with corner arcs, instead of requiring all four borders to be identical (#115)folioRender accepts headerHtml/footerHtml in settings JSON, rendered via SetHeaderElement/SetFooterElement (#102)examples/invoice/) — professional invoice PDF demonstrating rounded table headers, CSS Grid, Flexbox, and optional Tailwind CSS v2runMeasurer — falls back to Helvetica when TextRun has no font set (#98)border-collapse changed from collapse to separate per CSS 2.1 §17.6 — previously prevented cell border-radius from rendering (#114)<table>; added border-spacing: 2px default per browser UA stylesheets (#117)drawRoundedBorders now uses per-corner radii (RoundedRectPerCorner) instead of uniform radius; previously only r[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 framework space-y-* utilities (#101)rem unit parsing — parsePlainLength checked "em" suffix before "rem", so "1rem" failed to parse (#111)border-collapse: collapse mode per CSS Backgrounds Level 3 §5.3 (#100)separate mode by default (previously collapse). Tables without explicit border-collapse: collapse in 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, add table { border-collapse: collapse; } to your CSS.playground.foliopdf.devAdd element-based header/footer API with automatic space reservation by @carlos7ags in #68
Full Changelog: v0.5.2...v0.6.0
Breaking changes — see MIGRATING.md for upgrade steps.
New*/Load*/Parse* across reader, barcode, layout, sign, formssign.LoadPKCS12 renamed to sign.ParsePKCS12 (same signature, name-only change)Document.Page(index) returns (*Page, error) instead of panickingreader and svgvertical-align accepts length/percentage values (previously ignored)SetHeaderElement, SetFooterElement, SetHeaderText, SetFooterTextAddHTMLTemplate / AddHTMLTemplateFuncs for Go template → PDFValidatePdfA for early PDF/A validationWithBackgroundColor, <mark> in HTML)<img>, <svg>, display:inline-block)<sub> and <sup> rendering with baseline shift and correct spacingbaseline-shift CSS property (keywords and lengths/percentages)vertical-align extended with length/percentage values per CSS 2.1\n\n in paragraphsRunInline for inline layout elements within paragraphstext-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, multiple box-shadow<sub>/<sup> baseline shift — previously only reduced font size (#86)</sup>/</sub> keeps correct styling (#86)\n\n produce visible empty lines (#91)(lineH + ascent - descent) / 2 (#90)cloneWithWords preserves all Word styling + line breaks on page-split paragraphsbuildParagraphFromRuns always uses NewStyledParagraph (was dropping BaselineShift/BackgroundColor)golang.org/x/image v0.37.0 → v0.38.0html/converter.go split into focused modulesARCHITECTURE.md with design principles, layering rules, naming conventionsNothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
PDF redaction — RedactText, RedactPattern, RedactRegions permanently remove text from content streams with character-level TJ splitting precision; con
RedactText, RedactPattern, RedactRegions permanently remove text from content streams with character-level TJ splitting precision; configurable fill color, overlay text, and metadata stripping (#59)Page.ImportPage and Page.ImportPageWithOpts load existing PDF pages as Form XObjects (ISO 32000 §8.10) for template workflows; reader.ExtractPageImport convenience API with full indirect-ref resolution (#47)Page.AddLine, Page.AddRect, Page.AddRectFilled for low-level graphics on pages\n line breaks — \n, \r\n, and \r now produce forced line breaks in paragraphs and table cells (#61, #63)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)resolveDeep recursively resolves all indirect references in imported resources; hoistStreams converts nested PdfStream objects to indirect refs, fixing blank/partial output with real-world PDFs\n collapsed to space — splitWords now splits on newlines first and inserts forced line break markersNothing published for this version
Nothing published for this version
Release workflow — replaced deprecated macos-13 runner with macos-latest for x86_64 builds
macos-13 runner with macos-latest for x86_64 builds-fuzz='^FuzzParse$' to avoid matching FuzzParsePDFMarc Ole Bulling — nil pointer fix
<br> nil pointer fix (#10)folio_barcode_qr, qr_ecc, code128, ean13 + layout elementsfolio_svg_parse, parse_bytes + layout elements with size/alignfolio_form_filler_new, set_value, set_checkbox, field_names, get_valuefolio_form_create_text_field, create_checkbox + set_value, set_read_only, set_required, set_background_color, set_border_color, then add_fieldfolio_document_set_watermark and set_watermark_configfolio_document_add_outline, add_outline_xyz, outline_add_childfolio_document_add_named_destfolio_document_set_viewer_preferencesfolio_document_add_page_labelfolio_document_attach_file for PDF/A-3b compliancefolio_document_add_html and add_html_with_optionsfolio_document_set_first_margins, set_left_margins, set_right_marginsfolio_document_add_absolutehelvetica_oblique, helvetica_bold_oblique, times_italic, times_bold_italic, courier_bold, courier_oblique, courier_bold_oblique, symbol, zapf_dingbatsscripts/audit-cabi.sh) — detects drift between Go exports, folio.h, and built symbols-ldflags "-X main.version=..." — folio_version() returns the git tag in releases, git describe in dev buildsaudit, 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)/AF, /Names, MIME types (#17):root selector — now correctly matches the <html> elementcounter(page) and counter(pages) now work in margin boxesMarginBoxes/FirstMarginBoxes on ConvertResult for simpler programmatic access (#54)Nothing published for this version
Clickable links in PDFs — inside paragraphs, headings, and list items now produce PDF link annotations (#23, #26, #27)
<a href="..."> inside paragraphs, headings, and list items now produce PDF link annotations (#23, #26, #27)layout.NewInternalLink resolves to direct page references for macOS Preview compatibilityTextRun.WithLinkURI() and WithDecoration() for building linked text programmatically; List.AddItemRuns() for linked list itemsexamples/links/) showcasing external, inline, multi-line, styled, heading, and list item links plus bookmarks and internal navigationexamples/fonts/) demonstrating custom @font-face with Unicode (CJK, Cyrillic, Japanese)@font-face family names ignored — parseFontFamily was mapping all names to standard fonts; now preserves custom names for embedded font matching (#16)page-break-after ignored when body has width: 100% — AreaBreak elements trapped inside Div wrappers are now hoisted out so the renderer can act on them (#21).myClass now matches class="myClass" regardless of case (#28)<b>word</b>.) no longer gets an extra inter-word space (#25)@font-face family name case mismatch — font-face names are now lowercased consistently so CSS lookup matcheshtml/converter.go into 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
layout, html, svg, and consolidated in core (had two conflicting comments)ARCHITECTURE.md documenting design principles, package responsibilities, layering rules, dependency policy, and non-goalsdss.Build return description, PdfObjects → BuildObjects, merged interface docs in layout, and otherscmd/folio printUsage referenced nonexistent "region" extraction strategysvg/doc.go listed nonexistent clipPath support, missing radialGradientlayout/doc.go referenced wrong type names (Tab → TabbedLine, nonexistent Transform element)font/standard.go package doc was stale ("and later font parsing" — parsing is fully implemented)folio.wasm binary (7.2MB) from the repository; the release workflow already builds it fresh per tagged version*.wasm to .gitignoreWOFF1 font decoding — @font-face now supports .woff files via automatic format detection (font.LoadFont)
@font-face now supports .woff files via automatic format detection (font.LoadFont)--name: value declarations with var(--name, fallback) resolution, inheritance, and nestingcounter-reset, counter-increment, counter() and counters() in ::before/::after contentclear property — clear: left/right/both advances past active floats before placing elementsborder-spacing — horizontal and vertical cell spacing for tables in separate border modelbackground-image: url('https://...') fetches remote images in non-WASM buildsdisplay: inline-block elements flow within paragraphs as "big words" with correct line-breaking and height expansionposition: absolute resolves against nearest positioned ancestor via overlay children, not just the pageexport/ package for FFI from Python, Ruby, Swift, etc.font.LoadTTF calls in HTML converter replaced with font.LoadFont (auto-detects TTF/OTF/WOFF)hyphenateWord() uses pattern-based breaks first, falls back to character splittingCSS Grid layout — display: grid with grid-template-columns, grid-template-rows, grid-template-areas, named areas, auto-rows, alignment (justify-items,
display: grid with grid-template-columns, grid-template-rows, grid-template-areas, named areas, auto-rows, alignment (justify-items, align-items), and page break supportposition: absolute elements ordered by z-indexmargin-left: auto right-alignment for inline-block SVGs in flex containerswidthUnit and percentage were setAuto-height pages via CSS @page { size: 80mm 0; } — page height sizes to content (receipts, flyers)
@page { size: 80mm 0; } — page height sizes to content (receipts, flyers)margin: -10px -14px to break out of parent paddingmargin-left: auto on flex row items — pushes items to the right edge (e.g., seat box alignment)margin-top: auto on flex column items — pushes items to the bottom (e.g., footer positioning)flex-grow — items with flex: 1 now grow to fill remaining space in column directionjustify-content — space-between, center, flex-end, space-around, space-evenly in column directionhasDefiniteCrossSize flag on Flex — enables stretch when Flex is wrapped in a height-constrained Divwatermark parameterbox-sizing: border-box no longer double-subtracts padding from width/height — only border is subtracted since the Div handles padding internallyletter-spacing in width measurement — Paragraph.MinWidth() and MaxWidth() now include letter-spacing, preventing flex items from being measured too narrowmargin-top: auto — added 0.01pt epsilon tolerance to prevent items from silently overflowing due to float roundingmargin-top: auto phase consistency — neededBelow calculation now includes marginBottom of subsequent items in both phase 1 and phase 3min-height backgrounds now fill the full height (kept on both Div wrapper and inner Flex)parseFloat negative numbers — CSS parser now correctly handles negative values like -10px<br> tags in paragraphs and CSS width as flex-basisnet/http from js buildsplanColumn refactored into 3-phase layout: measure, grow, positionNothing published for this version
CLI extract command with pluggable strategies (simple, location)
extract command with pluggable strategies (simple, location)sign command for PAdES digital signaturesSetBorderCollapse(true) for CSS-style collapsed borderscalc() support in HTML-to-PDF (e.g., width: calc(100% - 40px))@page rule parsing (size, margins) in HTML-to-PDForphans/widows properties in HTML-to-PDFbreak-before/break-after/break-inside modern syntax<img src="https://...">) in HTML-to-PDF<img src="data:image/png;base64,...">)<title> and <meta> tagsPDF generation with layout engine (Paragraph, Heading, Table, List, Div, Image, Float, Flex, Columns)
Your coding agent can read these notes before it upgrades. Set up the MCP server →