Skip to content

GraphCompose v2.2.2

Latest

Choose a tag to compare

@github-actions github-actions released this 26 Aug 23:48

v2.2.2 — 2026-08-27

Public API

  • The layout snapshot can now say what the text became. It could already say that a
    block moved; it could never say why the block is the size it is, because the thing that
    decides that — which font, at what size, broken into how many lines — was measured
    during layout and then discarded. A wrong font and a wrong padding produce the same
    symptom on the page, and telling them apart by eye is exactly the guessing a measured
    snapshot exists to end.

    It is opt-in, and LayoutSnapshot did not change shape. A diagnostic section that
    appeared on its own would turn every consumer's snapshot suite red on an upgrade that
    moved nothing in their document:

    LayoutSnapshot plain = document.layoutSnapshot();               // exactly as before
    
    LayoutDiagnosticSnapshot rich = document.layoutSnapshot(
            LayoutSnapshotOptions.builder().typography(true).build());
    
    rich.layout().equals(plain);                                    // true

    The diagnostics live on a new LayoutDiagnosticSnapshot that wraps the layout
    snapshot rather than on LayoutSnapshot itself. That distinction is the guarantee:
    LayoutSnapshot still has exactly the four components it had in 2.0, so its JSON, its
    toString() and its equals are unchanged however you serialize it — through
    LayoutSnapshotJson, through an ObjectMapper of your own, or by hand. Every committed
    baseline in this repo is unchanged, and nothing added here can reach one of yours.

    LayoutDiagnosticSnapshot.formatVersion versions the envelope independently of the
    layout snapshot's 2.0, so a section added later moves one number and not the other.
    LayoutSnapshotOptions is a builder rather than an overload so that next section costs
    a method rather than a new layoutSnapshot(...) signature.

    LayoutDiagnosticSnapshot.typography() is a list of LayoutTypographySnapshot, one
    entry per resolved paragraph fragment: the declared font, the resolved family, the
    decoration, the size, the line count, the bounds of the laid-out line boxes, and a
    LayoutTextLineSnapshot per line carrying its own bounds and baseline in absolute page
    coordinates.

    It hangs off fragments rather than nodes because that is what text is — a paragraph
    broken across a page boundary has one fragment per page, each with its own lines, and a
    per-node projection would have to keep one and discard the other. Join it to
    layout().nodes() on path, one-to-many. Entries are ordered by path, then page, then
    emission ordinal:
    a split paragraph restarts its ordinal at zero on each page, so page has to be in the
    key or the order falls back to whatever order pagination emitted fragments in.

    declaredFont, resolvedFamily and decoration are three fields because the face
    needs all three.
    A standard-14 face such as HELVETICA_BOLD is an alias of its family
    and contributes nothing on its own — the face comes from the decoration — so a style
    that names the bold face and sets no decoration renders regular, silently.
    fontSubstituted reports that, and only that: naming the bold face and asking for
    bold draws exactly what it named and is not flagged. Reporting the family alone could
    not tell those two apart, nor Helvetica + DEFAULT from Helvetica + BOLD. The family
    rule is reachable as FontLibrary.resolveFamily(FontName), pure and silent so a
    diagnostic pass emits no warnings of its own. A font that is neither registered nor
    aliased never reaches the snapshot: measurement fails first, loudly.

    resolvedFamily, decoration and fontSize describe the text the engine actually
    measured — after an autoSize shrink, and after a span-level override — so the reported
    size always matches the line boxes beside it. declaredFont stays what the paragraph
    asked for.

    The limits, stated rather than implied. A paragraph using a non-default
    TextVerticalAlign has its glyphs shifted by a correction read from the backend font's
    cap height, which nothing renderer-neutral can compute; those lines carry
    baselineExact = false, and because the shift moves the glyphs and not the line box the
    whole entry is positional there rather than a bound on painted output. The bounds are
    laid-out line boxes, not tight glyph ink — a code chip's fill extends past them by its
    own padding. Text drawn outside the paragraph pipeline, such as a table cell written as
    a plain string, produces no entry, so an empty list means "no paragraph text" rather
    than "no text". Coordinates are the laid-out ones, so a transformed or clipped container
    is not reflected. A paragraph whose spans mix fonts is described by its first span.

    The line's own text is deliberately not included. A snapshot excludes raw text payload,
    the words are already in the document that produced it, and a line is identified by its
    index within the fragment.

    The vertical line walk moved into ParagraphLineGeometry (contentTop, nextLineTop,
    baselineY) and the PDF handler now draws through it, so the snapshot and the page
    cannot describe different lines. That helper already existed for the horizontal half,
    for exactly this reason.

    No rendered output changed, and no layout, pagination or render behaviour changed.