Skip to main content

Layout snapshot

A debug render measures the document after the browser has laid it out for print, and writes the measurements as two files in the bundle:

FileHolds
snapshot.meta.jsonone object: the page geometry and how it was arrived at
snapshot.jsonlone JSON object per line: the elements, in document order

Both are written whenever --artifacts asks for snapshot. The measurement itself happens on every debug render regardless, because the built-in layout rules read it — tying capture to the selector would let --artifacts pdf report "no problems" about a document nothing ever checked.

This page is the field reference. For what the rules make of these numbers see Reading a render's problems; for writing your own rules against them see Layout rules.

Units

Two coordinate systems are in play, and the snapshot publishes the conversion rather than leaving a consumer to hardcode it:

  • Element boxes are CSS px, exactly as the browser measured them — never scaled, never rounded to the paper.
  • Paper and the content box are pt (1/72 inch), the PDF's own unit.
"units": { "element_boxes": "css_px", "paper": "pt", "pt_per_css_px": 0.75 }

pt_per_css_px is 0.75 × scale. It is 0.75 for an unscaled render and moves with the print scale parameter — a document printed at scale: 2 publishes 1.5. Divide a pt measurement by it to get CSS px, which is how the content box below becomes the width every by_px was measured against.

snapshot.meta.json

{
"schema_version": 2,
"template": "layout_problems",
"example": "overflow_boxes",
"pages": 3,
"units": { "element_boxes": "css_px", "paper": "pt", "pt_per_css_px": 0.75 },
"paper": { "width_pt": 576, "height_pt": 720, "scale": 1 },
"content_box_pt": { "x": 36, "y": 36, "width": 504, "height": 648 },
"page_breaks_css_px": [864, 1728],
"element_count": 7,
"elements_truncated": false,
"elements_dropped": 0,
"browser": { "product": "Chrome/151.0.0.0", "user_agent": "Mozilla/5.0 …" },
"page_model": "geometric"
}
FieldMeaning
policyThe resolved diagnostics policy this snapshot was measured and judged under: every kind's severity, the bands of the rules that grade a measurement, and the bounds. Without it a report is not reproducible — an absent finding means either the document was fine or the rule was switched off.
schema_versionThe shape of these two files. A rule declares the version it was written against, and one declaring another version does not run — see the version gate.
templateThe template that was rendered.
exampleThe data set: an example name, or data-<hash> for a body render.
pagesPage count. Reconciled against the printed PDF where that could be read — see pages_approximate.
unitsThe coordinate systems, above.
paperThe resolved page size in pt, and the print scale.
content_box_ptThe printable area inside the paper: x/y are the top and left margins, width/height what is left for content.
page_breaks_css_pxWhere each page after the first starts, in flow coordinates. Empty for a single-page document.
element_countHow many elements snapshot.jsonl holds.
elements_truncatedtrue when the element cap dropped boxes that would otherwise be listed. Truncation is never silent: it is also reported as a diagnostics-capped problem.
elements_droppedHow many measured boxes the cap left out — what the truncation cost, where elements_truncated says only that it happened. 0 when nothing was dropped.
browserThe engine that took the measurement, from the DevTools version.
page_modelHow pages was arrived at — below.
pages_approximatePresent and true when the printed page count disagreed with the measured geometry — below.

page_model

The page count is a measurement of the document, not of the PDF, and this field says which of three ways produced it:

ValueMeans
geometricPages were stepped off the content height. The ordinary case.
forced-breaksThe document asked for at least one break itself (break-before: page and friends), and the page starts follow it.
css-page-sizeThe render was made with preferCSSPageSize, so a CSS @page size can override the paper the print parameters named.

pages_approximate

Elements are assigned to pages from geometry, before the PDF exists. When the printed document turns out to have a different number of pages, the assignment is indicative rather than measured, and this flag says so.

The common cause is content wider than the page: Chromium shrinks a document that does not fit its width, which moves every page boundary. When the flag is set, the page-level rules stand down — a page nothing was assigned to means the two models disagreed about pagination, not that the page is blank.

elements_truncated has the same effect for the same reason: the elements that would have claimed a page may be the ones the cap dropped. Unlike an approximate assignment, which the printed page count contradicts on its own, a cut table leaves nothing behind to notice it by — so it is reported as a problem of its own, diagnostics-capped with cap: elements, and a render carrying one never prints ok no problems.

snapshot.jsonl

One object per line, in document order:

{"id":2,"sel":"div.wide","tag":"div","classes":["wide","block"],"text":"Wider than the content box","box":{"x":0,"y":0,"w":712,"h":32},"pages":[1],"overflow":{"x":40,"y":0},"clipped":false,"break_before":false,"break_after":false,"unsplittable":false,"font_size_px":16,"parent":1}
FieldAlwaysMeaning
idyes1-based position in this file, so a consumer can refer to a row.
selyesA CSS selector that resolves to this element alone where one could be built — anchored on a unique class where possible, so it survives small edits.
tagyesLowercased tag name.
classesyesThe class list as written, in source order. Empty array when there is none.
textyesText to find the element by: its own, whitespace-collapsed and cut to 80 characters (the last three being ...). A box with no text of its own — a container, a wrapper — is anchored by the text of everything inside it instead, so grepping the value in your template source lands somewhere either way. Empty only where there is no text at all.
boxyesThe border box in CSS px: x, y in flow coordinates, w, h its size. Negative coordinates are kept, not clamped.
pagesyesEvery page the box covers, ascending. A single page for a box the browser keeps whole.
overflowyesHow far the box reaches past the content box on the right (x) and past its own page bottom (y), in CSS px; 0 when it does not.
clippedyestrue when the browser reports the element's own content overflowing a box that hides it — part of it was never painted.
srcnoFor a replaced element, the source it tried to load. Omitted when there is none to read.
break_before, break_afteryestrue when the document's own style asks for a page break before or after this element (break-before / break-after, in either spelling). A break the browser produced by running out of page is not one of these.
unsplittableyestrue when the author declared this box must not be broken — break-inside: avoid in either spelling, or a replaced element. This is the author's own declaration, read from computed style: a rule checking it does not have to guess from a class name.
font_size_pxyesThe computed font size of this element's own text, in CSS px.
parentnoThe id of the nearest ancestor that is itself in this file. Omitted for an element with none. The walker drops insignificant nodes, so this is rarely the DOM parent — it is the nearest box a reader can act on, which is what makes "is anything else inside this block" a lookup rather than a box comparison.

Which elements are listed

The walker keeps the boxes a reader could act on, not every node:

  • an element with text of its own, a visible border, or a background;
  • a replaced element (img, svg, canvas);
  • anything the author marked unbreakable (break-inside: avoid);
  • a box whose author named it: an element with children, carrying a class none of its children carry — whatever CSS is or is not on it (below);
  • anything already out of place — clipped, outside the content box, or a replaced element with no area — which is kept whatever else it is.

Everything else is dropped: hidden elements, boxes with no area, redundant wrappers (below), and boxes smaller than a few px in either direction. A spacer with no border and no text is not an element however it is classed — naming keeps a container, not a leaf — which is exactly why the page it covers can be reported as empty.

The box you named is a box you can select

A class a container's children do not carry is the author naming that box, and naming is how every rule selects one. So a container that carries one is listed on that ground alone — with no border, no background and no text of its own, and with nothing else keeping it in.

That is what makes a rule trustworthy. A rule selecting an element the walker dropped matches nothing and reports nothing, which is exactly what a rule that ran and found the document healthy reports. <div class="totals"><table> is listed and <div><table class="totals"> is not, because in the second the author named the table, not the wrapper — and the table is listed.

It reads the children, not the descendants: a <tr class="grand"> whose cells carry amount and label is named, while a <div class="row"> wrapping a <span class="row"> is not — the name is already on a box in the table.

The same fact folds a wrapper away. A node with one element child, no text of its own, and a box matching that child's to within half a px says the same thing the child says, so only the child is listed — unless the wrapper names something the child does not, or carries break-inside: avoid (in either spelling), which is the exact element a break rule has to name.

The table is capped at 400 rows. Out-of-place boxes are kept first and the rest of the budget is filled in document order, so the cap spends ordinary boxes before it spends a finding; elements_truncated records that it fired and elements_dropped what it cost. The flag also silences the page-level rules — see pages_approximate — which is why the cap reports itself as a problem rather than leaving a quiet report to be read as a clean one.

Raise it for a document that needs it, per template, in params.json: {"diagnostics": {"capture": {"max_elements": 1200}}}. It costs render time rather than memory — a selector is built per listed element, and each costs several document-wide queries — and an operator running under --diagnostics-template-policy=clamp may refuse the raise, which is reported as policy-clamped rather than passed over.

overflow.y is not "crosses a page break"

A box that flows across a page boundary is ordinary pagination, not a defect, and reports overflow.y: 0. Only a box the browser cannot break — one the author marked break-inside: avoid, or a replaced element — reports how far it hangs past its page bottom. In practice this means an unsplittable block that reports break-split always reports overflow-y too: a block only splits when it is taller than the page it started on.

What the snapshot does not see

The measurement covers the main document only. Elements inside a shadow root, inside an iframe, or positioned fixed (which repeats on every printed page but is assigned to one page here) are not represented. A print header or footer is a separate document to Chromium and is not measured at all.