Large side-lane sheets: photos print in the sheet's own document, and a raster never draws a broken image
Measured on the 60x96 print: every photo beside a name rasterised as the browser's broken-image glyph. An SVG rasterised through <img> is an isolated document that fetches no external resource, and none of the people-group photo hosts (media.joshuaproject.net, joshuaproject.net, images.peoplegroups.org and its resizer, peoplegroupspublic.blob.core.windows.net, s3.doxa.life) sends CORS, so a photo cannot be inlined as data: in the browser either. - poster-raster.js: rasterPoster drops every <image> whose href is not data: (the basemap tiles are data: and stay), so a download shows the paper, never the glyph. hasExternalImages() says when a sheet carries such photos. - PosterWall: printing a generated sheet that carries photos prints THE SHEET ITSELF through printDocument, where the photos load, on its own @page (60in 96in), instead of a raster shrunk onto letter. Same HUD steps. - check:side-lanes: photo arms (0 external images reach the raster; the wall prints such a sheet as a document), each with a poison in its self-test. - check:side-lane-photo-pixels: the pixel side, a browser gate (a data: photo draws red; 99/99 external slots stay flat; without the strip 0/99 do). Declared on-demand until its first browser-seat run.
This commit is contained in:
parent
9775eb7494
commit
2f163e04c1
6 changed files with 163 additions and 6 deletions
|
|
@ -10,6 +10,8 @@
|
|||
"check:coverage-consistent": "the full run includes clause L, the age of the oldest link verification (45 days), which depends on the CALENDAR: chained, it would turn gates red on a date (first 2026-11-08) with no code change. Run it with the link re-verification; every other clause runs in gates:pre as check:coverage-consistent:chained",
|
||||
"check:snippets-draw": "AWAITING ITS FIRST BROWSER RUN: the browser suite lists a gate only after it has run. Moves into verify-browser-all's GATES, and out of this list, once that run is recorded.",
|
||||
"check:grid-merge-defaults": "WAITS FOR ITS CODE: it asserts the defaults of three grid props (filtersToggle, backdrop, backdropFloor) that are not on development, because the grid merge (b1e839a33) has not landed. Chain it in gates:pre in the same landing as that merge.",
|
||||
"check:snippets-draw:mapbox": "AWAITING ITS FIRST BROWSER RUN: the browser suite lists a gate only after it has run. Moves into verify-browser-all's GATES, and out of this list, once that run is recorded. Needs MAP_TOKEN."
|
||||
"check:snippets-draw:mapbox": "AWAITING ITS FIRST BROWSER RUN: the browser suite lists a gate only after it has run. Moves into verify-browser-all's GATES, and out of this list, once that run is recorded. Needs MAP_TOKEN.",
|
||||
"check:side-lane-photo-pixels": "a BROWSER gate (browser seat only; elsewhere NOT MEASURED, 75). Its first slot run must come before it joins the browser suite (verify-browser-all lists only gates already run there); until then its source twin, check:side-lanes' photo arms, runs in gates:pre",
|
||||
"check:side-lane-photo-pixels:self-test": "the poison run of check:side-lane-photo-pixels (a raster without stripExternalImages must draw the glyph); same browser-seat reason"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -573,7 +573,11 @@ async function exportPoster(item) {
|
|||
const format = props.posterAction === 'print' ? 'letter' : (item.sheet === '3x3' ? '3x3' : '3x4')
|
||||
const html = await Promise.resolve().then(() => props.sheet(item, format)).catch(() => '')
|
||||
if (html) {
|
||||
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js')
|
||||
const { rasterPoster, looksRendered, hasExternalImages } = await import('@map/packet/poster-raster.js')
|
||||
// ★ A SHEET WITH PHOTOS PRINTS AS ITS OWN DOCUMENT (the large side-lane sheets): a raster cannot carry a
|
||||
// photo from its own host (poster-raster.js says why), and the sheet's own @page is its true size
|
||||
// (60in 96in), where an image of it would be shrunk onto letter. A download still rasterises, without them.
|
||||
if (props.posterAction === 'print' && hasExternalImages(html)) { await printSheetDocument(html); return }
|
||||
const blob = await rasterPoster(html, { scale: 2 })
|
||||
if (await looksRendered(blob)) { await deliverPoster(blob, item); return }
|
||||
}
|
||||
|
|
@ -802,6 +806,20 @@ function printPacketAs(item, variantId) {
|
|||
// ONE delivery point for the finished poster, so 'download' and 'print' cannot
|
||||
// drift apart or be applied to only one of the two paths (shipped .webp vs
|
||||
// browser-rasterised). Both callers in exportPoster() route through here.
|
||||
// Print a generated sheet as the document it is (photos load, the page is its own size). Same HUD steps as
|
||||
// deliverPoster's print, including the phone's Print button when the tap has expired.
|
||||
async function printSheetDocument(html) {
|
||||
exportState.value = { ...exportState.value, phase: 'save', message: hud('wall.hud.openingPrint', 'opening the print dialog'), pct: 80 }
|
||||
const { printDocument } = await import('@map/composables/printInPlace.js')
|
||||
const how = await printDocument(html, { onNeedsTap: (printNow, cancel) => {
|
||||
exportState.value = { ...exportState.value, phase: 'ready', message: hud('wall.hud.readyToPrint', 'ready to print'), pct: 100,
|
||||
action: () => { exportState.value = null; printNow() }, cancel }
|
||||
} })
|
||||
if (how === 'needs-tap') return
|
||||
exportState.value = { ...exportState.value, phase: 'done', message: hud('wall.hud.printOpened', 'print dialog opened'), pct: 100 }
|
||||
setTimeout(() => { exportState.value = null }, 900)
|
||||
}
|
||||
|
||||
async function deliverPoster(blob, item) {
|
||||
if (props.posterAction === 'print') {
|
||||
exportState.value = { ...exportState.value, phase: 'save', message: hud('wall.hud.openingPrint', 'opening the print dialog'), pct: 80 }
|
||||
|
|
|
|||
|
|
@ -38,13 +38,24 @@ export function inlineSvg(posterHtml) {
|
|||
return svg.replace(/<svg([^>]*)>/, `<svg$1><style>${safe}</style>`);
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠ AN EXTERNAL IMAGE CAN NEVER DRAW IN THAT ISOLATED DOCUMENT. An SVG loaded through <img> fetches no external
|
||||
* resource by spec, and no people-group photo host sends CORS headers, so a photo cannot be inlined as data: in
|
||||
* the browser either. Left in, each one rasterises as the browser's broken-image glyph (measured on the large
|
||||
* side-lane sheet's photos). So the raster drops every <image> whose href is not data: (the basemap tiles, which
|
||||
* ARE data:, stay); a sheet that carries photos PRINTS AS A DOCUMENT instead, where they load (hasExternalImages).
|
||||
*/
|
||||
const EXTERNAL_IMAGE = /<image\b[^>]*?\bhref="(?!data:)[^"]*"[^>]*?\/>/g;
|
||||
export function hasExternalImages(posterHtml) { return /<image\b[^>]*?\bhref="(?!data:)/.test(posterHtml || ''); }
|
||||
export function stripExternalImages(svg) { return svg.replace(EXTERNAL_IMAGE, ''); }
|
||||
|
||||
/**
|
||||
* Rasterise a poster to a Blob in the browser.
|
||||
* @param {string} posterHtml the poster document (posters/<id>.html)
|
||||
* @param {object} opts scale (default 2), type ('image/webp'|'image/png'), quality
|
||||
*/
|
||||
export async function rasterPoster(posterHtml, { scale = 2, type = 'image/webp', quality = 0.85 } = {}) {
|
||||
const svg = inlineSvg(posterHtml);
|
||||
const svg = stripExternalImages(inlineSvg(posterHtml));
|
||||
// encodeURIComponent, not btoa: the posters carry non-Latin-1 characters (people
|
||||
// group and place names), and btoa throws on those.
|
||||
const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
||||
|
|
|
|||
|
|
@ -413,7 +413,9 @@
|
|||
"check:panel-sheets": "node internal/check-panel-sheets.mjs",
|
||||
"check:poster-wall-host-seams": "node internal/check-poster-wall-host-seams.mjs",
|
||||
"check:side-lanes": "node plugin-designs/country-posters/scripts/check-side-lanes.mjs",
|
||||
"check:side-lanes:self-test": "node plugin-designs/country-posters/scripts/check-side-lanes.mjs --self-test"
|
||||
"check:side-lanes:self-test": "node plugin-designs/country-posters/scripts/check-side-lanes.mjs --self-test",
|
||||
"check:side-lane-photo-pixels": "node plugin-designs/country-posters/scripts/check-side-lane-photo-pixels.mjs",
|
||||
"check:side-lane-photo-pixels:self-test": "node plugin-designs/country-posters/scripts/check-side-lane-photo-pixels.mjs --self-test"
|
||||
},
|
||||
"//scripts": {
|
||||
"_": "What each script does. These notes used to sit inside the command strings as trailing \"# …\" comments, which made the shell discard any arguments passed after \"--\" (so a gate given --url silently measured its hardcoded default port — possibly another agent's server). Keep notes here, never in a command.",
|
||||
|
|
@ -677,7 +679,9 @@
|
|||
"check:panel-sheets": "the shared panel geometry (library/components/poster/panelSheets.js) for posters printed on several sheets: 60x96 = 3 × 60×36 stacked and 66x66 = 2 × 36×66 side by side, 6 in joins, and a single sheet; the sheets tile the finished poster, glue slices tile and bleed slices repeat exactly the join, a bleed join prints a guide not a solid band, a screen preview always shows the red band, and the Bless 5x8ft takes its bands and marks from the module. Two-sided (4 planted defects). Headless.",
|
||||
"check:poster-wall-host-seams": "PosterWall's two host seams, and that with neither set every wall behaves as before. headerAction: default null is the Show-on-map TOGGLE (aria-pressed, one window event per click, is-last, no emit); { labelKey, ariaKey } is a plain button that emits header-action with the item, no window event, no aria-pressed, and reads the label alone for an item with no count. previewSrc: default null is the stored previews/<id>.webp and no IntersectionObserver; set, a card renders only once in view of the wall's own scroller, one render at a time, cached per page, and null / a throw / a timeout ends on the preview-unavailable placeholder. Image errors: a default wall retries the tree's own copy before the placeholder; a generated preview goes straight to it. Section heads: a section with people reads \"<n> people groups · <items>\" exactly as before; one with none reads its items alone. sectionItemCountKey lets a wall count one section in its own words ({ key, n }); unnamed sections keep the default. Headless: PosterWall compiled as a client component on a recording renderer; real onClick and a recording IntersectionObserver. 17 mutants each FAIL.",
|
||||
"check:side-lanes": "the large side-lane sheets (60x96, 66x66, 36x60) through the real poster-render.js, on a SEEDED worst case (855 groups, stacked duplicates, tight clusters, real and placeholder photos): every name drawn exactly once, leader crossings 0 (re-measured from the SVG, and equal to the renderer's own count), no overflow rows, photos only where a real one exists and the format allows them.",
|
||||
"check:side-lanes:self-test": "a poster-render.js copy WITHOUT the lane clamp must make check:side-lanes' crossing arm FAIL on the same seeded data (it does: 36 crossings), so the arm is shown able to fail."
|
||||
"check:side-lanes:self-test": "a poster-render.js copy WITHOUT the lane clamp must make check:side-lanes' crossing arm FAIL on the same seeded data (it does: 36 crossings), so the arm is shown able to fail.",
|
||||
"check:side-lane-photo-pixels": "BROWSER gate (browser seat; elsewhere 75 NOT MEASURED): the 60x96 side-lane sheet rasterised by poster-raster.js, measured in pixels at every photo slot. A data: photo draws; every photo on its own host stays flat, never the broken-image glyph an external <image> rasterises as.",
|
||||
"check:side-lane-photo-pixels:self-test": "the same raster WITHOUT stripExternalImages must leave the external photo slots NOT flat (the glyph), else the gate proves nothing."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.6.9",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env node
|
||||
// check:side-lane-photo-pixels — a BROWSER gate (browser seat only: internal/browser-slot.sh; elsewhere exit 75,
|
||||
// NOT MEASURED). The 60x96 side-lane sheet, rendered by the real poster-render.js, rasterised by the real
|
||||
// poster-raster.js, measured in PIXELS at every photo slot:
|
||||
// - a data: photo (a local red fixture, so it is offline) DRAWS: its slot is red;
|
||||
// - every photo on its own host stays FLAT (the paper shows through): never the broken-image glyph, which is
|
||||
// what an external <image> rasterises as inside an SVG loaded as an image.
|
||||
// --self-test: the same raster WITHOUT stripExternalImages must leave external slots NOT flat (the glyph).
|
||||
// Its source-level twin, always measured, is check:side-lanes' photo arms.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import zlib from 'node:zlib'
|
||||
import { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
||||
const { chromium } = await import(join(ROOT, 'internal/lib/playwright-guarded.mjs'))
|
||||
const J = (p) => JSON.parse(readFileSync(join(ROOT, p), 'utf8'))
|
||||
// a 4x4 red PNG, built here (no file)
|
||||
function png(w, h, rgb) {
|
||||
const crcT = Array.from({ length: 256 }, (_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c >>> 0 })
|
||||
const crc = (b) => { let c = 0xffffffff; for (const x of b) c = crcT[(c ^ x) & 255] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0 }
|
||||
const chunk = (t, d) => { const l = Buffer.alloc(4); l.writeUInt32BE(d.length); const td = Buffer.concat([Buffer.from(t), d]); const c = Buffer.alloc(4); c.writeUInt32BE(crc(td)); return Buffer.concat([l, td, c]) }
|
||||
const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2
|
||||
const raw = Buffer.concat(Array.from({ length: h }, () => Buffer.concat([Buffer.from([0]), Buffer.concat(Array.from({ length: w }, () => Buffer.from(rgb)))])))
|
||||
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))])
|
||||
}
|
||||
const RED = 'data:image/png;base64,' + png(4, 4, [230, 20, 20]).toString('base64')
|
||||
const { createPosterGenerator } = await import(join(ROOT, 'library/packet/poster-render.js'))
|
||||
let seed = 7; const rnd = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648)
|
||||
const pins = Array.from({ length: 300 }, (_, i) => ({ name: `Group ${i}`, lat: 8 + rnd() * 27, lng: 68 + rnd() * 28, slug: `g${i}`, country_code: 'IND', population: 1000 + i,
|
||||
image_url: i % 3 === 0 ? `https://media.joshuaproject.net/public/assets/media/profiles/photos/p${10120 + i}.jpg` : 'https://s3.doxa.life/no-photo-images/asia.jpg' }))
|
||||
const adm0 = J('public/dx-maps/assets/boundaries/world-adm0.geojson')
|
||||
const zone = { id: 'country-ind', zoneKey: 'country-ind', label: 'India', count: pins.length, color: '#d4a640', adm0: ['IND'], selector: { type: 'country_codes', values: ['IND'] } }
|
||||
const gen = createPosterGenerator({ env: { FORMAT: '60x96', SPARSE: '1', POSTER_DATE: '2026-10-01' }, SERIES_DEF: J('library/scripts/posters/series.json').country, ISO3_NAME: { IND: 'India' }, adm0, adm1Ind: null, adm1Chn: null, nePlaces: [], series: { totalGroups: pins.length, totalLists: 1, index: 1 } })
|
||||
let { html } = await gen.renderPoster(zone, pins, { type: 'FeatureCollection', features: adm0.features.filter((f) => f.properties?.iso3 === 'IND') })
|
||||
html = html.replace(/(<image href=")https:[^"]+(")/, `$1${RED}$2`) // the FIRST photo becomes the local fixture
|
||||
const slots = [...html.matchAll(/<image href="([^"]+)" x="([\d.]+)" y="([\d.]+)" width="([\d.]+)" height="([\d.]+)"/g)].map((m) => ({ data: m[1].startsWith('data:'), x: +m[2], y: +m[3], s: +m[4] }))
|
||||
const vb = html.match(/<svg[^>]*viewBox="0 0 ([\d.]+) ([\d.]+)"/); const W = vb ? +vb[1] : 2500
|
||||
const cur = readFileSync(join(ROOT, 'library/packet/poster-raster.js'), 'utf8')
|
||||
const poison = cur.replace('stripExternalImages(inlineSvg(posterHtml))', 'inlineSvg(posterHtml)')
|
||||
if (poison === cur) throw new Error('poison anchor moved')
|
||||
console.log(`sheet: ${slots.length} photo slots (${slots.filter((s) => s.data).length} data: fixture, ${slots.filter((s) => !s.data).length} external), viewBox width ${W}`)
|
||||
const selfTest = process.argv.includes('--self-test')
|
||||
const b = await chromium.launch()
|
||||
const page = await b.newPage({ viewport: { width: 800, height: 600 }, deviceScaleFactor: 1 })
|
||||
let verdict = 0
|
||||
try {
|
||||
await page.setContent('<!doctype html><html><body></body></html>')
|
||||
for (const [label, src] of selfTest ? [['POISON (no strip)', poison]] : [['CURRENT', cur]]) {
|
||||
const r = await page.evaluate(async ({ src, html, slots, W }) => {
|
||||
const mod = await import(URL.createObjectURL(new Blob([src], { type: 'text/javascript' })))
|
||||
const blob = await mod.rasterPoster(html, { scale: 0.5, type: 'image/png' })
|
||||
const bmp = await createImageBitmap(blob); const c = document.createElement('canvas'); c.width = bmp.width; c.height = bmp.height
|
||||
const ctx = c.getContext('2d'); ctx.drawImage(bmp, 0, 0); const k = bmp.width / W
|
||||
const stat = (s) => { const x = Math.round((s.x + s.s * 0.2) * k), y = Math.round((s.y + s.s * 0.2) * k), n = Math.max(2, Math.round(s.s * 0.6 * k))
|
||||
const d = ctx.getImageData(x, y, n, n).data; let r = 0, g = 0, b = 0, v = 0; const px = d.length / 4
|
||||
for (let i = 0; i < d.length; i += 4) { r += d[i]; g += d[i + 1]; b += d[i + 2] } r /= px; g /= px; b /= px
|
||||
for (let i = 0; i < d.length; i += 4) v += Math.abs(d[i] - r) + Math.abs(d[i + 1] - g) + Math.abs(d[i + 2] - b)
|
||||
return { r: Math.round(r), g: Math.round(g), b: Math.round(b), varr: +(v / px).toFixed(1) } }
|
||||
const bg = stat({ x: 2, y: 2, s: 30 })
|
||||
const fix = slots.filter((s) => s.data).map(stat)
|
||||
const ext = slots.filter((s) => !s.data).map(stat)
|
||||
const near = (a) => a.varr <= 6 // flat: nothing drawn in the slot (paper shows through)
|
||||
return { size: [bmp.width, bmp.height], bg, fixture: fix, extBackground: ext.filter(near).length, extTotal: ext.length, extSample: ext.filter((a) => !near(a)).slice(0, 3) }
|
||||
}, { src, html, slots, W })
|
||||
const redOk = r.fixture.length && r.fixture.every((a) => a.r > 180 && a.g < 80 && a.b < 80)
|
||||
const flatAll = r.extBackground === r.extTotal && r.extTotal > 0
|
||||
if (selfTest) verdict = r.extBackground === 0 ? 0 : 1
|
||||
else verdict = redOk && flatAll ? 0 : 1
|
||||
console.log(`${label}: raster ${r.size.join('x')}, background rgb(${r.bg.r},${r.bg.g},${r.bg.b}); fixture slot ${JSON.stringify(r.fixture)} -> ${redOk ? 'DRAWS (red)' : 'NOT drawn'}; external slots FLAT (nothing drawn): ${r.extBackground}/${r.extTotal}${r.extSample.length ? ' non-background e.g. ' + JSON.stringify(r.extSample) : ''}`)
|
||||
}
|
||||
} finally { await b.close() }
|
||||
if (selfTest) console.log(verdict ? '✗ MISSED: external slots stayed flat without the strip' : '✓ caught: without the strip every external slot draws (the glyph)')
|
||||
else console.log(verdict ? 'FAIL check:side-lane-photo-pixels' : 'PASS check:side-lane-photo-pixels — the data: photo draws; every external photo slot is flat (no broken-image glyph)')
|
||||
process.exitCode = verdict
|
||||
|
|
@ -7,7 +7,12 @@
|
|||
//
|
||||
// Asserts per format: every name drawn exactly once; leader crossings 0 (the renderer's own count, re-measured here
|
||||
// from the SVG); no overflow rows; photos = the pins with a real photo (60x96/66x66) or none (36x60).
|
||||
// --self-test: a renderer copy WITHOUT the lane clamp must FAIL the crossing arm (else the arm proves nothing).
|
||||
// PHOTOS ARE NEVER RASTERISED: a sheet whose photos sit on their own hosts must (a) reach the raster with NO external
|
||||
// <image> (poster-raster.js strips them: inside an SVG loaded as an image they can only draw the broken-image glyph,
|
||||
// and no photo host sends CORS), and (b) PRINT as its own document from PosterWall, where the photos load. The pixel
|
||||
// side of this (a data: photo draws, an external slot stays flat) is check:side-lane-photo-pixels, a browser gate.
|
||||
// --self-test: a renderer copy WITHOUT the lane clamp must FAIL the crossing arm, a raster that keeps external
|
||||
// images must FAIL arm (a), and a PosterWall print that rasterises them must FAIL arm (b) (else the arms prove nothing).
|
||||
// Exit 0 pass, 1 fail.
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, dirname } from 'node:path'
|
||||
|
|
@ -77,6 +82,33 @@ function judge(format, m, pins, wantPhotos) {
|
|||
return fails
|
||||
}
|
||||
|
||||
// (a) + (b), on the 60x96 sheet's HTML. rasterSrc / wallSrc are the source texts, so a poison can swap them.
|
||||
async function photoArms(html, rasterSrc, wallSrc) {
|
||||
const fails = []
|
||||
const m = await import('data:text/javascript;base64,' + Buffer.from(rasterSrc).toString('base64'))
|
||||
const ext = (t) => (t.match(/<image\b[^>]*?\bhref="(?!data:)/g) || []).length
|
||||
if (!ext(html)) fails.push('photos: the 60x96 sheet carries no external photo, so these arms measure nothing')
|
||||
if (typeof m.hasExternalImages !== 'function' || !m.hasExternalImages(html)) fails.push('photos: poster-raster.js hasExternalImages() does not see the sheet\'s photos')
|
||||
// what rasterPoster hands to new Image(): its own first line, inlineSvg + whatever it applies
|
||||
const body = (rasterSrc.match(/export async function rasterPoster[\s\S]*?const url = /) || [''])[0]
|
||||
const prep = (body.match(/const svg = ([^;]+);/) || [])[1] || ''
|
||||
const svg = prep ? new Function('inlineSvg', 'stripExternalImages', 'posterHtml', `return ${prep}`)(m.inlineSvg, m.stripExternalImages, html) : ''
|
||||
if (!prep) fails.push('photos: could not find what rasterPoster rasterises (const svg = ...)')
|
||||
else if (ext(svg)) fails.push(`photos: rasterPoster would rasterise ${ext(svg)} external <image> (each one prints the broken-image glyph)`)
|
||||
// (b) the wall's generated-sheet branch prints a sheet with external images as a document, before any raster
|
||||
const branch = (wallSrc.match(/if \(props\.sheet\) \{[\s\S]*?rasterPoster\(html/) || [''])[0]
|
||||
if (!/posterAction === 'print' && hasExternalImages\(html\)\) \{ await printSheetDocument\(html\); return \}/.test(branch)) fails.push('photos: PosterWall prints a sheet with photos through the raster (they cannot draw there)')
|
||||
if (!/async function printSheetDocument\(html\)[\s\S]{0,400}printDocument\(html,/.test(wallSrc)) fails.push('photos: printSheetDocument does not hand the sheet itself to printDocument')
|
||||
return fails
|
||||
}
|
||||
const RASTER = join(ROOT, 'library/packet/poster-raster.js'), WALL = join(ROOT, 'library/components/poster/PosterWall.vue')
|
||||
async function sheetHtml(createPosterGenerator, pins) {
|
||||
const adm0 = J('public/dx-maps/assets/boundaries/world-adm0.geojson')
|
||||
const gen = createPosterGenerator({ env: { FORMAT: '60x96', SPARSE: '1', POSTER_DATE: '2026-10-01' }, SERIES_DEF: J('library/scripts/posters/series.json').country, ISO3_NAME: { IND: 'India' }, adm0, adm1Ind: null, adm1Chn: null, nePlaces: [], series: { totalGroups: pins.length, totalLists: 1, index: 1 } })
|
||||
const zone = { id: 'country-ind', zoneKey: 'country-ind', label: 'India', count: pins.length, color: '#d4a640', adm0: ['IND'], selector: { type: 'country_codes', values: ['IND'] } }
|
||||
return (await gen.renderPoster(zone, pins, { type: 'FeatureCollection', features: adm0.features.filter((f) => f.properties?.iso3 === 'IND') })).html || ''
|
||||
}
|
||||
|
||||
const selfTest = process.argv.includes('--self-test')
|
||||
const src = readFileSync(RENDER, 'utf8')
|
||||
const pins = synthetic()
|
||||
|
|
@ -92,6 +124,18 @@ if (selfTest) {
|
|||
console.log(f.some((x) => /crossings/.test(x)) ? `✓ caught: no lane clamp -> ${m.crossings} crossings` : '✗ MISSED: the crossing arm stayed green without the lane clamp')
|
||||
process.exitCode = f.some((x) => /crossings/.test(x)) ? 0 : 1
|
||||
}
|
||||
const { createPosterGenerator } = await import(RENDER)
|
||||
const html = await sheetHtml(createPosterGenerator, pins)
|
||||
const rs = readFileSync(RASTER, 'utf8'), ws = readFileSync(WALL, 'utf8')
|
||||
for (const [name, r, w, rx] of [
|
||||
['a raster that keeps external images', rs.replace('stripExternalImages(inlineSvg(posterHtml))', 'inlineSvg(posterHtml)'), ws, /rasterise \d+ external/],
|
||||
['a wall print that rasterises a sheet with photos', rs, ws.replace("if (props.posterAction === 'print' && hasExternalImages(html)) { await printSheetDocument(html); return }", ''), /through the raster/],
|
||||
]) {
|
||||
if (r === rs && w === ws) { console.log(`NOT MEASURED: the poison anchor for "${name}" moved`); process.exitCode = 75; continue }
|
||||
const f = await photoArms(html, r, w)
|
||||
const ok = f.some((x) => rx.test(x)); console.log(ok ? `✓ caught: ${name} -> ${f.find((x) => rx.test(x))}` : `✗ MISSED: ${name}`)
|
||||
if (!ok) process.exitCode = 1
|
||||
}
|
||||
} else {
|
||||
const { createPosterGenerator } = await import(RENDER)
|
||||
const fails = [], lines = []
|
||||
|
|
@ -100,6 +144,9 @@ if (selfTest) {
|
|||
fails.push(...judge(format, m, pins, photos))
|
||||
lines.push(`${format}: ${m.names.length} names, ${m.leaders} leaders, ${m.crossings} crossings, ${m.photos} photos, ${m.stats?.namePt} pt`)
|
||||
}
|
||||
const html = await sheetHtml(createPosterGenerator, pins)
|
||||
const pf = await photoArms(html, readFileSync(RASTER, 'utf8'), readFileSync(WALL, 'utf8'))
|
||||
fails.push(...pf); if (!pf.length) lines.push(`photos: ${(html.match(/<image\b[^>]*?\bhref="(?!data:)/g) || []).length} external, 0 reach the raster, the wall prints the sheet as a document`)
|
||||
if (fails.length) { fails.forEach((f) => console.log('FAIL ' + f)); process.exitCode = 1 }
|
||||
else console.log(`PASS check:side-lanes — synthetic ${pins.length} groups (stacked + clustered); ${lines.join(' | ')}`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue