first commit
This commit is contained in:
commit
ff482ebe23
251 files changed
+19126
No files matched your search
@@ -0,0 +1,56 @@
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user