import { describe, expect, it } from "vitest"; import { ASPECT_H, ASPECT_W, FILL_TARGET, OUTPUT_PX_MIN, } from "../lib/pipeline/constants.js"; import { computeCropTarget, cropTo34, findSlabBounds, } from "../lib/pipeline/crop.js"; import { simpleAxis, tv6LargeSourceFixture, tv7SmallSourceFixture, } from "./fixtures/crop.js"; describe("TV-6 3:4 crop, 80% fill, transparent PNG, ≥1600px", () => { it("computes crop target dimensions preserving 3:4 aspect and 80% fill at extremes", () => { // Slab 1281×3000 — height limiting, width gets extra margin const t1 = computeCropTarget(1281, 3000); expect(Math.abs(t1.width / t1.height - ASPECT_W / ASPECT_H)).toBeLessThan(0.01); // Width-limiting case: slab 2000×1600 const t2 = computeCropTarget(2000, 1600); expect(Math.abs(t2.width / t2.height - ASPECT_W / ASPECT_H)).toBeLessThan(0.01); console.log( JSON.stringify({ case: "TV-6", target1: { slabW: 1281, slabH: 3000, frame: t1, aspect: t1.width / t1.height }, target2: { slabW: 2000, slabH: 1600, frame: t2, aspect: t2.width / t2.height }, }), ); }); it("finds slab bounding box correctly", () => { const fx = tv6LargeSourceFixture(); const bounds = findSlabBounds(fx.mask, fx.width, fx.height); // findSlabBounds returns inclusive coordinates; fixture uses exclusive. expect(bounds.left).toBe(fx.slabLeft); expect(bounds.top).toBe(fx.slabTop); expect(bounds.right).toBe(fx.slabRight - 1); expect(bounds.bottom).toBe(fx.slabBottom - 1); // WidthPx should match slab dimensions expect(bounds.widthPx).toBe(fx.slabRight - fx.slabLeft); expect(bounds.heightPx).toBe(fx.slabBottom - fx.slabTop); console.log( JSON.stringify({ case: "TV-6", bounds: { left: bounds.left, top: bounds.top, right: bounds.right, bottom: bounds.bottom, widthPx: bounds.widthPx, heightPx: bounds.heightPx, }, }), ); }); it("crops large source to 3:4 with 80% fill, shorter side ≥ 1600, transparent PNG", () => { const fx = tv6LargeSourceFixture(); const { axis, lengthIn } = simpleAxis(fx.slabRight - fx.slabLeft, fx.slabBottom - fx.slabTop); const result = cropTo34(fx.image, fx.mask, axis, lengthIn); expect(result.width).toBe(fx.expectedFrameW); expect(result.height).toBe(fx.expectedFrameH); expect(result.meetsMinPx).toBe(true); // Verify 3:4 aspect ratio expect(Math.abs(result.width / result.height - ASPECT_W / ASPECT_H)).toBeLessThan(0.01); // Verify shorter side ≥ OUTPUT_PX_MIN expect(Math.min(result.width, result.height)).toBeGreaterThanOrEqual(OUTPUT_PX_MIN); // Verify transparency: margin pixels have alpha = 0 const marginAlpha = result.image.data[3]; expect(marginAlpha).toBe(0); // Verify slab pixels are opaque (alpha = 255) const slabCenterX = Math.round(result.width / 2); const slabCenterY = Math.round(result.height / 2); const slabCenterAlpha = result.image.data[(slabCenterY * result.width + slabCenterX) * 4 + 3]!; expect(slabCenterAlpha).toBe(255); console.log( JSON.stringify({ case: "TV-6", frameW: result.width, frameH: result.height, shorterSide: Math.min(result.width, result.height), meetsMinPx: result.meetsMinPx, marginAlpha: marginAlpha, slabCenterAlpha: slabCenterAlpha, aspect: result.width / result.height, }), ); }); it("property: fill at extremes equals FILL_TARGET at limiting dimension", () => { const fx = tv6LargeSourceFixture(); const slabW = fx.slabRight - fx.slabLeft; const slabH = fx.slabBottom - fx.slabTop; const target = computeCropTarget(slabW, slabH); const slabAspect = slabW / slabH; const targetAspect = ASPECT_W / ASPECT_H; // At the extremes, fill should be FILL_TARGET const fillAtLimitingDim = slabAspect >= targetAspect ? slabH / target.height : slabW / target.width; expect(fillAtLimitingDim).toBeCloseTo(FILL_TARGET, 3); console.log( JSON.stringify({ case: "TV-6", fillAtLimitingDim, expectedFill: FILL_TARGET, }), ); }); }); describe("TV-7 Too-small source — warn, no upscale, not publishable", () => { it("detects undersized source and refuses upscale", () => { const fx = tv7SmallSourceFixture(); const bounds = findSlabBounds(fx.mask, fx.width, fx.height); const target = computeCropTarget(bounds.widthPx, bounds.heightPx); expect(Math.min(target.width, target.height)).toBeLessThan(OUTPUT_PX_MIN); expect(target.width).toBe(fx.expectedFrameW); expect(target.height).toBe(fx.expectedFrameH); console.log( JSON.stringify({ case: "TV-7", slabW: bounds.widthPx, slabH: bounds.heightPx, frameW: target.width, frameH: target.height, shorterSide: Math.min(target.width, target.height), outputPxMin: OUTPUT_PX_MIN, undersized: Math.min(target.width, target.height) < OUTPUT_PX_MIN, }), ); }); it("cropTo34 returns meetsMinPx = false for undersized source", () => { const fx = tv7SmallSourceFixture(); const { axis, lengthIn } = simpleAxis(fx.slabRight - fx.slabLeft, fx.slabBottom - fx.slabTop); const result = cropTo34(fx.image, fx.mask, axis, lengthIn); expect(result.meetsMinPx).toBe(false); expect(Math.min(result.width, result.height)).toBeLessThan(OUTPUT_PX_MIN); console.log( JSON.stringify({ case: "TV-7", frameW: result.width, frameH: result.height, shorterSide: Math.min(result.width, result.height), meetsMinPx: result.meetsMinPx, }), ); }); it("property: no upscale path — output is never larger than source", () => { // For any source, the crop output dimensions should never exceed // the source dimensions. This ensures no fake upscaling. const fx = tv7SmallSourceFixture(); const { axis, lengthIn } = simpleAxis(fx.slabRight - fx.slabLeft, fx.slabBottom - fx.slabTop); const result = cropTo34(fx.image, fx.mask, axis, lengthIn); expect(result.width).toBeLessThanOrEqual(fx.width); expect(result.height).toBeLessThanOrEqual(fx.height); // Also verify for the large source const fxLarge = tv6LargeSourceFixture(); const { axis: axis2, lengthIn: lengthIn2 } = simpleAxis( fxLarge.slabRight - fxLarge.slabLeft, fxLarge.slabBottom - fxLarge.slabTop, ); const result2 = cropTo34(fxLarge.image, fxLarge.mask, axis2, lengthIn2); expect(result2.width).toBeLessThanOrEqual(fxLarge.width); expect(result2.height).toBeLessThanOrEqual(fxLarge.height); console.log( JSON.stringify({ case: "TV-7", noUpscaleCheck: { small: { source: `${fx.width}x${fx.height}`, output: `${result.width}x${result.height}`, }, large: { source: `${fxLarge.width}x${fxLarge.height}`, output: `${result2.width}x${result2.height}`, }, }, }), ); }); });