Files
siftlode/frontend/src/lib/cropGeometry.test.ts
T
peter fb2c843fa9 fix(downloads): anchor crop overlay to the letterboxed picture (E7 S2)
The video preview is object-fit: contain (the browser default for <video>), so a
non-16:9 source is letter/pillarboxed inside the element box. The crop marquee was
drawn in fractions of the whole box (bars included) and those fractions were then
multiplied by the source resolution, so the emitted ffmpeg crop diverged from what
the selector showed. Anchor the overlay to the computed content rect (and set
object-contain explicitly) so a marquee fraction is a fraction of the real picture;
extract the display-px<->source-px math into cropGeometry.ts with unit tests, and
clamp the even-rounded window in-bounds so a full-frame crop can't overflow.
2026-08-29 05:43:41 +02:00

89 lines
3.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { contentRect, cropToSourcePx } from "./cropGeometry";
describe("contentRect (display-px letterbox mapping)", () => {
it("returns the full box when box aspect matches the video aspect", () => {
// 16:9 picture in a 16:9 box → no bars, content == box
expect(contentRect(1600, 900, 16 / 9)).toEqual({
left: 0,
top: 0,
width: 1600,
height: 900,
});
});
it("pillarboxes (bars left/right) when the box is wider than the picture", () => {
// 16:9 picture in a 2:1 box → full height, narrower centered width
const r = contentRect(2000, 1000, 16 / 9)!;
expect(r.height).toBe(1000);
expect(r.width).toBeCloseTo(1000 * (16 / 9), 3);
expect(r.top).toBe(0);
expect(r.left).toBeCloseTo((2000 - 1000 * (16 / 9)) / 2, 3);
});
it("letterboxes (bars top/bottom) when the box is taller than the picture", () => {
// 16:9 picture in a 1:1 box → full width, shorter centered height
const r = contentRect(1000, 1000, 16 / 9)!;
expect(r.width).toBe(1000);
expect(r.height).toBeCloseTo(1000 / (16 / 9), 3);
expect(r.left).toBe(0);
expect(r.top).toBeCloseTo((1000 - 1000 / (16 / 9)) / 2, 3);
});
it("returns null for unmeasurable inputs", () => {
expect(contentRect(0, 900, 16 / 9)).toBeNull();
expect(contentRect(1600, 0, 16 / 9)).toBeNull();
expect(contentRect(1600, 900, 0)).toBeNull();
expect(contentRect(1600, 900, NaN)).toBeNull();
});
});
describe("cropToSourcePx (fraction-of-content → even source px)", () => {
it("scales content fractions straight to source pixels", () => {
// A marquee anchored to the content rect: a fraction is a fraction of the real picture.
expect(cropToSourcePx({ x: 0.25, y: 0.25, w: 0.5, h: 0.5 }, 1920, 1080)).toEqual({
x: 480,
y: 270,
w: 960,
h: 540,
});
});
it("rounds to even dimensions", () => {
// 0.1 * 1920 = 192 (even); 0.333 * 1080 = 359.64 → 360
const c = cropToSourcePx({ x: 0.1, y: 0.333, w: 0.2, h: 0.111 }, 1920, 1080)!;
expect(c.w % 2).toBe(0);
expect(c.h % 2).toBe(0);
expect(c.x % 2).toBe(0);
expect(c.y % 2).toBe(0);
});
it("allows a top-left origin (x/y may be 0)", () => {
const c = cropToSourcePx({ x: 0, y: 0, w: 0.5, h: 0.5 }, 1920, 1080)!;
expect(c.x).toBe(0);
expect(c.y).toBe(0);
});
it("clamps the window inside the frame so even-rounding can't overflow", () => {
// A full-frame crop must stay exactly at the source size, not round past it.
const c = cropToSourcePx({ x: 0, y: 0, w: 1, h: 1 }, 1921, 1081)!;
expect(c.w).toBeLessThanOrEqual(1921);
expect(c.h).toBeLessThanOrEqual(1081);
expect(c.x + c.w).toBeLessThanOrEqual(1921);
expect(c.y + c.h).toBeLessThanOrEqual(1081);
});
it("keeps x+w within the frame for an off-square region near the right edge", () => {
const srcW = 1280;
const srcH = 720;
const c = cropToSourcePx({ x: 0.7, y: 0.1, w: 0.3, h: 0.8 }, srcW, srcH)!;
expect(c.x + c.w).toBeLessThanOrEqual(srcW);
expect(c.y + c.h).toBeLessThanOrEqual(srcH);
});
it("returns null for an unusable source size", () => {
expect(cropToSourcePx({ x: 0, y: 0, w: 1, h: 1 }, 0, 1080)).toBeNull();
expect(cropToSourcePx({ x: 0, y: 0, w: 1, h: 1 }, 1920, 0)).toBeNull();
});
});