137 lines
3.8 KiB
JavaScript
137 lines
3.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
applyMaskToImageData,
|
|
convertImageData,
|
|
loadImage,
|
|
prepareImageForCanvas,
|
|
tensorToImageData,
|
|
validateImageData,
|
|
} from '../js/media/image_utils.js';
|
|
import { ErrorTypes } from '../js/shared/error_handler.js';
|
|
|
|
test('image validation accepts tensor-like payloads and normalizes array data', () => {
|
|
const payload = [{
|
|
shape: [1, 1, 1, 3],
|
|
data: [0.1, 0.2, 0.3],
|
|
}];
|
|
|
|
assert.equal(validateImageData(payload), true);
|
|
assert.ok(payload[0].data instanceof Float32Array);
|
|
assert.equal(validateImageData(null), false);
|
|
assert.equal(validateImageData({ shape: [1, 1, 1, 3] }), false);
|
|
});
|
|
|
|
test('image conversion maps RGB values and mask values to RGBA bytes', () => {
|
|
const image = convertImageData({
|
|
shape: [1, 1, 2, 3],
|
|
data: new Float32Array([0, 0.5, 1, 1, 0, 0]),
|
|
});
|
|
|
|
assert.equal(image.width, 2);
|
|
assert.equal(image.height, 1);
|
|
assert.deepEqual([...image.data], [0, 128, 255, 255, 255, 0, 0, 255]);
|
|
|
|
const masked = applyMaskToImageData(image, {
|
|
shape: [1, 1, 2, 1],
|
|
data: new Float32Array([0, 0.5]),
|
|
});
|
|
assert.deepEqual([...masked.data], [0, 128, 255, 0, 255, 0, 0, 128]);
|
|
});
|
|
|
|
test('prepareImageForCanvas returns pixels and reports validation errors', async () => {
|
|
const result = await prepareImageForCanvas({
|
|
shape: [1, 1, 1, 3],
|
|
data: new Float32Array([0.25, 0.5, 0.75]),
|
|
});
|
|
|
|
assert.deepEqual([...result.data], [64, 128, 191, 255]);
|
|
|
|
await assert.rejects(
|
|
() => prepareImageForCanvas(null),
|
|
error => error.type === ErrorTypes.VALIDATION && error.message === 'Invalid input image format'
|
|
);
|
|
});
|
|
|
|
test('convertImageData and prepareImageForCanvas preserve identical RGB conversion semantics', async () => {
|
|
const tensor = {
|
|
shape: [1, 1, 2, 3],
|
|
data: new Float32Array([-0.1, 0.5, 1.2, 0.25, 0.5, 0.75]),
|
|
};
|
|
|
|
const converted = convertImageData(tensor);
|
|
const prepared = await prepareImageForCanvas([tensor]);
|
|
|
|
assert.equal(converted.width, prepared.width);
|
|
assert.equal(converted.height, prepared.height);
|
|
assert.deepEqual([...converted.data], [...prepared.data]);
|
|
});
|
|
|
|
test('tensorToImageData supports grayscale masks without a browser canvas', () => {
|
|
const originalImageData = globalThis.ImageData;
|
|
globalThis.ImageData = class {
|
|
constructor(width, height) {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.data = new Uint8ClampedArray(width * height * 4);
|
|
}
|
|
};
|
|
|
|
try {
|
|
const imageData = tensorToImageData({
|
|
shape: [1, 1, 2, 1],
|
|
data: new Float32Array([0, 1]),
|
|
}, 'grayscale');
|
|
|
|
assert.deepEqual([...imageData.data], [0, 0, 0, 255, 255, 255, 255, 255]);
|
|
} finally {
|
|
if (originalImageData === undefined) {
|
|
delete globalThis.ImageData;
|
|
} else {
|
|
globalThis.ImageData = originalImageData;
|
|
}
|
|
}
|
|
});
|
|
|
|
test('image loading resolves loaded images and preserves load errors', async () => {
|
|
const originalImage = globalThis.Image;
|
|
globalThis.Image = class {
|
|
constructor() {
|
|
this.onload = null;
|
|
this.onerror = null;
|
|
this.width = 4;
|
|
this.height = 3;
|
|
}
|
|
|
|
set src(value) {
|
|
this.source = value;
|
|
queueMicrotask(() => {
|
|
if (value === 'bad-image') {
|
|
this.onerror?.(new Error('image decode failed'));
|
|
} else {
|
|
this.onload?.();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
try {
|
|
const image = await loadImage('good-image');
|
|
assert.equal(image.source, 'good-image');
|
|
assert.equal(image.width, 4);
|
|
assert.equal(image.height, 3);
|
|
|
|
await assert.rejects(
|
|
() => loadImage('bad-image'),
|
|
error => error.message === 'image decode failed'
|
|
);
|
|
} finally {
|
|
if (originalImage === undefined) {
|
|
delete globalThis.Image;
|
|
} else {
|
|
globalThis.Image = originalImage;
|
|
}
|
|
}
|
|
});
|