@chr33s/pdf-upng 5.0.0 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@chr33s/pdf-upng",
3
3
  "license": "MIT",
4
- "version": "5.0.0",
4
+ "version": "5.0.2",
5
5
  "description": "Small, fast and advanced PNG / APNG encoder and decoder",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
+ "files": [
9
+ "dist/**",
10
+ "src/**"
11
+ ],
8
12
  "exports": {
9
13
  ".": {
10
14
  "types": "./dist/index.d.ts",
@@ -37,7 +41,7 @@
37
41
  "conversion"
38
42
  ],
39
43
  "dependencies": {
40
- "@chr33s/pdf-common": "file:../common"
44
+ "@chr33s/pdf-common": "5.0.2"
41
45
  },
42
46
  "devDependencies": {
43
47
  "typescript": "5.9.3",
package/test/bin.test.ts DELETED
@@ -1,173 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { Bin } from "../src/bin.js";
3
-
4
- describe("nextZero", () => {
5
- test("returns index of first zero starting at p", () => {
6
- const data = new Uint8Array([1, 2, 3, 0, 4]);
7
- expect(Bin.nextZero(data, 0)).toBe(3);
8
- });
9
-
10
- test("returns p when it already points to zero", () => {
11
- const data = new Uint8Array([1, 2, 0, 4]);
12
- expect(Bin.nextZero(data, 2)).toBe(2);
13
- });
14
- });
15
-
16
- describe("readUshort / writeUshort", () => {
17
- test("reads a big-endian unsigned short written by writeUshort", () => {
18
- const buff = new Uint8Array(4);
19
- const value = 0x1234; // 4660
20
- Bin.writeUshort(buff, 1, value);
21
- expect(buff[1]).toBe(0x12);
22
- expect(buff[2]).toBe(0x34);
23
-
24
- const readValue = Bin.readUshort(buff, 1);
25
- expect(readValue).toBe(value);
26
- });
27
-
28
- test("handles minimum and maximum ushort values", () => {
29
- const buff = new Uint8Array(4);
30
-
31
- Bin.writeUshort(buff, 0, 0);
32
- expect(Bin.readUshort(buff, 0)).toBe(0);
33
-
34
- Bin.writeUshort(buff, 0, 0xffff);
35
- expect(Bin.readUshort(buff, 0)).toBe(0xffff);
36
- });
37
- });
38
-
39
- describe("readUint / writeUint", () => {
40
- test("reads a big-endian unsigned int written by writeUint", () => {
41
- const buff = new Uint8Array(8);
42
- const value = 0x12345678; // 305419896
43
- Bin.writeUint(buff, 2, value);
44
-
45
- expect(buff[2]).toBe(0x12);
46
- expect(buff[3]).toBe(0x34);
47
- expect(buff[4]).toBe(0x56);
48
- expect(buff[5]).toBe(0x78);
49
-
50
- const readValue = Bin.readUint(buff, 2);
51
- expect(readValue).toBe(value);
52
- });
53
-
54
- test("handles minimum and maximum 32-bit unsigned values", () => {
55
- const buff = new Uint8Array(4);
56
-
57
- Bin.writeUint(buff, 0, 0);
58
- expect(Bin.readUint(buff, 0)).toBe(0);
59
-
60
- const max = 0xffffffff; // 4294967295
61
- Bin.writeUint(buff, 0, max);
62
- expect(Bin.readUint(buff, 0)).toBe(max);
63
- });
64
- });
65
-
66
- describe("readASCII / writeASCII", () => {
67
- test("writes and reads ASCII strings correctly", () => {
68
- const buff = new Uint8Array(10);
69
- const s = "Hello";
70
- Bin.writeASCII(buff, 2, s);
71
-
72
- // Raw bytes
73
- expect(Array.from(buff.slice(2, 7))).toEqual([
74
- "H".charCodeAt(0),
75
- "e".charCodeAt(0),
76
- "l".charCodeAt(0),
77
- "l".charCodeAt(0),
78
- "o".charCodeAt(0),
79
- ]);
80
-
81
- const read = Bin.readASCII(buff, 2, s.length);
82
- expect(read).toBe(s);
83
- });
84
-
85
- test("can write shorter strings without touching earlier bytes", () => {
86
- const buff = new Uint8Array(5).fill(0xff);
87
- Bin.writeASCII(buff, 1, "A");
88
- expect(buff[0]).toBe(0xff);
89
- expect(buff[1]).toBe("A".charCodeAt(0));
90
- });
91
- });
92
-
93
- describe("readBytes", () => {
94
- test("returns a JS array slice of the specified bytes", () => {
95
- const buff = new Uint8Array([10, 20, 30, 40, 50]);
96
- const arr = Bin.readBytes(buff, 1, 3);
97
- expect(arr).toEqual([20, 30, 40]);
98
- });
99
-
100
- test("works with length 0", () => {
101
- const buff = new Uint8Array([1, 2, 3]);
102
- const arr = Bin.readBytes(buff, 1, 0);
103
- expect(arr).toEqual([]);
104
- });
105
- });
106
-
107
- describe("pad", () => {
108
- test("adds a leading zero for single-character strings", () => {
109
- expect(Bin.pad("a")).toBe("0a");
110
- expect(Bin.pad("1")).toBe("01");
111
- });
112
-
113
- test("returns the original string if length >= 2", () => {
114
- expect(Bin.pad("10")).toBe("10");
115
- expect(Bin.pad("abc")).toBe("abc");
116
- });
117
- });
118
-
119
- describe("readUTF8", () => {
120
- test("decodes valid UTF-8 sequences", () => {
121
- // "hé" in UTF-8
122
- const encoder = new TextEncoder();
123
- const s = "hé";
124
- const bytes = encoder.encode(s); // Uint8Array
125
-
126
- const buff = new Uint8Array(10);
127
- buff.set(bytes, 2);
128
-
129
- const decoded = Bin.readUTF8(buff, 2, bytes.length);
130
- expect(decoded).toBe(s);
131
- });
132
-
133
- test("falls back to ASCII when decodeURIComponent throws", () => {
134
- // Create bytes that produce an invalid percent-escape sequence
135
- // '%' followed by non-hex characters
136
- const _buff = new Uint8Array([37, 71, 71]); // '%GG'
137
- // readUTF8 will create string "%25%47%47", which is valid,
138
- // so we instead force a bad sequence by hand:
139
- // We'll directly test the catch path by constructing an impossible pattern:
140
- //
141
- // To get into catch, we need decodeURIComponent to throw.
142
- // One way is to craft an incomplete escape at end: e.g. "%E2%82"
143
- const invalid = new Uint8Array([
144
- 0xe2, // 226
145
- 0x82, // 130
146
- ]);
147
- const bigBuff = new Uint8Array(10);
148
- bigBuff.set(invalid, 0);
149
-
150
- // Monkey-patch pad+readASCII behavior is not necessary,
151
- // instead we rely on decodeURIComponent("%e2%82") throwing.
152
- const result = Bin.readUTF8(bigBuff, 0, invalid.length);
153
-
154
- // Fallback is ASCII for same bytes
155
- const ascii = Bin.readASCII(bigBuff, 0, invalid.length);
156
- expect(result).toBe(ascii);
157
- });
158
-
159
- test("matches ASCII for plain ASCII strings", () => {
160
- const text = "Hello";
161
- const encoder = new TextEncoder();
162
- const bytes = encoder.encode(text);
163
-
164
- const buff = new Uint8Array(10);
165
- buff.set(bytes, 1);
166
-
167
- const utf8 = Bin.readUTF8(buff, 1, bytes.length);
168
- const ascii = Bin.readASCII(buff, 1, bytes.length);
169
-
170
- expect(utf8).toBe(text);
171
- expect(utf8).toBe(ascii);
172
- });
173
- });
package/test/crc.test.ts DELETED
@@ -1,71 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { CRC } from "../src/crc.js";
3
-
4
- describe("table", () => {
5
- test("has 256 entries", () => {
6
- expect(CRC.table).toBeInstanceOf(Uint32Array);
7
- expect(CRC.table.length).toBe(256);
8
- });
9
- });
10
-
11
- describe("update", () => {
12
- test("returns the same value when updating with zero-length buffer", () => {
13
- const initial = 0x12345678;
14
- const buf = new Uint8Array([1, 2, 3, 4]);
15
- const result = CRC.update(initial, buf, 0, 0);
16
- expect(result).toBe(initial);
17
- });
18
-
19
- test("updates CRC incrementally equivalent to one-shot", () => {
20
- const text = "Hello, world!";
21
- const bytes = new TextEncoder().encode(text);
22
-
23
- const full = CRC.update(0xffffffff, bytes, 0, bytes.length);
24
-
25
- const mid = Math.floor(bytes.length / 2);
26
- const part1 = CRC.update(0xffffffff, bytes, 0, mid);
27
- const part2 = CRC.update(part1, bytes, mid, bytes.length - mid);
28
-
29
- expect(part2 >>> 0).toBe(full >>> 0);
30
- });
31
-
32
- test("respects offset and length", () => {
33
- const buf = new Uint8Array([0, 1, 2, 3, 4, 5]);
34
- const c1 = CRC.update(0xffffffff, buf, 1, 3);
35
-
36
- const slice = buf.slice(1, 4);
37
- const c2 = CRC.update(0xffffffff, slice, 0, slice.length);
38
-
39
- expect(c1 >>> 0).toBe(c2 >>> 0);
40
- });
41
- });
42
-
43
- describe("crc", () => {
44
- test("matches known CRC-32 values for test vectors", () => {
45
- const enc = new TextEncoder();
46
-
47
- // Empty buffer (standard CRC-32)
48
- expect(CRC.crc(new Uint8Array([]), 0, 0) >>> 0).toBe(0x00000000);
49
-
50
- // "123456789" -> 0xCBF43926
51
- const v1 = enc.encode("123456789");
52
- expect(CRC.crc(v1, 0, v1.length) >>> 0).toBe(0xcbf43926);
53
-
54
- // Just verify that we return a valid 32-bit value for another string
55
- const v2 = enc.encode("Hello, world!");
56
- const crc2 = CRC.crc(v2, 0, v2.length) >>> 0;
57
- expect(crc2).toBeGreaterThanOrEqual(0);
58
- expect(crc2).toBeLessThanOrEqual(0xffffffff);
59
- });
60
-
61
- test("computes CRC on a subrange using offset and length", () => {
62
- const enc = new TextEncoder();
63
- const full = enc.encode("ABCDEF");
64
- const subCrc = CRC.crc(full, 1, 3); // "BCD"
65
-
66
- const sub = enc.encode("BCD");
67
- const expected = CRC.crc(sub, 0, sub.length);
68
-
69
- expect(subCrc >>> 0).toBe(expected >>> 0);
70
- });
71
- });
@@ -1,9 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import UPNG from "../src/index.js";
3
-
4
- describe("UPNG public API", () => {
5
- test("exposes encode/decode helpers", () => {
6
- expect(typeof UPNG.encode).toBe("function");
7
- expect(typeof UPNG.decode).toBe("function");
8
- });
9
- });
@@ -1,227 +0,0 @@
1
- import { deflate } from "@chr33s/pdf-common";
2
- import { describe, expect, test } from "vitest";
3
- import { Inflator } from "../src/inflator.js";
4
-
5
- describe("Inflator tables initialization", () => {
6
- const D = Inflator.D;
7
-
8
- test("creates all expected table types with correct lengths", () => {
9
- expect(D.m).toBeInstanceOf(Uint16Array);
10
- expect(D.v).toBeInstanceOf(Uint16Array);
11
- expect(D.B).toBeInstanceOf(Uint16Array);
12
- expect(D.h).toBeInstanceOf(Uint32Array);
13
- expect(D.g).toBeInstanceOf(Uint16Array);
14
- expect(D.A).toBeInstanceOf(Uint16Array);
15
- expect(D.k).toBeInstanceOf(Uint16Array);
16
- expect(D.n).toBeInstanceOf(Uint16Array);
17
- expect(D.C).toBeInstanceOf(Uint16Array);
18
- expect(D.i).toBeInstanceOf(Uint16Array);
19
- expect(D.r).toBeInstanceOf(Uint32Array);
20
- expect(D.f).toBeInstanceOf(Uint32Array);
21
- expect(D.l).toBeInstanceOf(Uint32Array);
22
- expect(D.u).toBeInstanceOf(Uint32Array);
23
- expect(D.q).toBeInstanceOf(Uint16Array);
24
- expect(D.j).toBeInstanceOf(Uint16Array);
25
-
26
- expect(D.m.length).toBe(16);
27
- expect(D.v.length).toBe(16);
28
- expect(D.B.length).toBe(32);
29
- expect(D.h.length).toBe(32);
30
- expect(D.g.length).toBe(512);
31
- expect(D.A.length).toBe(32);
32
- expect(D.k.length).toBe(32768);
33
- expect(D.n.length).toBe(32768);
34
- expect(D.C.length).toBe(512);
35
- expect(D.i.length).toBe(1 << 15);
36
- expect(D.r.length).toBe(286);
37
- expect(D.f.length).toBe(30);
38
- expect(D.l.length).toBe(19);
39
- expect(D.u.length).toBe(15000);
40
- expect(D.q.length).toBe(1 << 16);
41
- expect(D.j.length).toBe(1 << 15);
42
- });
43
-
44
- test("precomputes correct fixed Huffman length and distance tables for some entries", () => {
45
- // For fixed Huffman, the first 144 literal/length symbols use 8-bit codes.
46
- // `tables.s` is the canonical (code,length) pair array backing `g`.
47
- // Check some sample lengths.
48
- const s = D.s;
49
- // s is [code0, len0, code1, len1, ...]
50
- const len0 = s[1];
51
- const len143 = s[(143 << 1) + 1];
52
- const len144 = s[(144 << 1) + 1];
53
-
54
- expect(len0).toBe(8);
55
- expect(len143).toBe(8);
56
- expect(len144).toBe(9); // start of the 9-bit block
57
-
58
- // Distance codes for fixed Huffman use 5 bits each.
59
- const t = D.t;
60
- const distLen0 = t[1];
61
- const distLen29 = t[(29 << 1) + 1];
62
- expect(distLen0).toBe(5);
63
- expect(distLen29).toBe(5);
64
- });
65
-
66
- test("bit-reversal table i is self-consistent for a few sample entries", () => {
67
- const i = D.i;
68
- // For some simple patterns, reversing twice should give the original index
69
- const samples = [0, 1, 0b101010101010101, 0b111000000000000, 0x7fff];
70
- for (const v of samples) {
71
- const rev = i[v];
72
- const rev2 = i[rev];
73
- expect(rev2).toBe(v);
74
- }
75
- });
76
- });
77
-
78
- describe("Inflator helpers", () => {
79
- test("H expands buffer capacity when needed and preserves content", () => {
80
- const original = new Uint8Array([1, 2, 3, 4]);
81
- const same = Inflator.H(original, 4);
82
- expect(same).toBe(original);
83
-
84
- const expanded = Inflator.H(original, 10);
85
- expect(expanded).not.toBe(original);
86
- expect(expanded.length).toBeGreaterThanOrEqual(10);
87
- expect(Array.from(expanded.slice(0, 4))).toEqual([1, 2, 3, 4]);
88
- });
89
-
90
- test("C_inner assigns canonical codes consistently with straightforward implementation", () => {
91
- // Build a small length array: 4 symbols with lengths [2,3,3,1]
92
- const lengths = [2, 3, 3, 1];
93
- // o: [code0,len0,code1,len1,...] initially codes are 0
94
- const o: number[] = [];
95
- for (const len of lengths) {
96
- o.push(0, len);
97
- }
98
-
99
- // Run C_inner with max bits = 3
100
- Inflator.C_inner(o, 3);
101
-
102
- // Our own simple canonical-code computation for comparison
103
- const maxBits = 3;
104
- const blCount: number[] = Array.from({ length: maxBits + 1 }).fill(0) as number[];
105
- lengths.forEach((l) => {
106
- if (l > 0) blCount[l]++;
107
- });
108
-
109
- const nextCode: number[] = Array.from({ length: maxBits + 1 }).fill(0) as number[];
110
- let code = 0;
111
- for (let bits = 1; bits <= maxBits; bits++) {
112
- code = (code + blCount[bits - 1]) << 1;
113
- nextCode[bits] = code;
114
- }
115
-
116
- const expectedCodes: number[] = [];
117
- for (const len of lengths) {
118
- if (len === 0) {
119
- expectedCodes.push(0);
120
- } else {
121
- const c = nextCode[len];
122
- expectedCodes.push(c);
123
- nextCode[len]++;
124
- }
125
- }
126
-
127
- const actualCodes = [];
128
- for (let idx = 0; idx < o.length; idx += 2) {
129
- actualCodes.push(o[idx]);
130
- }
131
-
132
- expect(actualCodes).toEqual(expectedCodes);
133
- });
134
-
135
- test("d maps lengths into (code,length) pairs and returns max length", () => {
136
- const lengths = [3, 0, 5, 1]; // I = 4 symbols
137
- const target: number[] = Array.from({ length: 8 }).fill(0) as number[]; // length = 8 => 4 pairs
138
- const maxBits = Inflator.d(lengths, 0, lengths.length, target);
139
-
140
- // target: [code0,len0, code1,len1, ...] but C_inner hasn't run yet,
141
- // so codes should all be 0 and lengths should match.
142
- expect(target).toEqual([0, 3, 0, 0, 0, 5, 0, 1]);
143
- expect(maxBits).toBe(5);
144
- });
145
- });
146
-
147
- describe("Inflator.inflateRaw", () => {
148
- test("returns empty output for special case header 0x03 0x00", () => {
149
- const input = new Uint8Array([0x03, 0x00]); // triggers early-return branch
150
- const result = Inflator.inflateRaw(input);
151
- expect(result).toBeInstanceOf(Uint8Array);
152
- expect(result.length).toBe(0);
153
- });
154
-
155
- test("inflates a stored (uncompressed) block correctly", () => {
156
- // Deflate "ABC" as a single stored block:
157
- // BFINAL=1, BTYPE=00
158
- // LEN=3, NLEN=~3 (0xFFFC)
159
- // raw data: 0x41,0x42,0x43
160
- //
161
- // Bits: 1 (final) + 00 (stored) => 001 => 0x01 at low bits
162
- // We'll assemble minimal correct stored block bytes:
163
- const stored = new Uint8Array([
164
- 0x01, // BFINAL=1,BTYPE=00,and padding
165
- 0x03,
166
- 0x00, // LEN = 3
167
- 0xfc,
168
- 0xff, // NLEN = ~3
169
- 0x41,
170
- 0x42,
171
- 0x43, // "ABC"
172
- ]);
173
-
174
- const out = Inflator.inflateRaw(stored);
175
- expect(new TextDecoder().decode(out)).toBe("ABC");
176
- });
177
-
178
- test("inflates a fixed Huffman block identically to deflateRaw", async () => {
179
- const text = "Hello, world! Hello, world!";
180
- const encoder = new TextEncoder();
181
- const data = encoder.encode(text);
182
-
183
- const compressed = await deflate(data, "deflate-raw");
184
- const ours = Inflator.inflateRaw(compressed);
185
-
186
- expect(new TextDecoder().decode(ours)).toBe(text);
187
- });
188
-
189
- test("inflates a dynamic Huffman block identically to deflateRaw", async () => {
190
- const text = "Dynamic Huffman blocks are used for better compression on varied data.";
191
- const encoder = new TextEncoder();
192
- const data = encoder.encode(text);
193
-
194
- // deflateRaw uses dynamic Huffman by default
195
- const compressed = await deflate(data, "deflate-raw"); // contains dynamic blocks
196
- const ours = Inflator.inflateRaw(compressed);
197
-
198
- expect(new TextDecoder().decode(ours)).toBe(text);
199
- });
200
-
201
- test("supports inflating into a preallocated output buffer", async () => {
202
- const text = "Preallocated output buffer test.";
203
- const encoder = new TextEncoder();
204
- const data = encoder.encode(text);
205
-
206
- const compressed = await deflate(data, "deflate-raw");
207
-
208
- // Preallocate a buffer with exactly the required size
209
- const out = new Uint8Array(text.length);
210
- const result = Inflator.inflateRaw(compressed, out);
211
-
212
- // Using supplied buffer should either reuse or slice it exactly to size
213
- expect(result.length).toBe(text.length);
214
- expect(new TextDecoder().decode(result)).toBe(text);
215
- });
216
-
217
- test("grows output buffer as needed when not provided", async () => {
218
- const text = "X".repeat(100000); // big to force resizing via H()
219
- const encoder = new TextEncoder();
220
- const data = encoder.encode(text);
221
- const compressed = await deflate(data, "deflate-raw");
222
-
223
- const result = Inflator.inflateRaw(compressed);
224
- expect(result.length).toBe(text.length);
225
- expect(new TextDecoder().decode(result)).toBe(text);
226
- });
227
- });
@@ -1,323 +0,0 @@
1
- import { beforeEach, describe, expect, test } from "vitest";
2
- import { Quantizer } from "../src/quantizer.js";
3
-
4
- describe("Quantizer.M4", () => {
5
- test("multVec multiplies 4x4 matrix by 4-vector", () => {
6
- const m = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
7
- const v = [1, 0, -1, 2];
8
- const r = Quantizer.M4.multVec(m, v);
9
-
10
- // m = [1,2,3,4; 5,6,7,8; 9,10,11,12; 13,14,15,16] row-major
11
- // v = [1, 0, -1, 2]
12
- // Result: row_i dot v = m[i*4]*1 + m[i*4+1]*0 + m[i*4+2]*(-1) + m[i*4+3]*2
13
- expect(r).toEqual([6, 14, 22, 30]);
14
- });
15
-
16
- test("dot computes 4D dot product", () => {
17
- const x = [1, 2, 3, 4];
18
- const y = [4, 3, 2, 1];
19
- const d = Quantizer.M4.dot(x, y);
20
- expect(d).toBe(1 * 4 + 2 * 3 + 3 * 2 + 4 * 1);
21
- });
22
-
23
- test("sml scales 4-vector", () => {
24
- const v = [1, -2, 3, -4];
25
- const s = Quantizer.M4.sml(2, v);
26
- expect(s).toEqual([2, -4, 6, -8]);
27
- });
28
- });
29
-
30
- describe("Quantizer.basic math helpers", () => {
31
- test("dist computes squared 4D distance", () => {
32
- const q = [0, 0, 0, 0];
33
- const d = Quantizer.dist(q, 1, 2, 3, 4);
34
- expect(d).toBe(1 * 1 + 2 * 2 + 3 * 3 + 4 * 4);
35
- });
36
-
37
- test("vecDot matches manual computation", () => {
38
- const px = new Uint8Array([10, 20, 30, 40]);
39
- const e = [0.1, 0.2, 0.3, 0.4];
40
- const v = Quantizer.vecDot(px, 0, e);
41
- const manual = 10 * 0.1 + 20 * 0.2 + 30 * 0.3 + 40 * 0.4;
42
- expect(v).toBeCloseTo(manual);
43
- });
44
-
45
- test("planeDst returns signed distance from plane", () => {
46
- const est = {
47
- e: [1, 0, 0, 0],
48
- eMq: 0.5,
49
- };
50
- const d1 = Quantizer.planeDst(est, 1, 0, 0, 0);
51
- const d2 = Quantizer.planeDst(est, 0, 0, 0, 0);
52
- expect(d1).toBeCloseTo(0.5);
53
- expect(d2).toBeCloseTo(-0.5);
54
- });
55
- });
56
-
57
- describe("Quantizer.stats and estats", () => {
58
- test("stats computes count, means and covariance-like accumulators", () => {
59
- // 2 pixels: (255,0,0,255) and (0,255,0,255)
60
- const img = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]);
61
- const s = Quantizer.stats(img, 0, img.length);
62
- expect(s.N).toBe(2);
63
- // Means are in [0,1] range
64
- const iN = 1 / s.N;
65
- expect(s.m[0] * iN).toBeCloseTo(0.5, 5);
66
- expect(s.m[1] * iN).toBeCloseTo(0.5, 5);
67
- expect(s.m[2] * iN).toBeCloseTo(0, 5);
68
- expect(s.m[3] * iN).toBeCloseTo(1, 5);
69
- });
70
-
71
- test("estats produces expected structure and stable eigenvector", () => {
72
- const img = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]);
73
- const stats = Quantizer.stats(img, 0, img.length);
74
- const est = Quantizer.estats(stats);
75
-
76
- expect(est).toHaveProperty("Cov");
77
- expect(est).toHaveProperty("q");
78
- expect(est).toHaveProperty("e");
79
- expect(est).toHaveProperty("L");
80
- expect(est).toHaveProperty("eMq255");
81
- expect(est).toHaveProperty("eMq");
82
- expect(est).toHaveProperty("rgba");
83
-
84
- // q is the mean color in [0,1]
85
- expect(est.q.length).toBe(4);
86
- expect(est.L).toBeGreaterThanOrEqual(0);
87
- // rgba is packed 8-bit channels
88
- expect(est.rgba >>> 0).toBeGreaterThanOrEqual(0);
89
- });
90
- });
91
-
92
- describe("Quantizer.splitPixels", () => {
93
- test("splits pixels into two groups based on plane", () => {
94
- // Four pixels: two dark, two bright
95
- const img = new Uint8Array([
96
- 0,
97
- 0,
98
- 0,
99
- 255, // 0
100
- 10,
101
- 10,
102
- 10,
103
- 255, // 4
104
- 200,
105
- 200,
106
- 200,
107
- 255, // 8
108
- 250,
109
- 250,
110
- 250,
111
- 255, // 12
112
- ]);
113
- const img32 = new Uint32Array(img.buffer);
114
-
115
- // Plane roughly on brightness
116
- const e = [1, 1, 1, 0];
117
- const mq = 200 * 3; // threshold ~sum of channels
118
- const splitIdx = Quantizer.splitPixels(img, img32, 0, img.length, e, mq);
119
-
120
- // All pixels before splitIdx are <= mq, all after are > mq
121
- for (let i = 0; i < splitIdx; i += 4) {
122
- const v = Quantizer.vecDot(img, i, e);
123
- expect(v).toBeLessThanOrEqual(mq);
124
- }
125
- for (let i = splitIdx; i < img.length; i += 4) {
126
- const v = Quantizer.vecDot(img, i, e);
127
- expect(v).toBeGreaterThan(mq);
128
- }
129
- });
130
- });
131
-
132
- describe("Quantizer.getKDtree and getNearest", () => {
133
- let img: Uint8Array;
134
-
135
- beforeEach(() => {
136
- // small 4-color image
137
- img = new Uint8Array([
138
- 255,
139
- 0,
140
- 0,
141
- 255, // red
142
- 0,
143
- 255,
144
- 0,
145
- 255, // green
146
- 0,
147
- 0,
148
- 255,
149
- 255, // blue
150
- 255,
151
- 255,
152
- 0,
153
- 255, // yellow
154
- ]);
155
- });
156
-
157
- test("builds KD tree with requested max leaf count", () => {
158
- const [root, leafs] = Quantizer.getKDtree(img, 4);
159
- expect(root).toHaveProperty("left");
160
- expect(root).toHaveProperty("right");
161
- expect(Array.isArray(leafs)).toBe(true);
162
- expect(leafs.length).toBeLessThanOrEqual(4);
163
- // Leaves are sorted by population descending
164
- for (let i = 1; i < leafs.length; i++) {
165
- expect(leafs[i - 1].bst.N).toBeGreaterThanOrEqual(leafs[i].bst.N);
166
- }
167
- // Each leaf has an index
168
- for (let i = 0; i < leafs.length; i++) {
169
- expect(leafs[i].ind).toBe(i);
170
- }
171
- });
172
-
173
- test("getNearest returns a leaf whose color is close to the query", () => {
174
- const [root, leafs] = Quantizer.getKDtree(img, 4);
175
- const qColor = [1, 0.1, 0.1, 1]; // close to red
176
- const nearest = Quantizer.getNearest(root, qColor[0], qColor[1], qColor[2], qColor[3]);
177
-
178
- expect(leafs.includes(nearest)).toBe(true);
179
-
180
- // Check that returned leaf has reasonable distance to its mean q
181
- const dist = Quantizer.dist(nearest.est.q, qColor[0], qColor[1], qColor[2], qColor[3]);
182
- expect(dist).toBeLessThan(0.5); // arbitrary sanity bound
183
- });
184
- });
185
-
186
- describe("Quantizer palette update and nearest search", () => {
187
- test("updatePalette recomputes palette entries as means of assigned pixels", () => {
188
- // Image: 4 pixels. First two red-ish, last two green-ish.
189
- const sb = new Uint8Array([250, 0, 0, 255, 255, 10, 0, 255, 0, 240, 0, 255, 5, 255, 10, 255]);
190
-
191
- // Two palette entries: start as midpoints
192
- const plte = new Uint8Array([
193
- 128,
194
- 0,
195
- 0,
196
- 255, // approximate red
197
- 0,
198
- 128,
199
- 0,
200
- 255, // approximate green
201
- ]);
202
-
203
- // First two pixels -> index 0, last two -> index 1
204
- const inds = new Uint8Array([0, 0, 1, 1]);
205
-
206
- Quantizer.updatePalette(sb, inds, plte);
207
-
208
- // Palette[0] should now be near average of first two reds
209
- const avgR0 = Math.round((250 + 255) / 2);
210
- const avgG0 = Math.round((0 + 10) / 2);
211
- const avgB0 = 0;
212
-
213
- expect(plte[0]).toBe(avgR0);
214
- expect(plte[1]).toBe(avgG0);
215
- expect(plte[2]).toBe(avgB0);
216
-
217
- // Palette[1] should be near average of two greens
218
- const avgR1 = Math.round((0 + 5) / 2);
219
- const avgG1 = Math.round((240 + 255) / 2);
220
- const avgB1 = Math.round((0 + 10) / 2);
221
-
222
- expect(plte[4]).toBe(avgR1);
223
- expect(plte[5]).toBe(avgG1);
224
- expect(plte[6]).toBe(avgB1);
225
- });
226
-
227
- test("findNearest assigns each pixel to closest palette color and returns average error", () => {
228
- const sb = new Uint8Array([
229
- 255,
230
- 0,
231
- 0,
232
- 255, // red-like
233
- 0,
234
- 255,
235
- 0,
236
- 255, // green-like
237
- 0,
238
- 0,
239
- 255,
240
- 255, // blue-like
241
- ]);
242
-
243
- // Palette: exact red, green, blue
244
- const plte = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]);
245
-
246
- const inds = new Uint8Array(sb.length >> 2);
247
- // Start with some arbitrary indices
248
- inds.fill(0);
249
-
250
- const err = Quantizer.findNearest(sb, inds, plte);
251
-
252
- // Perfect match => each pixel should go to its exact palette index, zero error
253
- expect(Array.from(inds)).toEqual([0, 1, 2]);
254
- expect(err).toBe(0);
255
- });
256
-
257
- test("kmeans runs a single iteration and returns an error value", () => {
258
- const sb = new Uint8Array([255, 0, 0, 255, 250, 0, 10, 255, 0, 255, 0, 255, 0, 245, 10, 255]);
259
- const plte = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]);
260
- const inds = new Uint8Array([0, 0, 1, 1]);
261
-
262
- const err = Quantizer.kmeans(sb, inds, plte);
263
- expect(err).toBeGreaterThanOrEqual(0);
264
- // kmeans mutates palette toward means
265
- expect(plte[0]).toBeLessThanOrEqual(255);
266
- expect(plte[4]).toBeLessThanOrEqual(255);
267
- });
268
- });
269
-
270
- describe("Quantizer.remap", () => {
271
- test("remap fills destination buffer from palette using indices", () => {
272
- const pl32 = new Uint32Array([
273
- 0xff0000ff, // red
274
- 0x00ff00ff, // green
275
- ]);
276
- const tb32 = new Uint32Array(3);
277
- const inds = new Uint8Array([0, 1, 1]);
278
-
279
- Quantizer.remap(inds, tb32, pl32);
280
-
281
- expect(Array.from(tb32)).toEqual([0xff0000ff, 0x00ff00ff, 0x00ff00ff]);
282
- });
283
- });
284
-
285
- describe("Quantizer.quantize end-to-end", () => {
286
- test("quantizes a tiny image to 2 colors and returns expected shape", () => {
287
- // Simple 4-pixel RGBA image: two reds, two greens
288
- const img = new Uint8Array([255, 0, 0, 255, 250, 10, 0, 255, 0, 255, 0, 255, 5, 240, 10, 255]);
289
- const abuf = img.buffer;
290
-
291
- const ps = 2; // target palette size
292
- const result = Quantizer.quantize(abuf, ps, false);
293
-
294
- expect(result).toHaveProperty("abuf");
295
- expect(result).toHaveProperty("inds");
296
- expect(result).toHaveProperty("plte");
297
-
298
- const out = new Uint8Array(result.abuf);
299
- expect(out.length).toBe(img.length);
300
- expect(result.inds.length).toBe(img.length / 4);
301
- expect(result.plte.length).toBeLessThanOrEqual(ps);
302
-
303
- // Palette entries have est.rgba defined
304
- for (const leaf of result.plte) {
305
- expect(typeof leaf.est.rgba).toBe("number");
306
- }
307
-
308
- // All indices should be < K
309
- const K = result.plte.length;
310
- for (const idx of result.inds) {
311
- expect(idx).toBeLessThan(K);
312
- }
313
- });
314
-
315
- test("can run with kmeans refinement enabled", () => {
316
- const img = new Uint8Array([255, 0, 0, 255, 250, 10, 0, 255, 0, 255, 0, 255, 5, 240, 10, 255]);
317
- const result = Quantizer.quantize(img.buffer, 2, true);
318
-
319
- // Same basic structural expectations
320
- expect(result.inds.length).toBe(img.length / 4);
321
- expect(result.plte.length).toBeLessThanOrEqual(2);
322
- });
323
- });
package/test/upng.test.ts DELETED
@@ -1,242 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { UPNG, type Image, type ImageTabs } from "../src/upng.js";
3
-
4
- describe("UPNG basic encode/decode", () => {
5
- test("round-trips a small opaque RGBA image", async () => {
6
- const w = 2;
7
- const h = 2;
8
- const src = new Uint8Array([
9
- 255,
10
- 0,
11
- 0,
12
- 255, // red
13
- 0,
14
- 255,
15
- 0,
16
- 255, // green
17
- 0,
18
- 0,
19
- 255,
20
- 255, // blue
21
- 255,
22
- 255,
23
- 0,
24
- 255, // yellow
25
- ]);
26
- const encoded = await UPNG.encode([src.buffer], w, h, 0);
27
- const decoded = UPNG.decode(encoded);
28
-
29
- expect(decoded.width).toBe(w);
30
- expect(decoded.height).toBe(h);
31
- expect(decoded.frames.length).toBe(1);
32
-
33
- const [rgbaBuf] = UPNG.toRGBA8(decoded);
34
- const out = new Uint8Array(rgbaBuf);
35
-
36
- expect(Array.from(out)).toEqual(Array.from(src));
37
- });
38
-
39
- test("round-trips an image with transparency (forces alpha ctype 6 or pal+TRNS)", async () => {
40
- const w = 2;
41
- const h = 2;
42
- const buf = new Uint8Array([255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0]).buffer;
43
-
44
- const encoded = await UPNG.encode([buf], w, h, 0);
45
- const decoded = UPNG.decode(encoded);
46
- const [rgbaBuf] = UPNG.toRGBA8(decoded);
47
- const out = new Uint8Array(rgbaBuf);
48
-
49
- expect(out.length).toBe(w * h * 4);
50
- // Colors should match original exactly
51
- expect(Array.from(out)).toEqual(Array.from(new Uint8Array(buf)));
52
- });
53
- });
54
-
55
- describe("UPNG.toRGBA8", () => {
56
- test("returns a single frame buffer for non-animated PNG", async () => {
57
- const w = 1;
58
- const h = 1;
59
- const buf = new Uint8Array([10, 20, 30, 40]).buffer;
60
-
61
- const encoded = await UPNG.encode([buf], w, h, 0);
62
- const decoded = UPNG.decode(encoded);
63
-
64
- expect(decoded.tabs.acTL).toBeUndefined();
65
-
66
- const frames = UPNG.toRGBA8(decoded);
67
- expect(frames.length).toBe(1);
68
- const rgba = new Uint8Array(frames[0]);
69
- expect(Array.from(rgba)).toEqual([10, 20, 30, 40]);
70
- });
71
-
72
- test("returns buffers for each animation frame and respects blend/dispose", async () => {
73
- const w = 2;
74
- const h = 1;
75
-
76
- const frame0 = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]).buffer;
77
- const frame1 = new Uint8Array([0, 0, 255, 255, 255, 255, 255, 255]).buffer;
78
-
79
- const dels = [100, 200];
80
- const encoded = await UPNG.encode([frame0, frame1], w, h, 0, dels, { loop: 0 });
81
- const decoded = UPNG.decode(encoded);
82
-
83
- expect(decoded.tabs.acTL).toBeDefined();
84
- expect(decoded.frames.length).toBe(2);
85
-
86
- const framesRGBA = UPNG.toRGBA8(decoded);
87
- expect(framesRGBA.length).toBe(2);
88
-
89
- const f0 = new Uint8Array(framesRGBA[0]);
90
- const f1 = new Uint8Array(framesRGBA[1]);
91
-
92
- expect(Array.from(f0)).toEqual(Array.from(new Uint8Array(frame0)));
93
- expect(Array.from(f1)).toEqual(Array.from(new Uint8Array(frame1)));
94
- });
95
- });
96
-
97
- describe("UPNG.decode ancillary chunks", () => {
98
- test("throws on non-PNG magic", () => {
99
- const bogus = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]).buffer;
100
- expect(() => UPNG.decode(bogus)).toThrow(/not a PNG/i);
101
- });
102
-
103
- test("parses tEXt & zTXt chunks into tabs", async () => {
104
- const w = 1;
105
- const h = 1;
106
- const buf = new Uint8Array([0, 0, 0, 255]).buffer;
107
-
108
- const tabs: ImageTabs = {
109
- tEXt: { Author: "Test" },
110
- iTXt: { Comment: "Hello world" },
111
- };
112
-
113
- const encoded = await UPNG.encode([buf], w, h, 0, undefined, tabs);
114
- const decoded = UPNG.decode(encoded);
115
-
116
- if (decoded.tabs.tEXt) {
117
- expect(decoded.tabs.tEXt["Author"]).toBe("Test");
118
- }
119
- if (decoded.tabs.iTXt) {
120
- expect(decoded.tabs.iTXt["Comment"]).toBe("Hello world");
121
- }
122
- });
123
- });
124
-
125
- describe("UPNG #getBPP and #filterZero via encode/decode", () => {
126
- test("handles grayscale (ctype 0) depth 8 correctly", async () => {
127
- const w = 3;
128
- const h = 1;
129
- const gray = new Uint8Array([50, 50, 50, 255, 100, 100, 100, 255, 200, 200, 200, 255]).buffer;
130
-
131
- // Use encodeLL to force grayscale: cc=1, ac=0
132
- const encoded = await UPNG.encodeLL([gray], w, h, 1, 0, 8);
133
- const decoded = UPNG.decode(encoded);
134
- expect(decoded.ctype).toBe(0);
135
-
136
- const [rgbaBuf] = UPNG.toRGBA8(decoded);
137
- const out = new Uint8Array(rgbaBuf);
138
-
139
- expect(out[0]).toBe(50);
140
- expect(out[1]).toBe(50);
141
- expect(out[2]).toBe(50);
142
- expect(out[3]).toBe(255);
143
- expect(out[4]).toBe(100);
144
- });
145
-
146
- test("handles palette-based images (ctype 3) when color count is small", async () => {
147
- const w = 2;
148
- const h = 2;
149
- const buf = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255, 255, 0, 0, 255, 0, 255, 0, 255])
150
- .buffer;
151
-
152
- // Force small palette size to encourage PLTE path
153
- const encoded = await UPNG.encode([buf], w, h, 4);
154
- const decoded = UPNG.decode(encoded);
155
-
156
- expect(decoded.ctype).toBe(3);
157
-
158
- const [rgbaBuf] = UPNG.toRGBA8(decoded);
159
- const out = new Uint8Array(rgbaBuf);
160
-
161
- // Should still decode back to original colors
162
- expect(Array.from(out)).toEqual(Array.from(new Uint8Array(buf)));
163
- });
164
- });
165
-
166
- describe("UPNG filter/deflate pipeline", () => {
167
- test("produces valid data when using filter strategy 0 (none)", async () => {
168
- const w = 4;
169
- const h = 2;
170
- const buf = new Uint8Array(w * h * 4);
171
- for (let i = 0; i < buf.length; i += 4) {
172
- buf[i] = i & 255;
173
- buf[i + 1] = (i * 2) & 255;
174
- buf[i + 2] = (i * 3) & 255;
175
- buf[i + 3] = 255;
176
- }
177
-
178
- // encodeLL with levelZero=true via encodeLL -> #compressPNG(filter=0, levelZero=true)
179
- const encoded = await UPNG.encodeLL([buf.buffer], w, h, 3, 1, 8);
180
- const decoded = UPNG.decode(encoded);
181
- const [rgbaBuf] = UPNG.toRGBA8(decoded);
182
- const out = new Uint8Array(rgbaBuf);
183
-
184
- expect(out.length).toBe(buf.length);
185
- });
186
- });
187
-
188
- describe("UPNG internal dithering path (by effect)", () => {
189
- test("dithers when quantizing with palette (non-empty result)", async () => {
190
- const w = 4;
191
- const h = 4;
192
- const buf = new Uint8Array(w * h * 4);
193
- for (let i = 0; i < buf.length; i += 4) {
194
- buf[i] = (i / 4) % 256;
195
- buf[i + 1] = 255 - ((i / 4) % 256);
196
- buf[i + 2] = ((i / 4) * 3) & 255;
197
- buf[i + 3] = 255;
198
- }
199
-
200
- // cnum=16 palette size, dithering enabled via encode (6th param of prms)
201
- const encoded = await (UPNG as any).encode([buf.buffer], w, h, 16, undefined, undefined, false);
202
- const decoded = UPNG.decode(encoded);
203
- const [rgbaBuf] = UPNG.toRGBA8(decoded);
204
- const out = new Uint8Array(rgbaBuf);
205
-
206
- expect(out.length).toBe(buf.length);
207
- });
208
- });
209
-
210
- describe("UPNG animated encodeLL", () => {
211
- test("encodes multiple frames as an animated PNG with acTL", async () => {
212
- const w = 2;
213
- const h = 2;
214
-
215
- const frame0 = new Uint8Array([
216
- 0, 0, 0, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
217
- ]).buffer;
218
-
219
- const frame1 = new Uint8Array([
220
- 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
221
- ]).buffer;
222
-
223
- const delays = [100, 200];
224
- const tabs = { loop: 0 };
225
-
226
- const encoded = await UPNG.encodeLL([frame0, frame1], w, h, 4, 1, 8, delays, tabs);
227
- const decoded = UPNG.decode(encoded) as Image;
228
-
229
- // We should have animation control chunk parsed
230
- expect(decoded.tabs.acTL).toBeDefined();
231
- expect(decoded.tabs.acTL!.num_frames).toBe(2);
232
-
233
- // We should have two frames decoded
234
- expect(decoded.frames.length).toBe(2);
235
-
236
- // Delays should be numbers (not NaN/undefined)
237
- expect(typeof decoded.frames[0].delay).toBe("number");
238
- expect(typeof decoded.frames[1].delay).toBe("number");
239
- expect(decoded.frames[0].delay).toBeGreaterThanOrEqual(0);
240
- expect(decoded.frames[1].delay).toBeGreaterThanOrEqual(0);
241
- });
242
- });
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "src",
5
- "outDir": "dist"
6
- },
7
- "include": ["src/**/*"],
8
- "exclude": ["dist", "node_modules", "test"]
9
- }
@@ -1,14 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "noEmit": true,
5
- "rootDir": "."
6
- },
7
- "include": [
8
- "**/*.ts",
9
- ],
10
- "exclude": [
11
- "dist",
12
- "node_modules"
13
- ]
14
- }
package/vitest.config.ts DELETED
@@ -1,8 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- environment: "node",
6
- include: ["test/**/*.test.ts"],
7
- },
8
- });