import { test } from 'node:test'; import assert from 'node:assert/strict'; import { baseCrop, clampCrop, centred, coverSize, containSize, type CropRel } from '../src/lib/cropmath.ts'; import { coverCrop, containCrop } from '../src/lib/printrender.ts'; /** Deterministischer Zufall — bei einem Fehlschlag ist der Fall reproduzierbar. */ function rng(seed: number) { let s = seed >>> 0; return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 0x1_0000_0000); } test('Server-Einstieg und Browser-Einstieg rechnen identisch (cover)', () => { const r = rng(20260821); for (let i = 0; i < 20000; i++) { const natW = 1 + Math.floor(r() * 8000); const natH = 1 + Math.floor(r() * 8000); const aspect = 0.05 + r() * 20; const server = coverCrop(natW, natH, aspect); // printrender.ts → PDF const browser = baseCrop(natW, natH, aspect, 'cover'); // cropmath.ts → Vorschau assert.deepEqual(browser, server, `cover ${natW}x${natH} @ ${aspect}`); } }); test('Server-Einstieg und Browser-Einstieg rechnen identisch (contain)', () => { const r = rng(7); for (let i = 0; i < 20000; i++) { const natW = 1 + Math.floor(r() * 8000); const natH = 1 + Math.floor(r() * 8000); const aspect = 0.05 + r() * 20; assert.deepEqual(baseCrop(natW, natH, aspect, 'contain'), containCrop(natW, natH, aspect), `contain ${natW}x${natH} @ ${aspect}`); } }); test('cover bleibt im Bild, contain umschließt das ganze Bild', () => { const r = rng(99); const EPS = 1e-9; for (let i = 0; i < 20000; i++) { const natW = 1 + Math.floor(r() * 6000); const natH = 1 + Math.floor(r() * 6000); const aspect = 0.05 + r() * 20; const c = baseCrop(natW, natH, aspect, 'cover'); assert.ok(c.x >= -EPS && c.y >= -EPS && c.x + c.w <= 1 + EPS && c.y + c.h <= 1 + EPS, `cover ragt heraus: ${JSON.stringify(c)}`); const k = baseCrop(natW, natH, aspect, 'contain'); assert.ok(k.x <= EPS && k.y <= EPS && k.x + k.w >= 1 - EPS && k.y + k.h >= 1 - EPS, `contain umschließt nicht: ${JSON.stringify(k)}`); } }); test('Das Seitenverhältnis stimmt — sonst würde verzerrt', () => { const r = rng(4242); for (let i = 0; i < 20000; i++) { const natW = 1 + Math.floor(r() * 6000); const natH = 1 + Math.floor(r() * 6000); const aspect = 0.05 + r() * 20; for (const fit of ['cover', 'contain'] as const) { const c = baseCrop(natW, natH, aspect, fit); // Der Ausschnitt in Pixeln muss exakt das Zielverhältnis haben. const ist = (c.w * natW) / (c.h * natH); assert.ok(Math.abs(ist - aspect) < 1e-9, `${fit} ${natW}x${natH}: Verhältnis ${ist} statt ${aspect}`); } } }); test('Bei passendem Verhältnis ist der Ausschnitt das ganze Bild', () => { for (const [w, h] of [[3000, 2000], [1000, 1000], [350, 450]]) { for (const fit of ['cover', 'contain'] as const) { assert.deepEqual(baseCrop(w, h, w / h, fit), { x: 0, y: 0, w: 1, h: 1 }, `${w}x${h} ${fit}`); } } }); test('Formatwechsel behält den Bildmittelpunkt', () => { // Nutzer hat nach links oben verschoben, wechselt dann von 3:2 auf 35x45. const vorher: CropRel = { x: 0.1, y: 0.05, w: 0.4, h: 0.4 }; const nachher = baseCrop(3000, 2000, 35 / 45, 'cover', vorher); const mitteVorher = vorher.x + vorher.w / 2; const mitteNachher = nachher.x + nachher.w / 2; // In der eingeschränkten Achse kann der Anschlag greifen — er darf den // Mittelpunkt aber nur zum Bild hin verschieben, nicht darüber hinaus. assert.ok(Math.abs(mitteNachher - mitteVorher) < 0.5, `Mitte sprang: ${mitteVorher} → ${mitteNachher}`); assert.ok(nachher.x >= 0 && nachher.x + nachher.w <= 1); }); test('clampCrop hält cover im Bild und lässt contain kontrolliert hinausragen', () => { assert.deepEqual(clampCrop({ x: -5, y: -5, w: 0.5, h: 0.5 }, 'cover'), { x: 0, y: 0, w: 0.5, h: 0.5 }); assert.deepEqual(clampCrop({ x: 5, y: 5, w: 0.5, h: 0.5 }, 'cover'), { x: 0.5, y: 0.5, w: 0.5, h: 0.5 }); // contain: mindestens 5 % des Ausschnitts müssen im Bild bleiben. const weit = clampCrop({ x: -99, y: -99, w: 2, h: 2 }, 'contain'); assert.ok(weit.x + weit.w > 0 && weit.y + weit.h > 0, 'Motiv komplett aus dem Rahmen geschoben'); }); test('centred legt den Ausschnitt um einen Mittelpunkt', () => { assert.deepEqual(centred(0.5, 0.5), { x: 0.25, y: 0.25, w: 0.5, h: 0.5 }); assert.deepEqual(centred(0.2, 0.2, { x: 0, y: 0, w: 1, h: 1 }), { x: 0.4, y: 0.4, w: 0.2, h: 0.2 }); }); test('coverSize und containSize sind zueinander invers im Verhältnis', () => { const c = coverSize(3000, 2000, 0.5), k = containSize(3000, 2000, 0.5); assert.ok(c.w <= 1 + 1e-9 && c.h <= 1 + 1e-9, 'cover darf nicht über 1'); assert.ok(k.w >= 1 - 1e-9 || k.h >= 1 - 1e-9, 'contain muss mindestens eine Kante füllen'); });