shots: Mac App Store screenshots (mac size, APP_DESKTOP to the macOS version), a landscape layout (Mac window or bare; Mac, Play Chromebook, Play tablets), safe margins in every template checked by shots check from the render's layout boxes, a contact sheet timeout that scales with images, sizes and load; pitfall 80

This commit is contained in:
CRTkafa 2026-10-20 03:29:19 +07:00
parent 30488c512d
commit e59539fec8
26 changed files with 532 additions and 67 deletions

View file

@ -3,7 +3,7 @@
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { boxOutside, type DeviceBox, PLAY_TABLET_SIZES, SIZES } from './lib';
import { boxOutside, type DeviceBox, marginViolations, PLAY_TABLET_SIZES, SIZES, type TextBox } from './lib';
import { fontIssues, type FontUsage } from './glyphs';
import { pulledEntry, readPulled } from '../lib/art-pull';
import { checkCount, checkImage, checkVideo, type ImageInfo, imageInfo, type Issue, IMAGE_SPECS, parseProbe, PREVIEW_RULES, type ProbeInfo, VIDEO_TARGETS } from './spec';
@ -12,7 +12,14 @@ import { checkCount, checkImage, checkVideo, type ImageInfo, imageInfo, type Iss
* fonts = which face drew the text (glyphs.ts FontUsage): automatic fallbacks and system-font glyphs are errors. */
export type RenderReport = Record<string, { overflow: string[]; missing?: string[]; fonts?: FontUsage;
/** Device frames' bounding boxes (the rotated outline, from the page's layout) in image px; bleed = cropped on purpose. */
devices?: DeviceBox[]; bleed?: boolean }>;
devices?: DeviceBox[]; bleed?: boolean;
/** Text and logo boxes in image px (from the page's layout), for the safe-margin rule (lib.ts marginViolations). */
boxes?: TextBox[] }>;
/** Text or logo closer to an edge than the safe margin: an error (stores crop and round corners; it reads cramped). */
export function marginIssues(entry: RenderReport[string] | undefined, W: number, H: number): Issue[] {
return marginViolations(entry?.boxes ?? [], W, H).map(msg => ({ level: 'error' as const, code: 'margin' as const, msg: `${msg}: render again with the current templates, or shorten the copy / shrink the logo` }));
}
/** Device frames that leave the image. Feature graphic: an error unless shots.feature.fit is 'bleed' (Play shows the
* phone cut off). Screenshots: the templates let the phone run off the bottom by design, so only top / left / right
@ -80,6 +87,7 @@ export function scanRenders(dir: string, o: { langs: string[]; variants?: string
for (const msg of entry?.overflow ?? []) issues.push({ level: 'warn', code: 'overflow', msg });
issues.push(...fontIssues(entry?.fonts));
issues.push(...deviceIssues(entry, size, info.w, info.h));
issues.push(...marginIssues(entry, info.w, info.h));
if (entry && !entry.fonts) issues.push({ level: 'warn', code: 'fonts', msg: 'font usage not recorded (rendered by an older storebound): run shots fonts + shots render again so missing glyphs are caught' });
out.images.push({ rel, lang, variant, size, file, info, issues });
}
@ -114,13 +122,13 @@ export function ffprobe(file: string): ProbeInfo | null {
/** Which sets the stores require for this config (missing ones are errors, others only checked when present). With a
* language id: Apple sets only when that language has an App Store locale, Play sets only when it has a Play code.
* Play tablets (tab7, tab10) only when `tablets` (resolved play.tablets; unset here = play.tablets === true). */
export function requiredSets(c: { apple?: { ipad?: boolean; iphoneMedium?: boolean } | undefined; play?: unknown; languages?: { id: string; apple?: string; play?: string | string[] }[] },
export function requiredSets(c: { apple?: { ipad?: boolean; iphoneMedium?: boolean; mac?: boolean } | undefined; play?: unknown; languages?: { id: string; apple?: string; play?: string | string[] }[] },
lang?: string, tablets?: boolean): Record<string, boolean> {
const l = lang === undefined ? undefined : c.languages?.find(x => x.id === lang);
const apple = !!c.apple && (!l || !!l.apple);
const play = !!c.play && (!l || (Array.isArray(l.play) ? l.play.length > 0 : !!l.play));
const tabs = play && (tablets ?? (c.play as { tablets?: boolean } | undefined)?.tablets === true);
return { ios69: apple, ...(c.apple?.iphoneMedium ? { 'iphone-medium': apple } : {}), ipad13: apple && !!c.apple?.ipad, play, feature: play,
return { ios69: apple, ...(c.apple?.iphoneMedium ? { 'iphone-medium': apple } : {}), ipad13: apple && !!c.apple?.ipad, ...(c.apple?.mac ? { mac: apple } : {}), play, feature: play,
...(tabs ? { tab7: true, tab10: true } : {}) };
}