// Tests for the geometry of an `image` entry — the part of a still's filter // chain that is expressed in the SOURCE file's own pixels. // // Pure, so it is tested without an encoder. The rest of buildImageSegment is // ffmpeg arguments and is exercised by building a real entry. // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import test from "node:test"; import { panelLayout, scrollHolds, scrollPlan, scrollYExpr, sourcePixelFilters, } from "./build-video.mjs"; const DIMS = { width: 1260, height: 2038, label: "pic.jpeg" }; const BG = "#000000"; test("nothing to do is an empty chain", () => { assert.deepEqual(sourcePixelFilters({ id: "i01" }, DIMS, BG), []); assert.deepEqual(sourcePixelFilters({ id: "i01", crop: null, redact: null }, DIMS, BG), []); }); test("a crop is [x, y, w, h] and ffmpeg's crop is w:h:x:y", () => { assert.deepEqual( sourcePixelFilters({ id: "i01", crop: [0, 1018, 1260, 1020] }, DIMS, BG), ["crop=1260:1020:0:1018"], ); // Flush against the far edge is INSIDE. The real manifest splits a 2038-tall // screenshot into two 1020s, and the second one ends exactly at the bottom. assert.deepEqual( sourcePixelFilters({ id: "i01", crop: [0, 2037, 1260, 1] }, DIMS, BG), ["crop=1260:1:0:2037"], ); }); test("redaction fills, and it happens BEFORE the crop", () => { // Blurring is not enough for a QR -- a pixelated code still scans -- so the // box is solid, in the frame's own background colour. assert.deepEqual( sourcePixelFilters( { id: "i00b", redact: [[10, 20, 100, 100], [500, 600, 40, 40]], crop: [0, 0, 1260, 1000] }, DIMS, BG, ), [ "drawbox=x=10:y=20:w=100:h=100:color=#000000:t=fill", "drawbox=x=500:y=600:w=40:h=40:color=#000000:t=fill", "crop=1260:1000:0:0", ], ); }); test("a redaction's numbers come off the ORIGINAL file, not the cropped one", () => { // Same rectangle, with and without a crop that moves the origin: the filter // is identical, because it runs first. const boxes = [[0, 1500, 200, 200]]; const withCrop = sourcePixelFilters({ id: "i", redact: boxes, crop: [0, 1000, 1260, 1038] }, DIMS, BG); const without = sourcePixelFilters({ id: "i", redact: boxes }, DIMS, BG); assert.equal(withCrop[0], without[0]); }); test("out of bounds is an error, not a clamp", () => { // ffmpeg would quietly shrink either box to fit. A redaction that silently // became a smaller redaction is the exact failure the field exists to stop. assert.throws( () => sourcePixelFilters({ id: "i01", crop: [0, 0, 5000, 5000] }, DIMS, BG), /i01: crop \[0, 0, 5000, 5000\] falls outside pic\.jpeg \(1260x2038\)/, ); assert.throws( () => sourcePixelFilters({ id: "i01", redact: [[0, 0, 10, 10], [1200, 0, 100, 10]] }, DIMS, BG), /i01: redact\[1\] \[1200, 0, 100, 10\] falls outside/, ); assert.throws( () => sourcePixelFilters({ id: "i01", redact: [[-1, 0, 10, 10]] }, DIMS, BG), /redact\[0\] \[-1, 0, 10, 10\] falls outside/, ); assert.throws( () => sourcePixelFilters({ id: "i01", redact: [[0, 0, 0, 10]] }, DIMS, BG), /redact\[0\] \[0, 0, 0, 10\] falls outside/, ); }); test("the shapes themselves are checked", () => { assert.throws( () => sourcePixelFilters({ id: "i01", redact: [0, 0, 10, 10] }, DIMS, BG), /redact\[0\] must be \[x, y, w, h\] in source pixels/, ); assert.throws( () => sourcePixelFilters({ id: "i01", redact: "all of it" }, DIMS, BG), /redact must be a list of \[x, y, w, h\] rectangles/, ); assert.throws( () => sourcePixelFilters({ id: "i01", crop: [0, 0, 100] }, DIMS, BG), /crop must be \[x, y, w, h\] in source pixels/, ); }); // --------------------------------------------------------------------------- // The crawl. A full tweet thread does not fit in a 1920x1024 hole, and both // ways out without one are worse: shrink until the text is unreadable, or cut // the thread into stills and make the reader reassemble it. // --------------------------------------------------------------------------- const AREA = { areaW: 1920, areaH: 1024, bg: "#000000" }; test("scrollHolds: true means the defaults", () => { assert.deepEqual(scrollHolds({ id: "i", scroll: true }, 10), { holdStart: 1.5, holdEnd: 2.0 }); assert.deepEqual(scrollHolds({ id: "i", scroll: { holdStart: 0 } }, 10), { holdStart: 0, holdEnd: 2.0 }); assert.equal(scrollHolds({ id: "i" }, 10), null); assert.equal(scrollHolds({ id: "i", scroll: false }, 10), null); }); test("scrollHolds: holds that leave no time to move in are refused", () => { // Not a slow crawl -- two holds and a jump cut between them. assert.throws( () => scrollHolds({ id: "i07", scroll: true }, 3), /i07: seconds is 3 but the holds alone are 1\.5 \+ 2 = 3\.5s/, ); assert.throws(() => scrollHolds({ id: "i", scroll: { holdStart: -1 } }, 10), /holdStart must be a number ≥ 0/); assert.throws(() => scrollHolds({ id: "i", scroll: [1, 2] }, 10), /scroll must be true or \{ holdStart, holdEnd \}/); }); test("scrollYExpr: clip() parks the crawl at both ends", () => { // crop's y IS per-frame in `t`; drawbox's is not, which is why the footer's // fill bar was drawn at its final width for weeks. assert.equal( scrollYExpr({ holdStart: 1.5, holdEnd: 2, seconds: 12, areaH: 1024 }), "'clip((t-1.5)/8.5,0,1)*(ih-1024)'", ); // No holds: the whole duration is travel. assert.equal( scrollYExpr({ holdStart: 0, holdEnd: 0, seconds: 6, areaH: 1024 }), "'clip((t-0)/6,0,1)*(ih-1024)'", ); }); test("scrollPlan: a tall capture is scaled to the area's WIDTH and walked", () => { const plan = scrollPlan( { id: "i07", scroll: true }, { width: 1260, height: 5000 }, { ...AREA, seconds: 12 }, ); assert.equal(plan.moving, true); // 1920/1260 = 1.5238…, under the 2x cap, so the picture fills the width. assert.deepEqual(plan.filters, [ "scale=1920:7619", "crop=w=1920:h=1024:x=0:y='clip((t-1.5)/8.5,0,1)*(ih-1024)'", ]); }); test("scrollPlan: never upscales past 2x, and centres what is left over", () => { const plan = scrollPlan( { id: "i", scroll: true }, { width: 400, height: 3000 }, { ...AREA, seconds: 10 }, ); assert.deepEqual(plan.filters.slice(0, 2), [ "scale=800:6000", "pad=1920:6000:(ow-iw)/2:0:color=#000000", ]); assert.match(plan.filters[2], /^crop=w=1920:h=1024:x=0:y='clip/); }); test("scrollPlan: a picture that fits is not a crawl", () => { // `scroll` on a short capture describes an intent, not a demand for motion. const plan = scrollPlan( { id: "i", scroll: true }, { width: 1260, height: 600 }, { ...AREA, seconds: 10 }, ); assert.equal(plan.moving, false); assert.ok(!plan.filters.some((f) => f.startsWith("crop="))); assert.equal(plan.filters.at(-1), "pad=1920:1024:0:(oh-ih)/2:color=#000000"); }); test("scrollPlan: nothing asked for, nothing planned", () => { assert.equal(scrollPlan({ id: "i" }, { width: 100, height: 100 }, { ...AREA, seconds: 4 }), null); }); // --------------------------------------------------------------------------- // A row of panels. Two phone captures that argue with each other, side by side. // --------------------------------------------------------------------------- test("panelLayout: panels are matched by HEIGHT, and the widths fall out", () => { // destinys-child p05: a 327x675 expulsion post beside an 853x1280 poster. const plan = panelLayout( [{ width: 327, height: 675 }, { width: 853, height: 1280 }], { areaW: 1920, areaH: 1024, gap: 48 }, ); assert.equal(plan.factor, 1); assert.deepEqual(plan.boxes, [{ w: 496, h: 1024 }, { w: 682, h: 1024 }]); assert.equal(plan.totalWidth, 496 + 682 + 48); assert.ok(plan.totalWidth <= 1920); }); test("panelLayout: an overflowing row shrinks by ONE factor", () => { // Four wide panels at full height would be 4 x 1024 = 4096px of picture. const plan = panelLayout( [1, 1, 1, 1].map(() => ({ width: 1000, height: 1000 })), { areaW: 1920, areaH: 1024, gap: 40 }, ); // room = 1920 - 120 = 1800, wanted = 4 x 1024 = 4096. assert.ok(Math.abs(plan.factor - 1800 / 4096) < 1e-9); assert.ok(plan.totalWidth <= 1920); // The SAME factor everywhere: a per-panel fit would keep the row fitting and // quietly restate the relative sizes of the exhibits. assert.equal(new Set(plan.boxes.map((b) => b.w)).size, 1); assert.equal(new Set(plan.boxes.map((b) => b.h)).size, 1); assert.ok(plan.boxes[0].h < 1024); }); test("panelLayout: the gaps keep their width while the pictures give way", () => { const plan = panelLayout( [{ width: 2000, height: 1000 }, { width: 2000, height: 1000 }], { areaW: 1920, areaH: 1024, gap: 48 }, ); assert.equal(plan.gap, 48); assert.equal(plan.totalWidth - plan.boxes.reduce((a, b) => a + b.w, 0), 48); assert.ok(plan.totalWidth <= 1920); }); test("panelLayout: two to four, and a gap that leaves room", () => { const area = { areaW: 1920, areaH: 1024, gap: 40 }; assert.throws(() => panelLayout([{ width: 1, height: 1 }], area), /2 to 4 images/); assert.throws( () => panelLayout(new Array(5).fill({ width: 1, height: 1 }), area), /2 to 4 images \(got 5\)/, ); assert.throws( () => panelLayout([{ width: 0, height: 1 }, { width: 1, height: 1 }], area), /panels\[0\]: no usable dimensions/, ); assert.throws( () => panelLayout([{ width: 1, height: 1 }, { width: 1, height: 1 }], { ...area, gap: 2000 }), /gap 2000 leaves no room for 2 panels across 1920px/, ); });