#!/usr/bin/env node // aesthetic-eye — render-first visual QA for papers and their visual evidence. // // A TeX build proves syntax, not design. This tool prepares evidence crops for // visual inference and enforces a current-PDF manifest whose verdict is the // literal `design: pass|fail` for every embedded evidence image and diagram. import { createHash } from "node:crypto"; import { execFile } from "node:child_process"; import { access, mkdir, readFile, readdir, rm } from "node:fs/promises"; import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; const exec = promisify(execFile); const REQUIRED_CHECKS = ["tangents", "type", "balance", "spaceUse", "hierarchy", "edgeRouting"]; const FIGURE_CHECKS = ["scale", "legibility", "evidenceDominance", "crop", "captionFit"]; const PRESENTATION_CHECKS = ["oneClaim", "distanceType", "evidenceDominance", "voteClarity", "routeOrientation", "qrLegibility"]; const BRAND_NAME = "Aesthetic.Computer"; const BRAND_DOT_COLOR = "#B44887"; const BRAND_CHECKS = ["period", "dotColor"]; const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); const CONTACT_FONT = resolve(SCRIPT_DIR, "../oven/fonts/ComicRelief-Regular.ttf"); async function exists(path) { try { await access(path); return true; } catch { return false; } } async function sha256(path) { return createHash("sha256").update(await readFile(path)).digest("hex"); } export function countEvidenceFigures(sourceText) { return [...String(sourceText || "").matchAll(/\\begin\{figure\*?\}([\s\S]*?)\\end\{figure\*?\}/g)] .reduce( (count, match) => count + [...match[1].matchAll(/\\includegraphics(?:\[[^\]]*\])?\{/g)].length, 0, ); } export function renderedPageNumber(name) { const match = String(name || "").match(/^page-(\d+)\.png$/); return match ? Number(match[1]) : null; } function resolveInputs(input, manifestArg) { const absolute = resolve(input); const paperDir = extname(absolute).toLowerCase() === ".pdf" ? dirname(absolute) : absolute; const manifestPath = manifestArg ? resolve(manifestArg) : join(paperDir, "aesthetic-eye.json"); return { paperDir, manifestPath }; } async function loadReview(input, manifestArg) { const { paperDir, manifestPath } = resolveInputs(input, manifestArg); if (!(await exists(manifestPath))) throw new Error(`missing aesthetic-eye manifest: ${manifestPath}`); const manifest = JSON.parse(await readFile(manifestPath, "utf8")); const pdfPath = isAbsolute(manifest.pdf || "") ? manifest.pdf : join(paperDir, manifest.pdf || ""); if (!manifest.pdf || !(await exists(pdfPath))) throw new Error(`manifest PDF is missing: ${pdfPath}`); const sourcePath = join(paperDir, `${basename(pdfPath, extname(pdfPath))}.tex`); const sourceFigureCount = await exists(sourcePath) ? countEvidenceFigures(await readFile(sourcePath, "utf8")) : null; return { paperDir, manifestPath, manifest, pdfPath, sourcePath, sourceFigureCount }; } export function validateManifest(manifest, currentPdfSha256, sourceFigureCount = null) { const errors = []; const diagrams = Array.isArray(manifest?.diagrams) ? manifest.diagrams : []; const figures = Array.isArray(manifest?.figures) ? manifest.figures : []; const brand = manifest?.brand; const presentation = manifest?.presentation; if (manifest?.schema !== 1) errors.push("schema must be 1"); if (!manifest?.visualInference) errors.push("visualInference must be true"); if (manifest?.reviewer?.kind !== "visual-inference") errors.push("reviewer.kind must be visual-inference"); if (!manifest?.reviewedAt) errors.push("reviewedAt is required"); if (!manifest?.pdfSha256) errors.push("pdfSha256 is required"); else if (currentPdfSha256 && manifest.pdfSha256 !== currentPdfSha256) errors.push("review is stale: PDF hash changed"); if (!Number.isInteger(manifest?.expectedDiagrams) || manifest.expectedDiagrams < 0) { errors.push("expectedDiagrams must be a non-negative integer"); } else if (diagrams.length !== manifest.expectedDiagrams) { errors.push(`expected ${manifest.expectedDiagrams} diagram(s), found ${diagrams.length}`); } if (!brand || typeof brand !== "object") errors.push("brand review is required"); else { if (brand.canonicalName !== BRAND_NAME) errors.push(`brand.canonicalName must be ${BRAND_NAME}`); if (brand.dotColor !== BRAND_DOT_COLOR) errors.push(`brand.dotColor must be ${BRAND_DOT_COLOR}`); if (!['pass', 'fail'].includes(brand.design)) errors.push("brand.design must be pass or fail"); for (const check of BRAND_CHECKS) { if (!['pass', 'fail'].includes(brand.checks?.[check])) errors.push(`brand.checks.${check} must be pass or fail`); } const failedBrandChecks = BRAND_CHECKS.filter((check) => brand.checks?.[check] === "fail"); if (brand.design === "pass" && failedBrandChecks.length) { errors.push(`brand.design cannot pass while ${failedBrandChecks.join(", ")} fail`); } } const ids = new Set(); for (const [index, diagram] of diagrams.entries()) { const prefix = `diagram ${diagram?.id || index + 1}`; if (!diagram?.id) errors.push(`${prefix}: id is required`); else if (ids.has(diagram.id)) errors.push(`${prefix}: id must be unique`); else ids.add(diagram.id); if (!Number.isInteger(diagram?.page) || diagram.page < 1) errors.push(`${prefix}: page must be >= 1`); if (!Array.isArray(diagram?.crop) || diagram.crop.length !== 4 || diagram.crop.some((n) => !Number.isFinite(n) || n < 0 || n > 1)) { errors.push(`${prefix}: crop must be four normalized values between 0 and 1`); } else if (diagram.crop[0] + diagram.crop[2] > 1 || diagram.crop[1] + diagram.crop[3] > 1) { errors.push(`${prefix}: crop extends beyond the rendered page`); } if (!['pass', 'fail'].includes(diagram?.design)) errors.push(`${prefix}: design must be pass or fail`); for (const check of REQUIRED_CHECKS) { if (!['pass', 'fail'].includes(diagram?.checks?.[check])) errors.push(`${prefix}: checks.${check} must be pass or fail`); } const failedChecks = REQUIRED_CHECKS.filter((check) => diagram?.checks?.[check] === "fail"); if (diagram?.design === "pass" && failedChecks.length) { errors.push(`${prefix}: design cannot pass while ${failedChecks.join(", ")} fail`); } } const allDesignPass = diagrams.length === manifest?.expectedDiagrams && diagrams.every((diagram) => diagram.design === "pass"); const figuresRequired = (Number.isInteger(sourceFigureCount) && sourceFigureCount > 0) || manifest?.expectedFigures != null || figures.length > 0; if (figuresRequired) { if (!Number.isInteger(manifest?.expectedFigures) || manifest.expectedFigures < 0) { errors.push("expectedFigures must be a non-negative integer when the source contains evidence images"); } else { if (figures.length !== manifest.expectedFigures) { errors.push(`expected ${manifest.expectedFigures} evidence image(s), found ${figures.length}`); } if (Number.isInteger(sourceFigureCount) && manifest.expectedFigures !== sourceFigureCount) { errors.push(`source contains ${sourceFigureCount} evidence image(s), manifest expects ${manifest.expectedFigures}`); } } const ids = new Set(); for (const [index, figure] of figures.entries()) { const prefix = `figure ${figure?.id || index + 1}`; if (!figure?.id) errors.push(`${prefix}: id is required`); else if (ids.has(figure.id)) errors.push(`${prefix}: id must be unique`); else ids.add(figure.id); if (!Number.isInteger(figure?.page) || figure.page < 1) errors.push(`${prefix}: page must be >= 1`); if (!Array.isArray(figure?.crop) || figure.crop.length !== 4 || figure.crop.some((n) => !Number.isFinite(n) || n < 0 || n > 1)) { errors.push(`${prefix}: crop must be four normalized values between 0 and 1`); } else if (figure.crop[0] + figure.crop[2] > 1 || figure.crop[1] + figure.crop[3] > 1) { errors.push(`${prefix}: crop extends beyond the rendered page`); } if (!["pass", "fail"].includes(figure?.design)) errors.push(`${prefix}: design must be pass or fail`); for (const check of FIGURE_CHECKS) { if (!["pass", "fail"].includes(figure?.checks?.[check])) errors.push(`${prefix}: checks.${check} must be pass or fail`); } const failedChecks = FIGURE_CHECKS.filter((check) => figure?.checks?.[check] === "fail"); if (figure?.design === "pass" && failedChecks.length) { errors.push(`${prefix}: design cannot pass while ${failedChecks.join(", ")} fail`); } } } const allFigureDesignsPass = !figuresRequired || (figures.length === manifest?.expectedFigures && figures.every((figure) => figure.design === "pass")); if (presentation) { const slides = Array.isArray(presentation.slides) ? presentation.slides : []; if (!Number.isInteger(presentation.expectedSlides) || presentation.expectedSlides < 1) { errors.push("presentation.expectedSlides must be a positive integer"); } else if (slides.length !== presentation.expectedSlides) { errors.push(`presentation expected ${presentation.expectedSlides} slide(s), found ${slides.length}`); } const pages = new Set(); for (const [index, slide] of slides.entries()) { const prefix = `slide ${slide?.page || index + 1}`; if (!Number.isInteger(slide?.page) || slide.page < 1) errors.push(`${prefix}: page must be >= 1`); else if (pages.has(slide.page)) errors.push(`${prefix}: page must be unique`); else pages.add(slide.page); if (!["pass", "fail"].includes(slide?.design)) errors.push(`${prefix}: design must be pass or fail`); for (const check of PRESENTATION_CHECKS) { if (!["pass", "fail", "n/a"].includes(slide?.checks?.[check])) errors.push(`${prefix}: checks.${check} must be pass, fail, or n/a`); } const failedChecks = PRESENTATION_CHECKS.filter((check) => slide?.checks?.[check] === "fail"); if (slide?.design === "pass" && failedChecks.length) errors.push(`${prefix}: design cannot pass while ${failedChecks.join(", ")} fail`); } } const brandPass = brand?.design === "pass" && BRAND_CHECKS.every((check) => brand?.checks?.[check] === "pass"); const presentationPass = !presentation || (presentation.slides?.length === presentation.expectedSlides && presentation.slides.every((slide) => slide.design === "pass")); return { pass: errors.length === 0 && allDesignPass && allFigureDesignsPass && brandPass && presentationPass, errors, diagrams, figures, brand, presentation, }; } export async function findVisibleBrandViolations(pdfPath) { const { stdout } = await exec("pdftotext", [pdfPath, "-"], { maxBuffer: 16 * 1024 * 1024 }); return [...stdout.matchAll(/\bAesthetic\s+Computer\b/gi)].map((match) => match[0].replace(/\s+/g, " ")); } async function check(input, manifestArg) { const review = await loadReview(input, manifestArg); const digest = await sha256(review.pdfPath); const verdict = validateManifest(review.manifest, digest, review.sourceFigureCount); const brandViolations = await findVisibleBrandViolations(review.pdfPath); const errors = [...verdict.errors]; if (brandViolations.length) { errors.push(`${brandViolations.length} visible brand name(s) omit the colored period; use ${BRAND_NAME}`); } const pass = verdict.pass && errors.length === 0; console.log(`brand: ${verdict.brand?.design || "missing"} ${BRAND_NAME} dot ${BRAND_DOT_COLOR}`); for (const diagram of verdict.diagrams) console.log(`design: ${diagram.design} ${diagram.id} page ${diagram.page}`); for (const figure of verdict.figures) console.log(`figure: ${figure.design} ${figure.id} page ${figure.page}`); for (const slide of verdict.presentation?.slides || []) console.log(`slide: ${slide.design} page ${slide.page}`); if (errors.length) for (const error of errors) console.error(`FAIL: ${error}`); if (!pass && !errors.length) console.error("FAIL: one or more visual verdicts have design: fail"); console.log(`aesthetic-eye: ${pass ? "PASS" : "FAIL"} ${basename(review.pdfPath)}`); if (!pass) process.exitCode = 1; } async function prepare(input, manifestArg, outputArg) { const review = await loadReview(input, manifestArg); const outputDir = outputArg ? resolve(outputArg) : join(review.paperDir, ".aesthetic-eye"); await mkdir(outputDir, { recursive: true }); const pagePrefix = join(outputDir, "page"); for (const name of await readdir(outputDir)) { if (renderedPageNumber(name) != null) await rm(join(outputDir, name)); } await exec("pdftoppm", ["-png", "-r", "144", review.pdfPath, pagePrefix], { maxBuffer: 8 * 1024 * 1024 }); const pageEntries = (await readdir(outputDir)) .map((name) => ({ name, page: renderedPageNumber(name) })) .filter((entry) => entry.page != null) .sort((a, b) => a.page - b.page) .map((entry) => ({ ...entry, path: join(outputDir, entry.name) })); const pagePaths = pageEntries.map((entry) => entry.path); const pagePathByNumber = new Map(pageEntries.map((entry) => [entry.page, entry.path])); if (pagePaths.length) { const pagesContactPath = join(outputDir, "pages-contact.png"); await exec("magick", ["montage", "-font", CONTACT_FONT, ...pagePaths, "-thumbnail", "360x480", "-tile", "4x", "-geometry", "+16+16", pagesContactPath]); console.log(`pages: ${pagesContactPath}`); if (review.manifest.presentation) { const slidesContactPath = join(outputDir, "slides-10pct-contact.png"); await exec("magick", ["montage", "-font", CONTACT_FONT, ...pagePaths, "-thumbnail", "10%", "-tile", "5x", "-geometry", "+12+12", slidesContactPath]); console.log(`slides at 10%: ${slidesContactPath}`); } } async function renderCrops(items, kind) { const crops = []; for (const item of items || []) { const pagePath = pagePathByNumber.get(item.page); if (!pagePath || !(await exists(pagePath))) throw new Error(`${item.id}: rendered page ${item.page} is missing`); const { stdout } = await exec("magick", ["identify", "-format", "%w %h", pagePath]); const [pageWidth, pageHeight] = stdout.trim().split(/\s+/).map(Number); const [x, y, width, height] = item.crop; const geometry = `${Math.round(pageWidth * width)}x${Math.round(pageHeight * height)}+${Math.round(pageWidth * x)}+${Math.round(pageHeight * y)}`; const cropPath = join(outputDir, `${kind}-${item.id}.png`); await exec("magick", [pagePath, "-crop", geometry, "+repage", cropPath]); crops.push(cropPath); console.log(`${item.id}: ${cropPath}`); } return crops; } const figureCrops = await renderCrops(review.manifest.figures, "figure"); if (figureCrops.length) { const contactPath = join(outputDir, "figures-contact.png"); await exec("magick", ["montage", "-font", CONTACT_FONT, ...figureCrops, "-thumbnail", "1000x700", "-tile", "2x", "-geometry", "+24+24", contactPath]); console.log(`figures: ${contactPath}`); } const diagramCrops = await renderCrops(review.manifest.diagrams, "diagram"); if (diagramCrops.length) { const contactPath = join(outputDir, "diagrams-contact.png"); await exec("magick", ["montage", "-font", CONTACT_FONT, ...diagramCrops, "-thumbnail", "1000x700", "-tile", "2x", "-geometry", "+24+24", contactPath]); console.log(`diagrams: ${contactPath}`); } const brandViolations = await findVisibleBrandViolations(review.pdfPath); console.log(`brand text: ${brandViolations.length ? `FAIL (${brandViolations.length} missing period)` : "PASS"}`); console.log(`pdfSha256: ${await sha256(review.pdfPath)}`); console.log("Next: inspect pages-contact.png for the colored Aesthetic.Computer period, inspect every evidence-figure and diagram crop, record the verdicts, then run `aesthetic-eye check`."); } function option(args, name) { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : undefined; } async function main() { const args = process.argv.slice(2); const command = args[0]; const input = args[1]; if (!['prepare', 'check'].includes(command) || !input) { console.error("usage: node papers/aesthetic-eye.mjs [--manifest path] [--out dir]"); process.exit(2); } if (command === "prepare") await prepare(input, option(args, "--manifest"), option(args, "--out")); else await check(input, option(args, "--manifest")); } const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isMain) main().catch((error) => { console.error(`aesthetic-eye: ${error.message}`); process.exit(1); });