// JPEG copies of the Play renders (play, tab7, tab10, feature): about 4x smaller, so `play images` uploads much faster. // The App Store screenshots stay PNG. // storebound shots jpeg [--q 92] [--lang en,tr] [--variant b | --variants] import { existsSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { spawnSync } from 'node:child_process'; import type { Ctx } from '../lib/ctx'; import { done, log } from '../lib/log'; import { variantRoot } from './lib'; /** ffmpeg's mjpeg -q:v (2 best .. 31 worst) for a 0-100 quality. */ export const ffmpegQ = (q: number): number => Math.max(2, Math.min(31, Math.round(2 + (100 - q) * 0.29))); export async function run(ctx: Ctx): Promise { const { config, resolve } = ctx.loaded; const base = resolve(ctx.args.get('src') ?? config.renders ?? config.shots?.out ?? 'store/renders'); const roots = ctx.args.has('variants') ? [base, ...Object.keys(config.shots?.variants ?? {}).map(v => variantRoot(base, v, config.shots?.variants))] : [variantRoot(base, ctx.args.get('variant'), config.shots?.variants)]; const q = ctx.args.num('q', 92); const langs = ctx.args.list('lang').length ? ctx.args.list('lang') : config.languages.map(l => l.id); const sips = spawnSync('sh', ['-c', 'command -v sips']).status === 0; let n = 0; for (const src of roots) for (const lang of langs) for (const sub of ['play', 'tab7', 'tab10', 'feature']) { const dir = join(src, lang, sub); if (!existsSync(dir)) continue; for (const f of readdirSync(dir).filter(f => f.endsWith('.png'))) { const inp = join(dir, f), out = inp.replace(/\.png$/, '.jpg'); const r = sips ? spawnSync('sips', ['-s', 'format', 'jpeg', '-s', 'formatOptions', String(q), inp, '--out', out]) : spawnSync('ffmpeg', ['-v', 'error', '-y', '-i', inp, '-q:v', String(ffmpegQ(q)), out]); if (r.status !== 0) throw new Error(`jpeg failed for ${inp}`); n++; } log('shots', `${src === base ? '' : src.slice(base.length + 1) + '/'}${lang}/${sub}: jpeg q${q}`); } done('shots', `${n} JPEG copies`); }