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:
| File | Holds |
|---|---|
snapshot.meta.json | one object: the page geometry and how it was arrived at |
snapshot.jsonl | one 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"
}
| Field | Meaning |
|---|---|
policy | The 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_version | The 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. |
template | The template that was rendered. |
example | The data set: an example name, or data-<hash> for a body render. |
pages | Page count. Reconciled against the printed PDF where that could be read — see pages_approximate. |
units | The coordinate systems, above. |
paper | The resolved page size in pt, and the print scale. |
content_box_pt | The printable area inside the paper: x/y are the top and left margins, width/height what is left for content. |
page_breaks_css_px | Where each page after the first starts, in flow coordinates. Empty for a single-page document. |
element_count | How many elements snapshot.jsonl holds. |
elements_truncated | true 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_dropped | How many measured boxes the cap left out — what the truncation cost, where elements_truncated says only that it happened. 0 when nothing was dropped. |
browser | The engine that took the measurement, from the DevTools version. |
page_model | How pages was arrived at — below. |
pages_approximate | Present 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:
| Value | Means |
|---|---|
geometric | Pages were stepped off the content height. The ordinary case. |
forced-breaks | The document asked for at least one break itself (break-before: page and friends), and the page starts follow it. |
css-page-size | The 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}
| Field | Always | Meaning |
|---|---|---|
id | yes | 1-based position in this file, so a consumer can refer to a row. |
sel | yes | A 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. |
tag | yes | Lowercased tag name. |
classes | yes | The class list as written, in source order. Empty array when there is none. |
text | yes | Text 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. |
box | yes | The border box in CSS px: x, y in flow coordinates, w, h its size. Negative coordinates are kept, not clamped. |
pages | yes | Every page the box covers, ascending. A single page for a box the browser keeps whole. |
overflow | yes | How 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. |
clipped | yes | true when the browser reports the element's own content overflowing a box that hides it — part of it was never painted. |
src | no | For a replaced element, the source it tried to load. Omitted when there is none to read. |
break_before, break_after | yes | true 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. |
unsplittable | yes | true 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_px | yes | The computed font size of this element's own text, in CSS px. |
parent | no | The 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.
Related
- Reading a render's problems — what the rules report from these numbers.
- Layout rules — writing your own rules against this document.
- Debugging templates — where the snapshot sits in the debugging loop.