harelcain commited on
Commit
d283c04
·
verified ·
1 Parent(s): 53bf5b7

Upload 19 files

Browse files
tests/bch.test.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from 'vitest';
2
+ import { BchCodec } from '../core/bch.js';
3
+
4
+ describe('BCH', () => {
5
+ const bch63 = new BchCodec({ n: 63, k: 36, t: 5, m: 6 });
6
+
7
+ it('should encode and decode without errors (63,36,5)', () => {
8
+ const message = new Uint8Array(36);
9
+ for (let i = 0; i < 36; i++) message[i] = Math.random() > 0.5 ? 1 : 0;
10
+
11
+ const codeword = bch63.encode(message);
12
+ expect(codeword.length).toBe(63);
13
+
14
+ const decoded = bch63.decode(codeword);
15
+ expect(decoded).not.toBeNull();
16
+ expect(Array.from(decoded!)).toEqual(Array.from(message));
17
+ });
18
+
19
+ it('should correct up to t=5 errors (63,36,5)', () => {
20
+ const message = new Uint8Array(36);
21
+ for (let i = 0; i < 36; i++) message[i] = Math.random() > 0.5 ? 1 : 0;
22
+
23
+ const codeword = bch63.encode(message);
24
+
25
+ // Introduce 5 errors at random positions
26
+ const errorPositions = new Set<number>();
27
+ while (errorPositions.size < 5) {
28
+ errorPositions.add(Math.floor(Math.random() * 63));
29
+ }
30
+ const corrupted = new Uint8Array(codeword);
31
+ for (const pos of errorPositions) {
32
+ corrupted[pos] ^= 1;
33
+ }
34
+
35
+ const decoded = bch63.decode(corrupted);
36
+ expect(decoded).not.toBeNull();
37
+ expect(Array.from(decoded!)).toEqual(Array.from(message));
38
+ });
39
+
40
+ it('should handle zero message', () => {
41
+ const message = new Uint8Array(36); // all zeros
42
+ const codeword = bch63.encode(message);
43
+ const decoded = bch63.decode(codeword);
44
+ expect(decoded).not.toBeNull();
45
+ expect(Array.from(decoded!)).toEqual(Array.from(message));
46
+ });
47
+ });
tests/crc.test.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from 'vitest';
2
+ import { crcAppend, crcVerify } from '../core/crc.js';
3
+
4
+ describe('CRC', () => {
5
+ for (const bits of [4, 8, 16] as const) {
6
+ it(`should append and verify CRC-${bits}`, () => {
7
+ const data = new Uint8Array(32);
8
+ for (let i = 0; i < 32; i++) data[i] = Math.random() > 0.5 ? 1 : 0;
9
+
10
+ const withCrc = crcAppend(data, bits);
11
+ expect(withCrc.length).toBe(32 + bits);
12
+
13
+ const verified = crcVerify(withCrc, bits);
14
+ expect(verified).not.toBeNull();
15
+ expect(Array.from(verified!)).toEqual(Array.from(data));
16
+ });
17
+
18
+ it(`should reject corrupted data with CRC-${bits}`, () => {
19
+ const data = new Uint8Array(32);
20
+ for (let i = 0; i < 32; i++) data[i] = Math.random() > 0.5 ? 1 : 0;
21
+
22
+ const withCrc = crcAppend(data, bits);
23
+ // Flip a bit in the data
24
+ withCrc[5] ^= 1;
25
+
26
+ const verified = crcVerify(withCrc, bits);
27
+ expect(verified).toBeNull();
28
+ });
29
+ }
30
+ });
tests/dct.test.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from 'vitest';
2
+ import { dctForward8x8, dctInverse8x8 } from '../core/dct.js';
3
+
4
+ describe('DCT', () => {
5
+ it('should round-trip an 8x8 block', () => {
6
+ const block = new Float64Array(64);
7
+ for (let i = 0; i < 64; i++) {
8
+ block[i] = Math.random() * 255;
9
+ }
10
+ const original = new Float64Array(block);
11
+
12
+ dctForward8x8(block);
13
+ dctInverse8x8(block);
14
+
15
+ for (let i = 0; i < 64; i++) {
16
+ expect(block[i]).toBeCloseTo(original[i], 6);
17
+ }
18
+ });
19
+
20
+ it('should produce DC coefficient as mean of block', () => {
21
+ const block = new Float64Array(64);
22
+ let sum = 0;
23
+ for (let i = 0; i < 64; i++) {
24
+ block[i] = 100 + Math.random() * 10;
25
+ sum += block[i];
26
+ }
27
+ const mean = sum / 64;
28
+
29
+ dctForward8x8(block);
30
+
31
+ // DC coefficient should be proportional to the mean
32
+ // DC = alpha(0) * alpha(0) * sum = (1/8) * sum = mean * 8 * (1/8) = mean * sqrt(64)/...
33
+ // Actually: DC = (1/sqrt(8)) * (1/sqrt(8)) * sum(block) = sum/8
34
+ expect(block[0]).toBeCloseTo(sum / 8, 4);
35
+ });
36
+ });
tests/dmqim.test.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from 'vitest';
2
+ import { dmqimEmbed, dmqimExtractSoft, dmqimExtractHard } from '../core/dmqim.js';
3
+
4
+ describe('DM-QIM', () => {
5
+ it('should embed and extract bit 0 correctly', () => {
6
+ const coeff = 42.5;
7
+ const delta = 10;
8
+ const dither = 3.7;
9
+
10
+ const embedded = dmqimEmbed(coeff, 0, delta, dither);
11
+ const hard = dmqimExtractHard(embedded, delta, dither);
12
+ expect(hard).toBe(0);
13
+ });
14
+
15
+ it('should embed and extract bit 1 correctly', () => {
16
+ const coeff = 42.5;
17
+ const delta = 10;
18
+ const dither = 3.7;
19
+
20
+ const embedded = dmqimEmbed(coeff, 1, delta, dither);
21
+ const hard = dmqimExtractHard(embedded, delta, dither);
22
+ expect(hard).toBe(1);
23
+ });
24
+
25
+ it('should produce correct soft decisions', () => {
26
+ const delta = 20;
27
+ const dither = 5.0;
28
+
29
+ for (let bit = 0; bit <= 1; bit++) {
30
+ const embedded = dmqimEmbed(50, bit, delta, dither);
31
+ const soft = dmqimExtractSoft(embedded, delta, dither);
32
+
33
+ if (bit === 1) {
34
+ expect(soft).toBeGreaterThan(0);
35
+ } else {
36
+ expect(soft).toBeLessThanOrEqual(0);
37
+ }
38
+ }
39
+ });
40
+
41
+ it('should work with various coefficient values and dithers', () => {
42
+ const delta = 15;
43
+ for (let trial = 0; trial < 100; trial++) {
44
+ const coeff = (Math.random() - 0.5) * 200;
45
+ const dither = (Math.random() - 0.5) * delta;
46
+ const bit = Math.random() > 0.5 ? 1 : 0;
47
+
48
+ const embedded = dmqimEmbed(coeff, bit, delta, dither);
49
+ const extracted = dmqimExtractHard(embedded, delta, dither);
50
+ expect(extracted).toBe(bit);
51
+ }
52
+ });
53
+ });
tests/dwt.test.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createBuffer2D, dwtForward, dwtInverse, yPlaneToBuffer, bufferToYPlane } from '../core/dwt.js';
3
+
4
+ describe('DWT', () => {
5
+ it('should round-trip a simple buffer (1 level)', () => {
6
+ const width = 16;
7
+ const height = 16;
8
+ const buf = createBuffer2D(width, height);
9
+ for (let i = 0; i < buf.data.length; i++) {
10
+ buf.data[i] = Math.round(Math.random() * 255);
11
+ }
12
+ const original = new Float64Array(buf.data);
13
+
14
+ const { buf: transformed, dims } = dwtForward(buf, 1);
15
+ dwtInverse(transformed, dims);
16
+
17
+ for (let i = 0; i < original.length; i++) {
18
+ expect(transformed.data[i]).toBeCloseTo(original[i], 8);
19
+ }
20
+ });
21
+
22
+ it('should round-trip a buffer (2 levels)', () => {
23
+ const width = 64;
24
+ const height = 64;
25
+ const buf = createBuffer2D(width, height);
26
+ for (let i = 0; i < buf.data.length; i++) {
27
+ buf.data[i] = Math.round(Math.random() * 255);
28
+ }
29
+ const original = new Float64Array(buf.data);
30
+
31
+ const { buf: transformed, dims } = dwtForward(buf, 2);
32
+ dwtInverse(transformed, dims);
33
+
34
+ for (let i = 0; i < original.length; i++) {
35
+ expect(transformed.data[i]).toBeCloseTo(original[i], 8);
36
+ }
37
+ });
38
+
39
+ it('should convert Y plane to buffer and back', () => {
40
+ const width = 32;
41
+ const height = 32;
42
+ const yPlane = new Uint8Array(width * height);
43
+ for (let i = 0; i < yPlane.length; i++) {
44
+ yPlane[i] = Math.floor(Math.random() * 256);
45
+ }
46
+
47
+ const buf = yPlaneToBuffer(yPlane, width, height);
48
+ const result = bufferToYPlane(buf);
49
+
50
+ expect(result).toEqual(yPlane);
51
+ });
52
+ });
tests/roundtrip.test.ts ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect } from 'vitest';
2
+ import { embedWatermark } from '../core/embedder.js';
3
+ import { detectWatermark, detectWatermarkMultiFrame } from '../core/detector.js';
4
+ import { getPreset } from '../core/presets.js';
5
+ import type { PresetName } from '../core/types.js';
6
+
7
+ /**
8
+ * Crop a Y plane by removing pixels from the edges.
9
+ */
10
+ function cropYPlane(
11
+ yPlane: Uint8Array,
12
+ width: number,
13
+ height: number,
14
+ left: number,
15
+ top: number,
16
+ right: number,
17
+ bottom: number,
18
+ ): { cropped: Uint8Array; croppedW: number; croppedH: number } {
19
+ const croppedW = width - left - right;
20
+ const croppedH = height - top - bottom;
21
+ const cropped = new Uint8Array(croppedW * croppedH);
22
+ for (let y = 0; y < croppedH; y++) {
23
+ for (let x = 0; x < croppedW; x++) {
24
+ cropped[y * croppedW + x] = yPlane[(y + top) * width + (x + left)];
25
+ }
26
+ }
27
+ return { cropped, croppedW, croppedH };
28
+ }
29
+
30
+ /**
31
+ * Generate a synthetic Y plane (gradient + noise)
32
+ */
33
+ function generateTestFrame(width: number, height: number): Uint8Array {
34
+ const frame = new Uint8Array(width * height);
35
+ for (let y = 0; y < height; y++) {
36
+ for (let x = 0; x < width; x++) {
37
+ const gradient = ((x + y) / (width + height)) * 200 + 20;
38
+ const noise = (Math.random() - 0.5) * 20;
39
+ frame[y * width + x] = Math.max(0, Math.min(255, Math.round(gradient + noise)));
40
+ }
41
+ }
42
+ return frame;
43
+ }
44
+
45
+ describe('Embed → Detect Round-trip', () => {
46
+ const width = 512;
47
+ const height = 512;
48
+ const payload = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]);
49
+ const key = 'test-secret-key-2024';
50
+
51
+ // Test the presets that should work with a 512x512 frame
52
+ for (const presetName of ['light', 'moderate', 'strong'] as PresetName[]) {
53
+ it(`should round-trip with ${presetName} preset`, () => {
54
+ const config = getPreset(presetName);
55
+ const frame = generateTestFrame(width, height);
56
+
57
+ // Embed
58
+ const embedResult = embedWatermark(frame, width, height, payload, key, config);
59
+ expect(embedResult.psnr).toBeGreaterThan(15); // Higher presets sacrifice invisibility for robustness
60
+
61
+ // Detect
62
+ const detectResult = detectWatermark(embedResult.yPlane, width, height, key, config);
63
+ expect(detectResult.detected).toBe(true);
64
+ expect(detectResult.payload).not.toBeNull();
65
+ expect(Array.from(detectResult.payload!)).toEqual(Array.from(payload));
66
+ });
67
+ }
68
+
69
+ it('should not detect watermark with wrong key', () => {
70
+ const config = getPreset('moderate');
71
+ const frame = generateTestFrame(width, height);
72
+
73
+ const embedResult = embedWatermark(frame, width, height, payload, key, config);
74
+
75
+ const detectResult = detectWatermark(embedResult.yPlane, width, height, 'wrong-key', config);
76
+ expect(detectResult.detected).toBe(false);
77
+ });
78
+
79
+ it('should not detect watermark on unwatermarked frame', () => {
80
+ const config = getPreset('moderate');
81
+ const frame = generateTestFrame(width, height);
82
+
83
+ const detectResult = detectWatermark(frame, width, height, key, config);
84
+ expect(detectResult.detected).toBe(false);
85
+ });
86
+ });
87
+
88
+ describe('Crop-resilient detection', () => {
89
+ const width = 512;
90
+ const height = 512;
91
+ const payload = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]);
92
+ const key = 'test-secret-key-2024';
93
+
94
+ it('should detect after arbitrary crop with multiple frames', () => {
95
+ const config = getPreset('strong');
96
+ // Generate multiple distinct frames (simulating video — at least 32 frames)
97
+ const frames: Uint8Array[] = [];
98
+ for (let i = 0; i < 32; i++) {
99
+ const frame = generateTestFrame(width, height);
100
+ const embedResult = embedWatermark(frame, width, height, payload, key, config);
101
+ // Crop by arbitrary offset (breaks DWT pixel pairing)
102
+ const { cropped } = cropYPlane(
103
+ embedResult.yPlane, width, height, 7, 3, 5, 9
104
+ );
105
+ frames.push(cropped);
106
+ }
107
+ const croppedW = width - 7 - 5;
108
+ const croppedH = height - 3 - 9;
109
+
110
+ const detectResult = detectWatermarkMultiFrame(
111
+ frames, croppedW, croppedH, key, config, { cropResilient: true }
112
+ );
113
+ expect(detectResult.detected).toBe(true);
114
+ expect(detectResult.payload).not.toBeNull();
115
+ expect(Array.from(detectResult.payload!)).toEqual(Array.from(payload));
116
+ });
117
+
118
+ it('should detect after large crop with multiple frames', () => {
119
+ const config = getPreset('strong');
120
+ const frames: Uint8Array[] = [];
121
+ for (let i = 0; i < 32; i++) {
122
+ const frame = generateTestFrame(width, height);
123
+ const embedResult = embedWatermark(frame, width, height, payload, key, config);
124
+ const { cropped } = cropYPlane(
125
+ embedResult.yPlane, width, height, 50, 50, 50, 50
126
+ );
127
+ frames.push(cropped);
128
+ }
129
+ const croppedW = width - 100;
130
+ const croppedH = height - 100;
131
+
132
+ const detectResult = detectWatermarkMultiFrame(
133
+ frames, croppedW, croppedH, key, config, { cropResilient: true }
134
+ );
135
+ expect(detectResult.detected).toBe(true);
136
+ expect(detectResult.payload).not.toBeNull();
137
+ expect(Array.from(detectResult.payload!)).toEqual(Array.from(payload));
138
+ });
139
+ });