57 lines
3.5 KiB
JavaScript
57 lines
3.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createRequire } from 'node:module';
|
|
const { visualFingerprint, hammingDistance } = createRequire(import.meta.url)('../packages/core/visual-hash.cjs');
|
|
function image(width = 64, height = 64, layout = 0, brightness = 0) {
|
|
const rgba = new Uint8Array(width * height * 4);
|
|
for (let y = 0; y < height; y++) for (let x = 0; x < width; x++) {
|
|
const nx = x / width, ny = y / height;
|
|
let value = 220;
|
|
if (!layout) {
|
|
if (ny < 0.2) value = 45;
|
|
if (nx > 0.2 && nx < 0.8 && ny > 0.35 && ny < 0.8) value = 140;
|
|
if (nx > 0.3 && nx < 0.7 && ny > 0.5 && ny < 0.6) value = 65;
|
|
} else {
|
|
if (nx < 0.25) value = 65;
|
|
if (nx > 0.4 && nx < 0.9 && ny > 0.2 && ny < 0.45) value = 120;
|
|
if (nx > 0.6 && ny > 0.7) value = 45;
|
|
}
|
|
const index = (y * width + x) * 4;
|
|
rgba[index] = rgba[index + 1] = rgba[index + 2] = value + brightness; rgba[index + 3] = 255;
|
|
}
|
|
return { rgba, width, height };
|
|
}
|
|
test('DCT fingerprints are deterministic, brightness stable and resize tolerant', () => {
|
|
const original = visualFingerprint(image());
|
|
assert.equal(original.algorithm, 'phash-dct32-v1'); assert.match(original.hash, /^[0-9a-f]{16}$/); assert.equal(original.usable, true);
|
|
assert.deepEqual(visualFingerprint(image()), original);
|
|
assert.equal(hammingDistance(original.hash, visualFingerprint(image(64, 64, 0, 12)).hash), 0);
|
|
assert.ok(hammingDistance(original.hash, visualFingerprint(image(128, 128)).hash) <= 6);
|
|
});
|
|
test('blank low contrast and transparent images are unusable', () => {
|
|
for (const value of [0, 127, 255]) {
|
|
const rgba = new Uint8Array(64 * 64 * 4);
|
|
for (let index = 0; index < rgba.length; index += 4) { rgba[index] = rgba[index + 1] = rgba[index + 2] = value; rgba[index + 3] = 255; }
|
|
const result = visualFingerprint({ rgba, width: 64, height: 64 }); assert.equal(result.usable, false); assert.equal(result.quality.contrast, 0);
|
|
}
|
|
const transparent = image(); for (let index = 3; index < transparent.rgba.length; index += 4) transparent.rgba[index] = 0;
|
|
assert.equal(visualFingerprint(transparent).usable, false);
|
|
});
|
|
test('different layouts exceed the conservative similarity threshold', () => {
|
|
const distance = hammingDistance(visualFingerprint(image()).hash, visualFingerprint(image(64, 64, 1)).hash);
|
|
assert.ok(distance > 10, `distance ${distance}`);
|
|
});
|
|
test('crop normalizes selected region and clamps safe partial intersections', () => {
|
|
const input = image();
|
|
assert.deepEqual(visualFingerprint({ ...input, region: { x: -10, y: -10, width: 74, height: 74 } }), visualFingerprint(input));
|
|
const cropped = visualFingerprint({ ...input, region: { x: 15, y: 15, width: 40, height: 40 } });
|
|
assert.notEqual(cropped.hash, visualFingerprint(input).hash);
|
|
});
|
|
test('malformed dimensions buffers crop and hash formats are rejected', () => {
|
|
const input = image();
|
|
for (const change of [{ width: -1 }, { width: 1.5 }, { width: 9000 }, { height: 9000 }, { rgba: [1, 2, 3] }, { rgba: new Uint8Array(4) }, { region: { x: 100, y: 100, width: 3, height: 3 } }, { region: { x: 0, y: 0, width: 0, height: 4 } }, { region: { x: Infinity, y: 0, width: 4, height: 4 } }]) assert.throws(() => visualFingerprint({ ...input, ...change }));
|
|
assert.throws(() => visualFingerprint({ rgba: new Uint8Array(4), width: 8192, height: 8192 }));
|
|
assert.throws(() => hammingDistance('A'.repeat(16), '0'.repeat(16))); assert.throws(() => hammingDistance('0', '0'.repeat(16)));
|
|
assert.equal(hammingDistance('0'.repeat(16), 'f'.repeat(16)), 64);
|
|
});
|