@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 +6 -2
- package/test/bin.test.ts +0 -173
- package/test/crc.test.ts +0 -71
- package/test/index.test.ts +0 -9
- package/test/inflator.test.ts +0 -227
- package/test/quantizer.test.ts +0 -323
- package/test/upng.test.ts +0 -242
- package/tsconfig.json +0 -9
- package/tsconfig.typecheck.json +0 -14
- package/vitest.config.ts +0 -8
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chr33s/pdf-upng",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "5.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": "
|
|
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
|
-
});
|
package/test/index.test.ts
DELETED
|
@@ -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
|
-
});
|
package/test/inflator.test.ts
DELETED
|
@@ -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
|
-
});
|
package/test/quantizer.test.ts
DELETED
|
@@ -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
package/tsconfig.typecheck.json
DELETED