Files
rplace/test/lib/image-transform.test.js
T
tiennm99 fc0121fd48 feat(importer): phase 3 — flip / rotate transforms
Pure-function transformRgba in src/lib/image-transform.js supports flip H,
flip V, and rotation in 90° steps (fixed application order: rotate → flipH
→ flipV). Wired into the importer pipeline before resize with buttons for
each op; ±90° rotations auto-swap the resize dims so output aspect tracks
the rotation. CLI gains --flip-h, --flip-v, --rotate flags using the same
shared module.

8 new unit tests cover identity, each flip, 90/180/270 rotation, the
flipH+flipV ≡ 180° identity, and invalid rotation rejection.
2026-04-17 11:49:54 +07:00

97 lines
3.5 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { transformRgba } from '../../src/lib/image-transform.js';
/** Build an RGBA buffer where pixel (x, y) has red = (y*w+x+1)*10, green = y, blue = x.
* Unique per-pixel so we can assert exact placement after transforms. */
function build(w, h) {
const out = new Uint8ClampedArray(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const i = (y * w + x) * 4;
out[i] = (y * w + x + 1) * 10;
out[i + 1] = y;
out[i + 2] = x;
out[i + 3] = 255;
}
}
return out;
}
function pixel(rgba, w, x, y) {
const i = (y * w + x) * 4;
return [rgba[i], rgba[i + 1], rgba[i + 2], rgba[i + 3]];
}
describe('transformRgba', () => {
it('identity (no flips, rotation=0) returns input copy', () => {
const src = build(2, 3);
const { rgba, width, height } = transformRgba(src, 2, 3);
expect(width).toBe(2);
expect(height).toBe(3);
expect(rgba).toEqual(src);
expect(rgba).not.toBe(src);
});
it('flipH mirrors columns', () => {
const src = build(3, 2);
const { rgba, width, height } = transformRgba(src, 3, 2, { flipH: true });
expect(width).toBe(3);
expect(height).toBe(2);
// Row 0: src cols 0,1,2 → dst cols 2,1,0
expect(pixel(rgba, 3, 0, 0)).toEqual(pixel(src, 3, 2, 0));
expect(pixel(rgba, 3, 2, 0)).toEqual(pixel(src, 3, 0, 0));
});
it('flipV mirrors rows', () => {
const src = build(2, 3);
const { rgba } = transformRgba(src, 2, 3, { flipV: true });
expect(pixel(rgba, 2, 0, 0)).toEqual(pixel(src, 2, 0, 2));
expect(pixel(rgba, 2, 0, 2)).toEqual(pixel(src, 2, 0, 0));
});
it('rotate 90° CW swaps dims and maps top-left → top-right', () => {
const src = build(2, 3); // 2 wide, 3 tall
const { rgba, width, height } = transformRgba(src, 2, 3, { rotation: 90 });
expect(width).toBe(3); // h
expect(height).toBe(2); // w
// src (0,0) is top-left → dst top-right, i.e. (width-1, 0) = (2, 0)
expect(pixel(rgba, 3, 2, 0)).toEqual(pixel(src, 2, 0, 0));
// src (1, 0) top-right → dst bottom-right (2, 1)
expect(pixel(rgba, 3, 2, 1)).toEqual(pixel(src, 2, 1, 0));
// src (0, 2) bottom-left → dst top-left (0, 0)
expect(pixel(rgba, 3, 0, 0)).toEqual(pixel(src, 2, 0, 2));
});
it('rotate 180° flips both axes', () => {
const src = build(2, 3);
const { rgba, width, height } = transformRgba(src, 2, 3, { rotation: 180 });
expect(width).toBe(2);
expect(height).toBe(3);
expect(pixel(rgba, 2, 0, 0)).toEqual(pixel(src, 2, 1, 2));
expect(pixel(rgba, 2, 1, 2)).toEqual(pixel(src, 2, 0, 0));
});
it('rotate 270° CW is inverse of 90° CW', () => {
const src = build(2, 3);
const r90 = transformRgba(src, 2, 3, { rotation: 90 });
const back = transformRgba(r90.rgba, r90.width, r90.height, { rotation: 270 });
expect(back.width).toBe(2);
expect(back.height).toBe(3);
expect(back.rgba).toEqual(new Uint8ClampedArray(src));
});
it('flipH + flipV equals 180° rotation (for square or rectangular)', () => {
const src = build(3, 2);
const flipped = transformRgba(src, 3, 2, { flipH: true, flipV: true });
const rotated = transformRgba(src, 3, 2, { rotation: 180 });
expect(flipped.rgba).toEqual(rotated.rgba);
expect(flipped.width).toBe(rotated.width);
expect(flipped.height).toBe(rotated.height);
});
it('throws on unsupported rotation', () => {
const src = build(2, 2);
expect(() => transformRgba(src, 2, 2, { rotation: 45 })).toThrow();
});
});