@wenathlan/cadria 2.0.84 → 2.0.88
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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// # matiz.test — the cadria pixel engine: the channel adjustments, the
|
|
2
|
+
// grayscale and sepia tints, the 3x3 convolution, the box blur, the bilinear
|
|
3
|
+
// resize and the composed pipeline, runnable with the node built-in runner
|
|
4
|
+
// (no canvas, no dependencies, no install):
|
|
5
|
+
// node --test tests/matiz.test.ts
|
|
6
|
+
// The fixture is a synthetic rgba buffer shaped like ImageData, and every
|
|
7
|
+
// test also watches the one invariant the engine promises: the input never
|
|
8
|
+
// mutates.
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { describe, it } from "node:test";
|
|
11
|
+
import {
|
|
12
|
+
adjustimage,
|
|
13
|
+
boxblurimage,
|
|
14
|
+
convolveimage,
|
|
15
|
+
type MatizImage,
|
|
16
|
+
matizkernels,
|
|
17
|
+
resizeimage,
|
|
18
|
+
runmatiz,
|
|
19
|
+
tintimage,
|
|
20
|
+
} from "../matiz.ts";
|
|
21
|
+
|
|
22
|
+
/** builds a w×h rgba buffer where every pixel carries the same color. */
|
|
23
|
+
function solidImage(width: number, height: number, color: [number, number, number, number]): MatizImage {
|
|
24
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
25
|
+
for (let at = 0; at < data.length; at += 4) {
|
|
26
|
+
data[at] = color[0];
|
|
27
|
+
data[at + 1] = color[1];
|
|
28
|
+
data[at + 2] = color[2];
|
|
29
|
+
data[at + 3] = color[3];
|
|
30
|
+
}
|
|
31
|
+
return { width, height, data };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** builds a w×h grayscale horizontal ramp (left black, right white, alpha full). */
|
|
35
|
+
function rampImage(width: number, height: number): MatizImage {
|
|
36
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
37
|
+
for (let y = 0; y < height; y += 1) {
|
|
38
|
+
for (let x = 0; x < width; x += 1) {
|
|
39
|
+
const at = (y * width + x) * 4;
|
|
40
|
+
const value = Math.round((255 * x) / (width - 1));
|
|
41
|
+
data[at] = value;
|
|
42
|
+
data[at + 1] = value;
|
|
43
|
+
data[at + 2] = value;
|
|
44
|
+
data[at + 3] = 255;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { width, height, data };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** the buffer snapshot used to prove no stage mutates its input. */
|
|
51
|
+
function snapshot(image: MatizImage): Uint8ClampedArray {
|
|
52
|
+
return new Uint8ClampedArray(image.data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("matiz adjustments", () => {
|
|
56
|
+
it("keeps the image unchanged when every amount answers one", () => {
|
|
57
|
+
const image = solidImage(3, 2, [40, 80, 160, 255]);
|
|
58
|
+
const before = snapshot(image);
|
|
59
|
+
const out = adjustimage(image);
|
|
60
|
+
assert.deepEqual([out.data[0], out.data[1], out.data[2], out.data[3]], [40, 80, 160, 255]);
|
|
61
|
+
assert.deepEqual(image.data, before);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("drives brightness to zero and multiplies honestly", () => {
|
|
65
|
+
const image = solidImage(2, 2, [100, 50, 25, 255]);
|
|
66
|
+
const dark = adjustimage(image, { brightness: 0 });
|
|
67
|
+
assert.deepEqual([dark.data[0], dark.data[1], dark.data[2]], [0, 0, 0]);
|
|
68
|
+
const twice = adjustimage(image, { brightness: 2 });
|
|
69
|
+
assert.deepEqual([twice.data[0], twice.data[1], twice.data[2]], [200, 100, 50]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("swings contrast around the 128 midpoint", () => {
|
|
73
|
+
const image = solidImage(2, 2, [128, 178, 78, 255]);
|
|
74
|
+
const out = adjustimage(image, { contrast: 2 });
|
|
75
|
+
assert.deepEqual([out.data[0], out.data[1], out.data[2]], [128, 228, 28]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("answers the luma when saturation reaches zero", () => {
|
|
79
|
+
const image = solidImage(2, 2, [100, 150, 200, 255]);
|
|
80
|
+
const out = adjustimage(image, { saturation: 0 });
|
|
81
|
+
const gray = out.data[0];
|
|
82
|
+
assert.equal(out.data[1], gray);
|
|
83
|
+
assert.equal(out.data[2], gray);
|
|
84
|
+
assert.equal(gray, Math.round(0.2126 * 100 + 0.7152 * 150 + 0.0722 * 200));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("refuses negative or non-finite amounts", () => {
|
|
88
|
+
const image = solidImage(2, 2, [10, 20, 30, 255]);
|
|
89
|
+
assert.throws(() => adjustimage(image, { brightness: -1 }), /adjustments must be finite/);
|
|
90
|
+
assert.throws(() => adjustimage(image, { contrast: Number.NaN }), /adjustments must be finite/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("matiz tints", () => {
|
|
95
|
+
it("answers the rec 709 luma on every channel for grayscale", () => {
|
|
96
|
+
const image = solidImage(2, 2, [90, 120, 30, 255]);
|
|
97
|
+
const out = tintimage(image, "grayscale");
|
|
98
|
+
const gray = Math.round(0.2126 * 90 + 0.7152 * 120 + 0.0722 * 30);
|
|
99
|
+
assert.deepEqual([out.data[0], out.data[1], out.data[2]], [gray, gray, gray]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("warms a neutral gray toward sepia and clamps at the ceiling", () => {
|
|
103
|
+
const white = solidImage(2, 2, [255, 255, 255, 255]);
|
|
104
|
+
const out = tintimage(white, "sepia");
|
|
105
|
+
assert.deepEqual([out.data[0], out.data[1], out.data[2]], [255, 255, 239]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("answers a copy for the none tint", () => {
|
|
109
|
+
const image = solidImage(2, 2, [12, 34, 56, 255]);
|
|
110
|
+
const out = tintimage(image, "none");
|
|
111
|
+
assert.deepEqual(new Uint8ClampedArray(out.data), image.data);
|
|
112
|
+
assert.notEqual(out.data, image.data);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("matiz convolution", () => {
|
|
117
|
+
it("answers the input colors for the identity kernel", () => {
|
|
118
|
+
const image = solidImage(3, 3, [30, 60, 90, 255]);
|
|
119
|
+
const out = convolveimage(image, matizkernels.identity);
|
|
120
|
+
assert.deepEqual(new Uint8ClampedArray(out.data), image.data);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("keeps the alpha untouched and the dimensions stable", () => {
|
|
124
|
+
const image = rampImage(4, 4);
|
|
125
|
+
const before = snapshot(image);
|
|
126
|
+
const out = convolveimage(image, matizkernels.sharpen);
|
|
127
|
+
assert.equal(out.width, 4);
|
|
128
|
+
assert.equal(out.height, 4);
|
|
129
|
+
for (let at = 3; at < image.data.length; at += 4) assert.equal(out.data[at], image.data[at]);
|
|
130
|
+
assert.deepEqual(image.data, before);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("replicates the clamped edges instead of reading outside", () => {
|
|
134
|
+
const image = solidImage(2, 2, [100, 100, 100, 255]);
|
|
135
|
+
const out = convolveimage(image, matizkernels.identity);
|
|
136
|
+
assert.deepEqual(new Uint8ClampedArray(out.data), image.data);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("refuses a kernel that is not nine finite weights", () => {
|
|
140
|
+
const image = solidImage(2, 2, [1, 2, 3, 255]);
|
|
141
|
+
assert.throws(() => convolveimage(image, [1, 2, 3] as unknown as typeof matizkernels.identity), /nine finite weights/);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("matiz box blur", () => {
|
|
146
|
+
it("answers a solid image unchanged", () => {
|
|
147
|
+
const image = solidImage(4, 4, [80, 120, 200, 255]);
|
|
148
|
+
const out = boxblurimage(image, 1);
|
|
149
|
+
assert.deepEqual(new Uint8ClampedArray(out.data), image.data);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("pulls a lone bright pixel toward its dark neighborhood", () => {
|
|
153
|
+
const image = solidImage(5, 5, [0, 0, 0, 255]);
|
|
154
|
+
const center = (2 * 5 + 2) * 4;
|
|
155
|
+
image.data[center] = 255;
|
|
156
|
+
image.data[center + 1] = 255;
|
|
157
|
+
image.data[center + 2] = 255;
|
|
158
|
+
const out = boxblurimage(image, 1);
|
|
159
|
+
assert.equal(out.data[center], 28, "the white center falls toward its dark ring");
|
|
160
|
+
assert.equal(out.data[0], 0, "the far corner stays dark");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("answers a copy for radius zero and refuses bad radii", () => {
|
|
164
|
+
const image = solidImage(2, 2, [9, 8, 7, 255]);
|
|
165
|
+
assert.deepEqual(new Uint8ClampedArray(boxblurimage(image, 0).data), image.data);
|
|
166
|
+
assert.throws(() => boxblurimage(image, -1), /integer >= 0/);
|
|
167
|
+
assert.throws(() => boxblurimage(image, 1.5), /integer >= 0/);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("matiz resize", () => {
|
|
172
|
+
it("answers the same color field for a solid image at any size", () => {
|
|
173
|
+
const image = solidImage(3, 3, [10, 200, 30, 255]);
|
|
174
|
+
const out = resizeimage(image, { width: 7, height: 5 });
|
|
175
|
+
assert.equal(out.width, 7);
|
|
176
|
+
assert.equal(out.height, 5);
|
|
177
|
+
for (let at = 0; at < out.data.length; at += 4)
|
|
178
|
+
assert.deepEqual([out.data[at], out.data[at + 1], out.data[at + 2], out.data[at + 3]], [10, 200, 30, 255]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("keeps a downsampled ramp monotonic", () => {
|
|
182
|
+
const image = rampImage(8, 2);
|
|
183
|
+
const out = resizeimage(image, { width: 4, height: 1 });
|
|
184
|
+
assert.ok(out.data[0] < out.data[4] && out.data[4] < out.data[8] && out.data[8] < out.data[12]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("refuses zero or fractional dimensions", () => {
|
|
188
|
+
const image = solidImage(2, 2, [1, 1, 1, 255]);
|
|
189
|
+
assert.throws(() => resizeimage(image, { width: 0, height: 2 }), /positive integer/);
|
|
190
|
+
assert.throws(() => resizeimage(image, { width: 2.5, height: 2 }), /positive integer/);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("matiz pipeline", () => {
|
|
195
|
+
it("runs the documented order: adjust, tint, convolve, blur, resize", () => {
|
|
196
|
+
const image = rampImage(6, 4);
|
|
197
|
+
const before = snapshot(image);
|
|
198
|
+
const out = runmatiz(image, {
|
|
199
|
+
adjustments: { brightness: 1.2, contrast: 1.1, saturation: 0.8 },
|
|
200
|
+
tint: "grayscale",
|
|
201
|
+
kernel: matizkernels.identity,
|
|
202
|
+
blurRadius: 1,
|
|
203
|
+
resize: { width: 3, height: 2 },
|
|
204
|
+
});
|
|
205
|
+
assert.equal(out.width, 3);
|
|
206
|
+
assert.equal(out.height, 2);
|
|
207
|
+
assert.deepEqual(image.data, before, "the pipeline never mutates the input");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("answers a copy with no options", () => {
|
|
211
|
+
const image = solidImage(2, 2, [5, 6, 7, 255]);
|
|
212
|
+
const out = runmatiz(image);
|
|
213
|
+
assert.deepEqual(new Uint8ClampedArray(out.data), image.data);
|
|
214
|
+
assert.notEqual(out.data, image.data);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("refuses an image whose buffer does not answer for its size", () => {
|
|
218
|
+
assert.throws(() => runmatiz({ width: 2, height: 2, data: new Uint8ClampedArray(4) }), /needs 16/);
|
|
219
|
+
assert.throws(() => runmatiz({ width: 0, height: 2, data: new Uint8ClampedArray(0) }), /positive integer/);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// #
|
|
1
|
+
// # mediaprobe.test — honest unit tests for the container probe, runnable with the
|
|
2
2
|
// node built-in runner (no install, no dependencies):
|
|
3
|
-
// node --test tests/
|
|
3
|
+
// node --test tests/mediaprobe.test.ts
|
|
4
4
|
// The fixtures are synthetic MP4 (ISO-BMFF) and WebM (EBML) heads built byte by byte
|
|
5
5
|
// below — no binary files, no canvas, no decoder.
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
7
|
import { describe, it } from "node:test";
|
|
8
|
-
import { probeMedia, ProbeError } from "../
|
|
8
|
+
import { probeMedia, ProbeError } from "../mediaprobe.ts";
|
|
9
9
|
|
|
10
10
|
/** big-endian u32 bytes. */
|
|
11
11
|
function u32(value: number): number[] {
|
|
@@ -129,7 +129,7 @@ function webmFixture(): Uint8Array {
|
|
|
129
129
|
return Uint8Array.from([...header, ...segment]);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
describe("
|
|
132
|
+
describe("mediaprobe mp4", () => {
|
|
133
133
|
const probe = probeMedia(mp4Fixture());
|
|
134
134
|
|
|
135
135
|
it("answers the container, brands and movie duration", () => {
|
|
@@ -155,7 +155,7 @@ describe("media.probe mp4", () => {
|
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
it("feeds the transcode planner as a source", async () => {
|
|
158
|
-
const { buildTranscodePlan } = await import("../
|
|
158
|
+
const { buildTranscodePlan } = await import("../transcodeplan.ts");
|
|
159
159
|
const source = {
|
|
160
160
|
width: probe.tracks[0].width ?? 0,
|
|
161
161
|
height: probe.tracks[0].height ?? 0,
|
|
@@ -172,7 +172,7 @@ describe("media.probe mp4", () => {
|
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
describe("
|
|
175
|
+
describe("mediaprobe webm", () => {
|
|
176
176
|
const probe = probeMedia(webmFixture());
|
|
177
177
|
|
|
178
178
|
it("answers the container, doctype and clock math", () => {
|
|
@@ -193,7 +193,7 @@ describe("media.probe webm", () => {
|
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
-
describe("
|
|
196
|
+
describe("mediaprobe errors", () => {
|
|
197
197
|
it("rejects buffers too small to probe", () => {
|
|
198
198
|
assert.throws(() => probeMedia(new Uint8Array(8)), ProbeError);
|
|
199
199
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// #
|
|
1
|
+
// # transcodeplan.test — honest unit tests for the transcode planner, runnable with
|
|
2
2
|
// the node built-in runner (no install, no dependencies):
|
|
3
|
-
// node --test tests/
|
|
3
|
+
// node --test tests/transcodeplan.test.ts
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import {
|
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
validateRung,
|
|
13
13
|
type SourceMedia,
|
|
14
14
|
type TranscodeRung,
|
|
15
|
-
} from "../
|
|
15
|
+
} from "../transcodeplan.ts";
|
|
16
16
|
|
|
17
|
-
/** The probed source the plan tests build from (the
|
|
17
|
+
/** The probed source the plan tests build from (the mediaprobe.ts shape). */
|
|
18
18
|
const SOURCE: SourceMedia = {
|
|
19
19
|
width: 1920,
|
|
20
20
|
height: 1080,
|
|
@@ -32,7 +32,7 @@ const LADDER: TranscodeRung[] = [
|
|
|
32
32
|
{ id: "480p", height: 480, videoCodec: "vp09", audioCodec: "opus", videoBitrate: "1M", audioBitrate: "64K" },
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
-
describe("
|
|
35
|
+
describe("transcodeplan bitrates", () => {
|
|
36
36
|
it("parses the renderer spellings into bps", () => {
|
|
37
37
|
assert.equal(parseBitrate("4M"), 4_000_000);
|
|
38
38
|
assert.equal(parseBitrate("2500K"), 2_500_000);
|
|
@@ -53,7 +53,7 @@ describe("transcode.plan bitrates", () => {
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
describe("
|
|
56
|
+
describe("transcodeplan validation", () => {
|
|
57
57
|
it("names the failing rung in every thrown error", () => {
|
|
58
58
|
assert.throws(
|
|
59
59
|
() => validateRung({ ...LADDER[1], height: 0 }),
|
|
@@ -86,7 +86,7 @@ describe("transcode.plan validation", () => {
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
describe("
|
|
89
|
+
describe("transcodeplan planner", () => {
|
|
90
90
|
it("builds the runnable variants: never upscales, orders highest-first", () => {
|
|
91
91
|
const plan = buildTranscodePlan(SOURCE, LADDER);
|
|
92
92
|
assert.deepEqual(
|
package/tests/visualize.test.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// The fixture is a synthetic grayscale gradient buffer shaped like ImageData.
|
|
5
5
|
import assert from "node:assert/strict";
|
|
6
6
|
import { describe, it } from "node:test";
|
|
7
|
-
import { gridDimensions } from "../
|
|
8
|
-
import { boundingBox, faceNormal, normalizedScale, vertexNormals } from "../
|
|
9
|
-
import { materialSummary, vertexColors } from "../
|
|
7
|
+
import { gridDimensions } from "../visualizemesh.ts";
|
|
8
|
+
import { boundingBox, faceNormal, normalizedScale, vertexNormals } from "../visualizenormals.ts";
|
|
9
|
+
import { materialSummary, vertexColors } from "../visualizematerial.ts";
|
|
10
10
|
import { visualize, VisualizeError, type ImageInput } from "../visualize.ts";
|
|
11
11
|
|
|
12
12
|
/** builds a w×h grayscale horizontal ramp (left black, right white, alpha full). */
|