This document describes typograph.validate(): one call that returns everything the library can
see wrong with the open document, so a host can show it to a user before they order.
It is a canvas method on window.typograph, so it works in the viewer bundle as well as the
editor — which is where a pre-order check usually lives.
Source of truth:
validate()in the canvas package atsrc/canvas.ts; the finding shape isdocument_findinginsrc/typograph_document.ts.
const findings = typograph.validate();
const blocking = findings.filter(f => f.severity === 'error');
if (blocking.length) showProblems(blocking);
Ask one question only:
typograph.validate({ checks: ['text_overflow'] });
{
severity: 'error', // 'error' | 'warning'
check: 'text_overflow',
message: 'Text does not fit in "Body copy"',
container_index: 5, // which spread
record_index: 1, // which page of it
element_uuid: 'a3f1…', // the element at fault
detail: { … } // check-specific, see below
}
container_index / record_index address a page the same way
PageSelection.md describes, so a finding can be turned into "go to this
page" without searching. Both are absent on a document-wide finding (font_missing), and
element_uuid is absent where no single element is at fault (swatch_missing).
Findings come back in document order — spread by spread, elements as they sit — then the document-wide ones.
check |
Severity | Reports |
|---|---|---|
text_overflow |
error |
the last frame of a text chain does not fit its text |
image_no_link |
error |
an image frame that names no image at all |
image_not_in_manifest |
warning |
the link is not in manifest.images; detail.link |
image_load_failed |
error |
the browser could not fetch the image; detail.url |
image_is_placeholder |
warning |
the image resolved — to a placeholder; detail.placeholder_in |
image_low_resolution |
warning or error |
the image prints below 300 dpi; error below 150; detail.dpi |
swatch_missing |
error |
a colour used on a spread is not in the document; detail.swatch_uuid |
font_missing |
warning |
a font had to be substituted; detail.kind / family / variant |
element_off_page |
warning |
an element entirely outside the paper and its bleed |
empty_text_frame |
warning |
a text frame holding no text |
autofit_exhausted |
warning |
every allowed adjustment was applied and the text still does not fit; detail.spent |
autofit_not_applied |
warning |
the frame carries Fit text rules the canvas will not apply; detail.reason |
role_missing |
warning |
the element is bound to a permission role this document does not have — its own permissions apply; detail.role_uuid. See EditPermissions.md |
severity is a suggestion, not a policyThe library reports facts and proposes a severity; what blocks an order is the host's decision.
Overflowing text may be fatal for one product and acceptable for another; a substituted font may
be worth a warning or nothing at all. This is the same split as
typograph_document_changed / is_changed() for saving: the library says
what happened, the host owns the policy.
text_overflow is asked of the last frame of each chain, once per story. Linked frames
re-derive their text from the chain root, so the last one is where text runs out of room; asking
every frame would report one story several times.
image_not_in_manifest is a warning, not an error. A link that is not in the manifest can
still resolve — the canvas builds a URL by convention, and a link that is itself a URL resolves
directly. It is reported because manifest.images is what the PDF publisher looks an image up
in, so print may find nothing where the screen looks fine.
An image frame with no link is reported once. It is not also reported as missing from the manifest: two findings about one absent thing is one too many.
image_is_placeholder is the one to watch before an order. A placeholder passes every other
image check — it has a link, the link is in the document's image list, and it loads, because it is
a real picture from a real service. So without this check a document reads as clean while every
picture on the page is a grey box with a filename across it.
Placeholders arrive from three places: a conversion whose image link was broken before the package was made, a conversion run without images, and a POD document, which carries image keys and never URLs — so every POD photo block starts as a placeholder. It is a warning rather than an error because that is the intended state of a template nobody has filled in yet; whether an unfilled slot blocks an order is your decision.
detail.placeholder_in lists where it was found — element, element_print, manifest_editor,
manifest_print, manifest_thumbnail. The manifest's print URL is the one that reaches paper, so
a placeholder there is the most serious: the screen can show artwork while print shows the grey
box, and vice versa.
image_low_resolution is the only check whose severity is not fixed. It is one question — is
this sharp enough to print — with two degrees, and 149 dpi is a different conversation from 299. The
figure is effective resolution: the artwork's own pixels spread over the space it is printed in.
⚠️ It is measured against the picture's rect, not the frame's. The frame crops. A 300 dpi photograph cropped to a stamp is still 300 dpi, and judging it by the frame would call it several thousand and wave through artwork that is genuinely soft.
⚠️ The pixel count comes from manifest.images[].size — the original's own pixels — never from the
copy the editor loaded, which is a web-sized version. detail carries dpi, dpi_x, dpi_y,
pixels, placed_mm, and the two thresholds so a host need not hard-code them.
Three kinds of image are not reported at all, and each for its own reason:
image_is_placeholder is the finding that belongs there;image_not_in_manifest.The same figure is shown in the editor's parameter bar beside the frame's name, red below 150, using the same thresholds — so a panel and an order cannot disagree about where the line is.
The two autofit_* checks exist because a fit is derived. Nothing about it is stored, so
nothing about it can be read off a saved document. A host asking "is this fit to print?" has no other
way to learn that the type it is about to buy is 62% of the size the designer set.
Fit text is a per-frame rule — an ordered list of adjustments the designer allows (break words,
smaller type, tighter lines, tighter letters, tighter spaces) which the canvas applies, in order, only
as far as it must. The result is recomputed whenever the text or the box changes and is never written
into the document, so the authored size stays the authored size. See
TextFit.md.
autofit_exhausted deliberately doubles up with text_overflow, which is an error on the same
element. That is the point rather than a duplicate: overflow says the text does not fit, and this says
every allowed adjustment was applied first — so raise the allowance and the box is too small are
different fixes, made by different people. detail.spent names only the levers that actually moved
(point_size_factor, effective_point_size, leading_factor, letter_spacing_em, word_spacing_em,
hyphenation), and detail.steps echoes the authored rule.
autofit_not_applied is the one that is otherwise invisible. A frame linked into a text chain and
a frame set to grow to fit its text are both left unfitted by design, so a designer who set a rule on
one sees no fit, no explanation and no complaint — the rule silently does nothing. Everything else
validate() reports can be seen on the page if you look; this cannot. detail.reason is
text_chain or auto_size.
⚠️ Neither reports a fit that simply worked. A template designed to be fitted would otherwise report on every frame every time, and a check that fires on correct documents teaches a host to ignore it — which would cost the two states above their audience.
element_off_page reports only elements entirely outside the paper and bleed. An element
overhanging the edge is normal and usually required — that is what bleed is for — so overhang is
not reported. Per-side bleed is honoured.
empty_text_frame treats whitespace as empty, because a placeholder frame carrying a stray
space is exactly the case worth reporting: it looks filled in the document JSON and prints
nothing. A frame holding a table is not empty — the table is content nobody has typed in yet.
swatch_missing is reported per spread, with no element_uuid. A colour reference can live in
a character run inside a story that spans several frames, so the story — not any one frame — owns
it; the spread is the honest unit. detail.swatch_uuid names the missing colour.
Nothing is composed. A frame's overflow depends on its laid-out text, so answering it outside
the canvas would mean laying the text out — which is exactly the cost the library avoids
elsewhere. The open document is already composed: every frame on every spread has been through
the composer in order to be drawn. validate() reads that and walks elements.
So it is safe to call on a change, on a button press, or before a save. It is a read: calling it twice returns the same thing and changes nothing.
⚠️ The one consequence of reading composed state: an element added programmatically and not yet drawn has no layout, and a text frame in that state will answer that its text fits. In an interactive editor a draw has always happened; a host building a document by API should let a redraw happen before validating.
This holds for the new checks too. autofit_exhausted reads the overflow the frame already has —
the fit was computed when the text or the box last changed, not here — and image_low_resolution
is arithmetic on two numbers the document already carries.
It does not replay get_load_errors(). That list records what went wrong while opening the
document, is append-only and is never cleared by editing — so replaying it would keep reporting a
font or an image that has since been replaced. Every finding from validate() is re-derived from
the document as it stands now.
get_load_errors() is still there and still useful for "what happened when this file was opened".
The two answer different questions.
function describe(f) {
const where = f.container_index === undefined
? ''
: ` (spread ${f.container_index + 1}, page ${f.record_index + 1})`;
return `${f.message}${where}`;
}
document.getElementById('order').addEventListener('click', (e) => {
const findings = typograph.validate();
const errors = findings.filter(f => f.severity === 'error');
const warnings = findings.filter(f => f.severity === 'warning');
// This host's policy: text that does not fit and missing images stop an order;
// a substituted font, an empty frame or an unfilled image slot is worth showing, not blocking.
// A shop selling personalised products might well reverse that last one — a placeholder that
// reaches print is a complaint — which is why the library proposes and you decide.
//
// Resolution arrives already graded: below 150 dpi is proposed as an error and below 300 as a
// warning, so a host that prints large format might raise both and one printing business cards
// might drop the warning entirely. `f.detail.warn_below` / `error_below` say what was applied.
if (errors.length) {
e.preventDefault();
showBlockingDialog(errors.map(describe));
return;
}
if (warnings.length) showAdvisoryList(warnings.map(describe));
});
// A product where overflowing text is the only thing that matters.
const overflow = typograph.validate({ checks: ['text_overflow'] });
overflow.forEach(f => markPageProblem(f.container_index, f.record_index));
typograph.validate() |
every check, document order |
typograph.validate({checks:[…]}) |
only those checks |
| Returns | document_finding[] — severity, check, message, container_index?, record_index?, element_uuid?, detail? |
| Checks | 12: four about text, five about images, plus colours, fonts and geometry |
| Cost | a walk over elements; composes nothing |
| Decides policy | no — the host does |