Files
rplace/test/lib/image-resize.test.js
tiennm99 f94f1dc5f5 feat(importer): phase 1 — resize controls with multiple methods
Add W/H inputs, aspect-ratio lock, resampling method dropdown (nearest /
bilinear / box), "Fit to canvas" (respects current origin) and "1:1" reset
buttons to the import panel. Pipeline now runs resize before palette
quantization so dither/skip-matching operate on the final dimensions.

Shared src/lib/image-resize.js is reused by scripts/image-to-colors.js,
which gains --width, --height, --method flags with aspect-preserve when
only one dimension is provided.
2026-04-17 11:43:11 +07:00

99 lines
3.8 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { resizeRgba } from '../../src/lib/image-resize.js';
/** Build an RGBA buffer from a function (x,y) → [r,g,b,a]. */
function build(w, h, fn) {
const out = new Uint8ClampedArray(w * h * 4);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const [r, g, b, a] = fn(x, y);
const i = (y * w + x) * 4;
out[i] = r; out[i + 1] = g; out[i + 2] = b; out[i + 3] = a;
}
}
return out;
}
describe('resizeRgba', () => {
it('identity (same dims) returns copy with same contents', () => {
const src = build(3, 2, (x, y) => [x * 10, y * 20, 0, 255]);
const out = resizeRgba(src, 3, 2, 3, 2);
expect(out).toEqual(new Uint8ClampedArray(src));
expect(out).not.toBe(src); // must be a fresh buffer
});
it('throws on zero or negative destination dims', () => {
const src = new Uint8ClampedArray(4);
expect(() => resizeRgba(src, 1, 1, 0, 1)).toThrow();
expect(() => resizeRgba(src, 1, 1, 1, -1)).toThrow();
});
it('nearest 2x downscale picks one of each 2x2 block (integer factor)', () => {
// 4x4 where each 2x2 block has a unique color
const src = build(4, 4, (x, y) => {
const bx = x >> 1, by = y >> 1;
return [bx * 80, by * 80, 0, 255];
});
const out = resizeRgba(src, 4, 4, 2, 2, 'nearest');
expect(out.length).toBe(2 * 2 * 4);
// Each output pixel must equal one of the four pixels in its source block.
// For default nearest (floor), 2x downscale picks the top-left of the block.
expect([out[0], out[1], out[2]]).toEqual([0, 0, 0]);
expect([out[4], out[5], out[6]]).toEqual([80, 0, 0]);
expect([out[8], out[9], out[10]]).toEqual([0, 80, 0]);
expect([out[12], out[13], out[14]]).toEqual([80, 80, 0]);
});
it('nearest 2x upscale duplicates pixels', () => {
const src = build(2, 1, (x) => [x * 255, 0, 0, 255]);
const out = resizeRgba(src, 2, 1, 4, 2, 'nearest');
// Each source pixel appears in a 2x2 block
expect([out[0], out[4]]).toEqual([0, 0]);
expect([out[8], out[12]]).toEqual([255, 255]);
expect([out[16], out[20]]).toEqual([0, 0]);
expect([out[24], out[28]]).toEqual([255, 255]);
});
it('bilinear produces an in-between value at the midpoint', () => {
// Two pixels: black (0) and white (255). Midpoint should blend toward 127/128.
const src = build(2, 1, (x) => [x * 255, x * 255, x * 255, 255]);
const out = resizeRgba(src, 2, 1, 3, 1, 'bilinear');
// out[1] is the middle pixel — must be strictly between the endpoints.
const mid = out[4];
expect(mid).toBeGreaterThan(0);
expect(mid).toBeLessThan(255);
// Endpoints are preserved
expect(out[0]).toBe(0);
expect(out[8]).toBe(255);
});
it('box averages a 2x2 block on 2x downscale', () => {
const src = build(2, 2, (x, y) => {
if (x === 0 && y === 0) return [0, 0, 0, 255];
if (x === 1 && y === 0) return [100, 0, 0, 255];
if (x === 0 && y === 1) return [0, 100, 0, 255];
return [100, 100, 0, 255];
});
const out = resizeRgba(src, 2, 2, 1, 1, 'box');
expect(out.length).toBe(4);
expect(out[0]).toBe(50); // avg of 0, 100, 0, 100
expect(out[1]).toBe(50);
expect(out[2]).toBe(0);
expect(out[3]).toBe(255);
});
it('unknown method falls back to nearest', () => {
const src = build(2, 1, (x) => [x * 255, 0, 0, 255]);
const out = resizeRgba(src, 2, 1, 4, 1, /** @type any */ ('mystery'));
// Should be nearest-neighbor output: 0,0,255,255
expect([out[0], out[4], out[8], out[12]]).toEqual([0, 0, 255, 255]);
});
it('preserves alpha channel', () => {
const src = build(2, 1, (x) => [255, 0, 0, x === 0 ? 0 : 128]);
const out = resizeRgba(src, 2, 1, 2, 1, 'nearest');
expect(out[3]).toBe(0);
expect(out[7]).toBe(128);
});
});