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:
parent
30488c512d
commit
e59539fec8
26 changed files with 532 additions and 67 deletions
|
|
@ -25,10 +25,18 @@ import { done, log, warn } from '../lib/log';
|
|||
import { closeBrowser, launch } from './browser';
|
||||
import { defaultConcurrency, pool, progressLine } from './capture-web';
|
||||
import { shotsStyle } from '../lib/config';
|
||||
import { type CopyFile, type DeviceBox, SIZES, featureLayout, copyText, defaultSizes, frameDims, mergeCopy, palette, renderDir, scriptFont, sheetLayout, validateCopy, variantScenes } from './lib';
|
||||
import { type CopyFile, type DeviceBox, type TextBox, SIZES, renderDims, sheetTimeoutMs, featureLayout, copyText, defaultSizes, frameDims, mergeCopy, palette, renderDir, scriptFont, sheetLayout, validateCopy, variantScenes } from './lib';
|
||||
import { faceFilePath, fontsRoute, resolveShotFonts, staleLocalFonts } from './local-fonts';
|
||||
import { type RenderReport, mergeReport, readReport, resolvedTablets } from './report';
|
||||
import * as rt from '../lib/rt';
|
||||
import { cpus, loadavg } from 'node:os';
|
||||
|
||||
/** max(1-min, 5-min load) / cores, 1..5 (the same factor tests/helpers/load.ts uses; STOREBOUND_LOAD_FACTOR overrides). */
|
||||
export function loadFactor(env = process.env, load = loadavg(), cores = cpus().length): number {
|
||||
const forced = Number(env.STOREBOUND_LOAD_FACTOR);
|
||||
if (Number.isFinite(forced) && forced > 0) return Math.min(5, Math.max(1, forced));
|
||||
return Math.min(5, Math.max(1, Math.max(load[0] ?? 0, load[1] ?? 0) / Math.max(1, cores)));
|
||||
}
|
||||
|
||||
const TYPES: Record<string, string> = { html: 'text/html; charset=utf-8', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
|
||||
webp: 'image/webp', woff2: 'font/woff2', woff: 'font/woff', ttf: 'font/ttf', otf: 'font/otf', css: 'text/css', svg: 'image/svg+xml' };
|
||||
|
|
@ -124,7 +132,9 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const lang = url.searchParams.get('lang') ?? '';
|
||||
const { scenes, copy } = jobs.get(url.searchParams.get('v') ?? '') ?? jobs.values().next().value!;
|
||||
const st = stacks(lang, copy);
|
||||
const base = { style, phone, tablet, rtl: rtl.get(lang), logo: !!shots.logo, appName: config.app.name,
|
||||
const ls = shots.landscape?.size;
|
||||
const landscape = { w: ls?.w ?? 1440, h: ls?.h ?? 900, bar: 0 };
|
||||
const base = { style, phone, tablet, landscape, side: shots.landscape?.side ?? 'left', rtl: rtl.get(lang), logo: !!shots.logo, appName: config.app.name,
|
||||
fonts: { ...fonts, script: st.script, titleStack: st.title, bodyStack: st.body } };
|
||||
if (url.searchParams.has('feature'))
|
||||
return Response.json({ ...base, frame: shots.feature?.frame ?? scenes[0]!.frames[0]!.src,
|
||||
|
|
@ -162,22 +172,30 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const started = Date.now();
|
||||
const report: RenderReport = {};
|
||||
|
||||
async function shoot(p: { page: any; errs: string[] }, url: string, w: number, h: number, out: string): Promise<void> { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
async function shoot(p: { page: any; errs: string[] }, url: string, w: number, h: number, out: string, timeout = 30000, what = ''): Promise<void> { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const { page, errs } = p;
|
||||
const t0 = Date.now();
|
||||
// heartbeat for long waits (the contact sheet decodes every image): one line every 15 s, the budget on timeout
|
||||
const beat = timeout > 30000 ? setInterval(() => log('shots', `${what || relative(root, out)}: still loading (${Math.round((Date.now() - t0) / 1000)} s of ${Math.round(timeout / 1000)} s)`), 15000) : null;
|
||||
try {
|
||||
errs.length = 0;
|
||||
mkdirSync(join(out, '..'), { recursive: true });
|
||||
await page.setViewportSize({ width: w, height: h });
|
||||
await page.goto(url, { waitUntil: 'load' });
|
||||
await page.waitForFunction("document.body.dataset.ready === '1'", null, { timeout: 30000 });
|
||||
await page.waitForFunction("document.body.dataset.ready === '1'", null, { timeout }).catch((e: Error) => {
|
||||
throw new Error(`${what || relative(root, out)}: not ready after ${Math.round((Date.now() - t0) / 1000)} s (budget ${Math.round(timeout / 1000)} s; ${errs.length ? errs.join(' | ') : 'no page errors'}): ${e.message.split('\n')[0]}`);
|
||||
});
|
||||
} finally { if (beat) clearInterval(beat); }
|
||||
await page.waitForTimeout(200);
|
||||
const tmp = out + '.tmp.png';
|
||||
const overflow = JSON.parse((await page.evaluate('document.body.dataset.overflow')) ?? '[]') as string[];
|
||||
const devices = JSON.parse((await page.evaluate('document.body.dataset.devices')) ?? '[]') as DeviceBox[];
|
||||
const bleed = (await page.evaluate('document.body.dataset.bleed')) === '1';
|
||||
const boxes = JSON.parse((await page.evaluate('document.body.dataset.boxes')) ?? '[]') as TextBox[];
|
||||
if (!out.endsWith('sheet.png')) {
|
||||
const measured = await measureFonts(page, Object.keys(faces));
|
||||
const usage = fontUsage(measured, faces);
|
||||
report[relative(outDir, out)] = { overflow, devices, ...(bleed ? { bleed } : {}), missing: errs.filter(e => e.startsWith('404')), fonts: usage };
|
||||
report[relative(outDir, out)] = { overflow, devices, ...(bleed ? { bleed } : {}), ...(boxes.length ? { boxes } : {}), missing: errs.filter(e => e.startsWith('404')), fonts: usage };
|
||||
for (const f of usage.fallback) warn('shots', `${relative(root, out)}: ${f.el} ${[...f.chars].join(' ')} drawn with ${f.family} (automatic fallback)`);
|
||||
for (const f of usage.missing) warn('shots', `${relative(root, out)}: ${f.el} ${[...f.chars].join(' ')} drawn with a system font`);
|
||||
}
|
||||
|
|
@ -194,14 +212,14 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
|
||||
// every image first (in parallel), then one contact sheet per language from its finished images
|
||||
const items: { url: string; w: number; h: number; out: string }[] = [];
|
||||
const sheets: { dir: string; rows: { size: string; files: string[] }[] }[] = [];
|
||||
const sheets: { dir: string; rows: { size: string; files: string[]; dims?: { w: number; h: number } }[] }[] = [];
|
||||
for (const [v, job] of jobs) for (const lang of langs) {
|
||||
const sceneIds = a.list('scenes').length ? a.list('scenes') : job.scenes.map(s => s.id);
|
||||
const vq = v ? `&v=${encodeURIComponent(v)}` : '';
|
||||
const dir = renderDir(outDir, lang, v || undefined);
|
||||
const rows: { size: string; files: string[] }[] = [];
|
||||
const rows: { size: string; files: string[]; dims?: { w: number; h: number } }[] = [];
|
||||
for (const size of sizes) {
|
||||
const s = SIZES[size]!;
|
||||
const s = SIZES[size]!, d = renderDims(size, shots);
|
||||
if (s.feature) {
|
||||
const f = join(dir, 'feature', '1024x500.png');
|
||||
items.push({ url: `${base}/${style === 'app' ? 'feature-app' : 'feature'}.html?lang=${encodeURIComponent(lang)}${vq}`, w: s.w, h: s.h, out: f });
|
||||
|
|
@ -211,10 +229,11 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const files: string[] = [];
|
||||
for (const [i, id] of sceneIds.entries()) {
|
||||
const f = join(dir, size, `${String(i + 1).padStart(2, '0')}.png`);
|
||||
items.push({ url: `${base}/${style === 'app' ? 'template-app' : 'template'}.html?s=${encodeURIComponent(id)}&w=${s.w}&h=${s.h}&lang=${encodeURIComponent(lang)}${s.device ? '&device=tablet' : ''}${size === 'tab7' || size === 'tab10' ? '&style=android' : ''}${vq}`, w: s.w, h: s.h, out: f });
|
||||
const dev = d.landscape ? `&device=landscape&frame=${d.frame}` : s.device ? '&device=tablet' : '';
|
||||
items.push({ url: `${base}/${style === 'app' ? 'template-app' : 'template'}.html?s=${encodeURIComponent(id)}&w=${d.w}&h=${d.h}&lang=${encodeURIComponent(lang)}${dev}${size === 'tab7' || size === 'tab10' ? '&style=android' : ''}${vq}`, w: d.w, h: d.h, out: f });
|
||||
files.push(f);
|
||||
}
|
||||
rows.push({ size, files });
|
||||
rows.push({ size, files, dims: { w: d.w, h: d.h } });
|
||||
}
|
||||
if (!a.has('no-sheet') && rows.length) sheets.push({ dir, rows });
|
||||
}
|
||||
|
|
@ -235,7 +254,8 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const sheet = join(dir, '.sheet.html');
|
||||
writeFileSync(sheet, `<!doctype html><body style="margin:0;background:#141418;width:${lay.w}px;height:${lay.h}px;position:relative">${imgs}
|
||||
<script>Promise.all([...document.images].map(i=>i.decode())).then(()=>document.body.dataset.ready='1')</script></body>`);
|
||||
await withPage(p => shoot(p, `${base}/out/${relative(outDir, sheet)}`, lay.w, lay.h, join(dir, 'sheet.png')));
|
||||
const ms = sheetTimeoutMs(lay.tiles.length, rows.length, loadFactor());
|
||||
await withPage(p => shoot(p, `${base}/out/${relative(outDir, sheet)}`, lay.w, lay.h, join(dir, 'sheet.png'), ms, `${relative(root, join(dir, 'sheet.png'))} (${lay.tiles.length} images, ${rows.length} sizes)`));
|
||||
rmSync(sheet, { force: true });
|
||||
});
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue