packet gate: look inside the PDF it was already making
Some checks are pending
build-gate / build (push) Waiting to run

check-packet-render has called page.pdf() for a long time and then read only
MediaBox and /Type /Page. Eight blank sheets of the right size would have passed
every assertion in it.

That matters because the packet exists to be PRINTED — it is a standalone file
with photos and fonts inlined precisely so it prints offline — and until now
every check was on-screen. A print-only CSS rule can strip content that the
on-screen checks still see.

The first mutation is the argument for the whole change. Adding
`@media print{.card img{display:none}}` left the on-screen line reading
"95 photos, 0 typographic" — every pre-existing check green — while the PDF
carried 2 images instead of 85. The second, hiding sheet contents in print,
fires all three new assertions at once on a packet that is still 8 correctly
sized pages.

Images are compared against DISTINCT srcs, not cards. A PDF de-duplicates
identical images, and laos has 95 cards and 95 <img> but only 85 distinct
sources, because ten groups share a portrait — a fact about the upstream photo
set, not a rendering fault. Comparing against 95 would have reported "8 photos
dropped at print time" on a correct packet. I nearly did exactly that.

A scare investigated and cleared rather than reported: the PDF names only
DejaVuSerif, which looked like the vendored Devanagari face failing to reach
print. It is not. Under print media emulation the stack still resolves to
"Noto Serif Devanagari", and the same string measures 210px in it against 420px
in plain serif, so the face is genuinely in use and the PDF names are Chromium
subset aliases. Corroborated independently: hi is the largest packet at 3.2MB
with the fewest faces at 7, which is one big vendored face, not a fallback. A
ToUnicode-based check was discarded after calibration found only 30 Latin
codepoints in a known-good English PDF — an incomplete parser whose
zero-Devanagari result proved nothing.

All 11 locales embed fonts and carry every photo. i18n re-verified through the
new assertions with a partial block (2 strings, 4/12 points, 5/12 rhythms):
2.8MB, 11 fonts, 87 images, wheel 5 translated and 7 English, 12 point rows,
0 blanks. The studio's declared map-less photo-less packet still passes — the
image assertion skips at 0 distinct sources, so the opt-outs compose. Wave 34/34.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 14:10:59 +00:00
parent e130cb672a
commit 588d0da48a

View file

@ -205,6 +205,14 @@ const r = await page.evaluate(() => {
sheets: document.querySelectorAll('.sheet').length,
cards: document.querySelectorAll('.card').length,
imgs: imgs.length,
// ⚠ DISTINCT sources, not the tag count. A PDF DE-DUPLICATES identical images, so the
// number of /Subtype /Image objects can never equal the number of <img> tags when two
// people groups share a portrait. MEASURED on laos: 95 cards, 95 <img>, but only 85
// distinct srcs — ten groups reuse another group's photo, which is a fact about the
// upstream photo set, not a rendering fault. Comparing the PDF against 95 would have
// reported "8 photos dropped at print time" on a correct packet; comparing against 85
// asks the question that has a right answer.
distinctImgSrc: new Set(imgs.map((i) => i.getAttribute('src'))).size,
broken: imgs.filter((i) => !i.complete || i.naturalWidth === 0).length,
nophoto: document.querySelectorAll('.nophoto').length,
rhythms: RHYTHMS.filter((w) => body.includes(w)).length,
@ -290,6 +298,16 @@ const raw = pdf.toString('latin1')
const boxes = [...raw.matchAll(/MediaBox\s*\[\s*0\s+0\s+([\d.]+)\s+([\d.]+)\s*\]/g)].map((m) => [+m[1], +m[2]])
const pages = (raw.match(/\/Type\s*\/Page[^s]/g) || []).length
const landscape = boxes.filter(([w, h]) => h <= w).length
// ★ THE PRINT LAYER, WHICH NOTHING ASSERTED. This gate already GENERATED a PDF and then
// read only its STRUCTURE — page count and MediaBox. The packet exists to be PRINTED (it
// is a standalone file with photos and fonts inlined precisely so it prints offline), so
// "8 pages of the right size" is satisfied by eight blank sheets.
// FontFile2 = a TrueType face actually EMBEDDED in the document. Zero means the text was
// either rasterised or left to whatever fonts the printer happens to have, which for a
// packet carrying a vendored Devanagari face is the difference between Hindi and tofu.
const pdfFonts = (raw.match(/\/FontFile2/g) || []).length
const pdfImages = (raw.match(/\/Subtype\s*\/Image/g) || []).length
const pdfBytes = pdf.length
// Counts are RELATIVE (sheets vs pages, cards vs photos) so this gate is not pinned to
// one zone's group count — a per-zone packet has a different number of both.
@ -426,6 +444,30 @@ if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`)
if (landscape) fails.push(`${landscape} landscape page(s) — the packet must be portrait`)
if (pages !== r.sheets) fails.push(`${pages} PDF pages for ${r.sheets} sheets — one sheet must be exactly one page`)
// ── THE PRINTED ARTEFACT ────────────────────────────────────────────────────
// TEXT MUST BE EMBEDDED TEXT. Measured across scripts: en 7 FontFile2, ar/hi/zh all
// embed too. Zero would mean the packet prints with whatever the printer has, which
// defeats the reason photos and fonts are inlined in the first place.
if (pdfFonts === 0) {
fails.push('the PDF embeds NO fonts (0 /FontFile2) — the packet is a standalone file '
+ 'meant to print offline, so its text must travel with it rather than depend on the '
+ "printer's font set")
}
// THE PHOTOS MUST SURVIVE PRINTING. Compared against DISTINCT srcs, because the PDF
// de-duplicates identical images — see the note on distinctImgSrc above. A small
// allowance covers the map and page furniture, which are images too.
if (expectPhotos && r.distinctImgSrc > 0 && pdfImages < r.distinctImgSrc) {
fails.push(`${pdfImages} image(s) in the PDF for ${r.distinctImgSrc} distinct photo source(s) — `
+ 'photos are being dropped at PRINT time, which no on-screen check can see')
}
// AND IT MUST NOT BE AN EMPTY SHELL. A packet with 95 inlined photos is megabytes; a
// few hundred KB means the content did not make it into the print output at all.
const PDF_MIN_KB = expectPhotos ? 500 : 40
if (pdfBytes / 1024 < PDF_MIN_KB) {
fails.push(`the PDF is only ${Math.round(pdfBytes / 1024)}KB, expected at least ${PDF_MIN_KB}KB — `
+ 'the pages are the right size and count but carry almost nothing')
}
// ── THE PAGE SIZE WAS PRINTED BUT NEVER CHECKED ─────────────────────────────
// `landscape` only catches h <= w, so a WRONG PORTRAIT size sailed through: A4 is
// 595x842pt, taller-than-wide like Letter, and would have been reported as a clean
@ -521,6 +563,8 @@ if (r.ranges.length) {
}
console.log(` sheets/PDF pages : ${r.sheets} / ${pages}`)
console.log(` printed artefact : ${(pdfBytes / 1024 / 1024).toFixed(1)}MB, ${pdfFonts} embedded font(s), `
+ `${pdfImages} image(s) for ${r.distinctImgSrc} distinct source(s)`)
console.log(` people cards : ${r.cards} (${r.imgs} photos, ${r.nophoto} typographic)`)
console.log(` prayer points : ${r.points} / ${POINTS_EXPECT}`)
console.log(` wheel segments : ${r.wheelSegs} (${r.wheelLabels}/${WHEEL_LABELS} labelled)`)