spine-rigc 0.2.1
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/LICENSE +21 -0
- package/NOTICE.md +76 -0
- package/README.md +558 -0
- package/cli.ts +739 -0
- package/docs/AUTHORING.md +1303 -0
- package/docs/SPEC_COVERAGE.md +1109 -0
- package/package.json +65 -0
- package/src/check.ts +1714 -0
- package/src/compile.ts +1861 -0
- package/src/diff.ts +847 -0
- package/src/errors.ts +22 -0
- package/src/framing.ts +539 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +433 -0
- package/src/png.ts +50 -0
- package/src/render.ts +974 -0
- package/src/rig.ts +731 -0
- package/src/slots.ts +603 -0
- package/src/timelines.ts +253 -0
- package/src/transform.ts +130 -0
- package/src/types.ts +586 -0
- package/src/validate.ts +1586 -0
- package/tools/font5x7.ts +101 -0
- package/tools/plate.ts +286 -0
package/tools/font5x7.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A 5x7 bitmap font, uppercase + digits + a little punctuation.
|
|
3
|
+
*
|
|
4
|
+
* It exists so a generated plate can burn its own name into its own pixels with
|
|
5
|
+
* no font file, no canvas and no dependency. That matters more than it sounds:
|
|
6
|
+
* the whole point of a synthetic stand-in plate is that nobody can mistake it
|
|
7
|
+
* for real art, and the strongest form of that is the word PLACEHOLDER sitting
|
|
8
|
+
* in the image itself.
|
|
9
|
+
*
|
|
10
|
+
* Each glyph is 7 rows of 5 bits, MSB = leftmost column.
|
|
11
|
+
*/
|
|
12
|
+
const GLYPHS: Record<string, number[]> = {
|
|
13
|
+
A: [0x0e, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11],
|
|
14
|
+
B: [0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e],
|
|
15
|
+
C: [0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e],
|
|
16
|
+
D: [0x1e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1e],
|
|
17
|
+
E: [0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f],
|
|
18
|
+
F: [0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10],
|
|
19
|
+
G: [0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0e],
|
|
20
|
+
H: [0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11],
|
|
21
|
+
I: [0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e],
|
|
22
|
+
J: [0x07, 0x02, 0x02, 0x02, 0x02, 0x12, 0x0c],
|
|
23
|
+
K: [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11],
|
|
24
|
+
L: [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f],
|
|
25
|
+
M: [0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11],
|
|
26
|
+
N: [0x11, 0x19, 0x19, 0x15, 0x13, 0x13, 0x11],
|
|
27
|
+
O: [0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e],
|
|
28
|
+
P: [0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10],
|
|
29
|
+
Q: [0x0e, 0x11, 0x11, 0x11, 0x15, 0x13, 0x0d],
|
|
30
|
+
R: [0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11],
|
|
31
|
+
S: [0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e],
|
|
32
|
+
T: [0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
|
|
33
|
+
U: [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e],
|
|
34
|
+
V: [0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04],
|
|
35
|
+
W: [0x11, 0x11, 0x11, 0x15, 0x15, 0x1b, 0x11],
|
|
36
|
+
X: [0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11],
|
|
37
|
+
Y: [0x11, 0x11, 0x0a, 0x04, 0x04, 0x04, 0x04],
|
|
38
|
+
Z: [0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f],
|
|
39
|
+
'0': [0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e],
|
|
40
|
+
'1': [0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e],
|
|
41
|
+
'2': [0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f],
|
|
42
|
+
'3': [0x1f, 0x02, 0x04, 0x06, 0x01, 0x11, 0x0e],
|
|
43
|
+
'4': [0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02],
|
|
44
|
+
'5': [0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e],
|
|
45
|
+
'6': [0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e],
|
|
46
|
+
'7': [0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08],
|
|
47
|
+
'8': [0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e],
|
|
48
|
+
'9': [0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c],
|
|
49
|
+
' ': [0, 0, 0, 0, 0, 0, 0],
|
|
50
|
+
_: [0, 0, 0, 0, 0, 0, 0x1f],
|
|
51
|
+
'-': [0, 0, 0, 0x1f, 0, 0, 0],
|
|
52
|
+
'.': [0, 0, 0, 0, 0, 0x0c, 0x0c],
|
|
53
|
+
',': [0, 0, 0, 0, 0x0c, 0x0c, 0x04],
|
|
54
|
+
':': [0, 0x0c, 0x0c, 0, 0x0c, 0x0c, 0],
|
|
55
|
+
'/': [0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10],
|
|
56
|
+
'(': [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02],
|
|
57
|
+
')': [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08],
|
|
58
|
+
'!': [0x04, 0x04, 0x04, 0x04, 0x04, 0, 0x04],
|
|
59
|
+
'+': [0, 0x04, 0x04, 0x1f, 0x04, 0x04, 0],
|
|
60
|
+
'=': [0, 0, 0x1f, 0, 0x1f, 0, 0],
|
|
61
|
+
'<': [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02],
|
|
62
|
+
'>': [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const GLYPH_W = 5;
|
|
66
|
+
export const GLYPH_H = 7;
|
|
67
|
+
|
|
68
|
+
/** Width in pixels of `text` drawn at `scale` with a one-column gap. */
|
|
69
|
+
export function textWidth(text: string, scale: number): number {
|
|
70
|
+
if (!text.length) return 0;
|
|
71
|
+
return (text.length * (GLYPH_W + 1) - 1) * scale;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Rasterise `text` by calling `plot(x, y)` for every lit pixel, with the top
|
|
76
|
+
* left of the first glyph at (x0, y0). Unknown characters draw as a full block
|
|
77
|
+
* so a typo is loud rather than invisible.
|
|
78
|
+
*/
|
|
79
|
+
export function drawText(
|
|
80
|
+
text: string,
|
|
81
|
+
x0: number,
|
|
82
|
+
y0: number,
|
|
83
|
+
scale: number,
|
|
84
|
+
plot: (x: number, y: number) => void,
|
|
85
|
+
): void {
|
|
86
|
+
let cursor = x0;
|
|
87
|
+
for (const ch of text.toUpperCase()) {
|
|
88
|
+
const glyph = GLYPHS[ch] ?? [0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f];
|
|
89
|
+
for (let row = 0; row < GLYPH_H; row++) {
|
|
90
|
+
for (let col = 0; col < GLYPH_W; col++) {
|
|
91
|
+
if (!(glyph[row] & (1 << (GLYPH_W - 1 - col)))) continue;
|
|
92
|
+
for (let sy = 0; sy < scale; sy++) {
|
|
93
|
+
for (let sx = 0; sx < scale; sx++) {
|
|
94
|
+
plot(cursor + col * scale + sx, y0 + row * scale + sy);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
cursor += (GLYPH_W + 1) * scale;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/tools/plate.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal RGBA canvas + PNG writer.
|
|
3
|
+
*
|
|
4
|
+
* `src/png.ts` reads 26 bytes of header because that is all the compiler needs;
|
|
5
|
+
* this writes whole files, because a rig has to have something to point at
|
|
6
|
+
* before its art exists — synthetic stand-in plates, reference frames, the
|
|
7
|
+
* selftest's own fixtures. Node's zlib is the only thing it leans on, so the
|
|
8
|
+
* rigc package still has exactly one dependency.
|
|
9
|
+
*
|
|
10
|
+
* Straight (non-premultiplied) alpha, colour type 6. Premultiplied alpha is the
|
|
11
|
+
* single most consequential export setting to get wrong — the renderer does not
|
|
12
|
+
* un-premultiply, so every part gains a dark rim — and a writer that cannot
|
|
13
|
+
* produce it is the cheapest possible guarantee.
|
|
14
|
+
*/
|
|
15
|
+
import { deflateSync, inflateSync } from 'node:zlib';
|
|
16
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { drawText, textWidth } from './font5x7.ts';
|
|
18
|
+
|
|
19
|
+
export type RGBA = [number, number, number, number];
|
|
20
|
+
|
|
21
|
+
export class Plate {
|
|
22
|
+
readonly width: number;
|
|
23
|
+
readonly height: number;
|
|
24
|
+
readonly data: Uint8Array;
|
|
25
|
+
|
|
26
|
+
constructor(width: number, height: number) {
|
|
27
|
+
this.width = width;
|
|
28
|
+
this.height = height;
|
|
29
|
+
this.data = new Uint8Array(width * height * 4);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Source-over composite of one pixel. Alpha in 0..255. */
|
|
33
|
+
blend(x: number, y: number, [r, g, b, a]: RGBA): void {
|
|
34
|
+
const px = Math.round(x);
|
|
35
|
+
const py = Math.round(y);
|
|
36
|
+
if (px < 0 || py < 0 || px >= this.width || py >= this.height) return;
|
|
37
|
+
if (a <= 0) return;
|
|
38
|
+
const i = (py * this.width + px) * 4;
|
|
39
|
+
const sa = a / 255;
|
|
40
|
+
const da = this.data[i + 3] / 255;
|
|
41
|
+
const oa = sa + da * (1 - sa);
|
|
42
|
+
if (oa <= 0) return;
|
|
43
|
+
for (let c = 0; c < 3; c++) {
|
|
44
|
+
const sc = [r, g, b][c];
|
|
45
|
+
this.data[i + c] = Math.round((sc * sa + this.data[i + c] * da * (1 - sa)) / oa);
|
|
46
|
+
}
|
|
47
|
+
this.data[i + 3] = Math.round(oa * 255);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Overwrite one pixel outright, alpha included. */
|
|
51
|
+
set(x: number, y: number, [r, g, b, a]: RGBA): void {
|
|
52
|
+
const px = Math.round(x);
|
|
53
|
+
const py = Math.round(y);
|
|
54
|
+
if (px < 0 || py < 0 || px >= this.width || py >= this.height) return;
|
|
55
|
+
const i = (py * this.width + px) * 4;
|
|
56
|
+
this.data[i] = r;
|
|
57
|
+
this.data[i + 1] = g;
|
|
58
|
+
this.data[i + 2] = b;
|
|
59
|
+
this.data[i + 3] = a;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get(x: number, y: number): RGBA {
|
|
63
|
+
const i = (y * this.width + x) * 4;
|
|
64
|
+
return [this.data[i], this.data[i + 1], this.data[i + 2], this.data[i + 3]];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Multiply the whole plate's alpha by a per-pixel factor in 0..1. */
|
|
68
|
+
maskAlpha(f: (x: number, y: number) => number): void {
|
|
69
|
+
for (let y = 0; y < this.height; y++) {
|
|
70
|
+
for (let x = 0; x < this.width; x++) {
|
|
71
|
+
const i = (y * this.width + x) * 4;
|
|
72
|
+
const k = Math.max(0, Math.min(1, f(x, y)));
|
|
73
|
+
this.data[i + 3] = Math.round(this.data[i + 3] * k);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
rect(x0: number, y0: number, w: number, h: number, colour: RGBA): void {
|
|
79
|
+
for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) this.blend(x, y, colour);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
frame(x0: number, y0: number, w: number, h: number, t: number, colour: RGBA): void {
|
|
83
|
+
this.rect(x0, y0, w, t, colour);
|
|
84
|
+
this.rect(x0, y0 + h - t, w, t, colour);
|
|
85
|
+
this.rect(x0, y0, t, h, colour);
|
|
86
|
+
this.rect(x0 + w - t, y0, t, h, colour);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
line(x0: number, y0: number, x1: number, y1: number, t: number, colour: RGBA): void {
|
|
90
|
+
const steps = Math.ceil(Math.hypot(x1 - x0, y1 - y0)) * 2 + 1;
|
|
91
|
+
for (let s = 0; s <= steps; s++) {
|
|
92
|
+
const u = s / steps;
|
|
93
|
+
const cx = x0 + (x1 - x0) * u;
|
|
94
|
+
const cy = y0 + (y1 - y0) * u;
|
|
95
|
+
for (let dy = -t; dy <= t; dy++) for (let dx = -t; dx <= t; dx++) this.blend(cx + dx, cy + dy, colour);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
disc(cx: number, cy: number, r: number, colour: RGBA): void {
|
|
100
|
+
for (let y = Math.floor(cy - r); y <= Math.ceil(cy + r); y++) {
|
|
101
|
+
for (let x = Math.floor(cx - r); x <= Math.ceil(cx + r); x++) {
|
|
102
|
+
if (Math.hypot(x - cx, y - cy) <= r) this.blend(x, y, colour);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ring(cx: number, cy: number, r: number, t: number, colour: RGBA): void {
|
|
108
|
+
for (let y = Math.floor(cy - r - t); y <= Math.ceil(cy + r + t); y++) {
|
|
109
|
+
for (let x = Math.floor(cx - r - t); x <= Math.ceil(cx + r + t); x++) {
|
|
110
|
+
const d = Math.hypot(x - cx, y - cy);
|
|
111
|
+
if (Math.abs(d - r) <= t) this.blend(x, y, colour);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
text(str: string, x: number, y: number, scale: number, colour: RGBA): void {
|
|
117
|
+
drawText(str, x, y, scale, (px, py) => this.blend(px, py, colour));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
textCentred(str: string, cx: number, y: number, scale: number, colour: RGBA): void {
|
|
121
|
+
this.text(str, Math.round(cx - textWidth(str, scale) / 2), y, scale, colour);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Alpha-weighted pixel count, and how much of it sits outside a predicate. */
|
|
125
|
+
alphaStats(inside: (x: number, y: number) => boolean): {
|
|
126
|
+
total: number;
|
|
127
|
+
outside: number;
|
|
128
|
+
outsideMaxAlpha: number;
|
|
129
|
+
} {
|
|
130
|
+
let total = 0;
|
|
131
|
+
let outside = 0;
|
|
132
|
+
let outsideMaxAlpha = 0;
|
|
133
|
+
for (let y = 0; y < this.height; y++) {
|
|
134
|
+
for (let x = 0; x < this.width; x++) {
|
|
135
|
+
const a = this.data[(y * this.width + x) * 4 + 3];
|
|
136
|
+
if (a === 0) continue;
|
|
137
|
+
total++;
|
|
138
|
+
if (!inside(x, y)) {
|
|
139
|
+
outside++;
|
|
140
|
+
if (a > outsideMaxAlpha) outsideMaxAlpha = a;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { total, outside, outsideMaxAlpha };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
writePng(path: string): void {
|
|
148
|
+
writeFileSync(path, encodePng(this.width, this.height, this.data));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// PNG encoding
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
const CRC_TABLE = (() => {
|
|
157
|
+
const table = new Uint32Array(256);
|
|
158
|
+
for (let n = 0; n < 256; n++) {
|
|
159
|
+
let c = n;
|
|
160
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
161
|
+
table[n] = c >>> 0;
|
|
162
|
+
}
|
|
163
|
+
return table;
|
|
164
|
+
})();
|
|
165
|
+
|
|
166
|
+
function crc32(buf: Uint8Array): number {
|
|
167
|
+
let c = 0xffffffff;
|
|
168
|
+
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
169
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function chunk(type: string, body: Uint8Array): Uint8Array {
|
|
173
|
+
const out = new Uint8Array(body.length + 12);
|
|
174
|
+
const view = new DataView(out.buffer);
|
|
175
|
+
view.setUint32(0, body.length);
|
|
176
|
+
for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
|
|
177
|
+
out.set(body, 8);
|
|
178
|
+
view.setUint32(body.length + 8, crc32(out.subarray(4, body.length + 8)));
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function encodePng(width: number, height: number, rgba: Uint8Array): Uint8Array {
|
|
183
|
+
// Filter type 0 (none) on every scanline. The plates are flat colour and
|
|
184
|
+
// deterministic output is worth more here than a few kilobytes.
|
|
185
|
+
const raw = new Uint8Array(height * (width * 4 + 1));
|
|
186
|
+
for (let y = 0; y < height; y++) {
|
|
187
|
+
raw[y * (width * 4 + 1)] = 0;
|
|
188
|
+
raw.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (width * 4 + 1) + 1);
|
|
189
|
+
}
|
|
190
|
+
const ihdr = new Uint8Array(13);
|
|
191
|
+
const view = new DataView(ihdr.buffer);
|
|
192
|
+
view.setUint32(0, width);
|
|
193
|
+
view.setUint32(4, height);
|
|
194
|
+
ihdr[8] = 8; // bit depth
|
|
195
|
+
ihdr[9] = 6; // colour type: truecolour + alpha
|
|
196
|
+
ihdr[10] = 0;
|
|
197
|
+
ihdr[11] = 0;
|
|
198
|
+
ihdr[12] = 0;
|
|
199
|
+
const parts = [
|
|
200
|
+
new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
201
|
+
chunk('IHDR', ihdr),
|
|
202
|
+
chunk('IDAT', new Uint8Array(deflateSync(raw, { level: 9 }))),
|
|
203
|
+
chunk('IEND', new Uint8Array(0)),
|
|
204
|
+
];
|
|
205
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
206
|
+
const out = new Uint8Array(total);
|
|
207
|
+
let at = 0;
|
|
208
|
+
for (const p of parts) {
|
|
209
|
+
out.set(p, at);
|
|
210
|
+
at += p.length;
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// PNG decoding
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Read an 8-bit RGB/RGBA PNG back into a `Plate`.
|
|
221
|
+
*
|
|
222
|
+
* `src/png.ts` parses 26 bytes because that is all the COMPILER needs, and it
|
|
223
|
+
* stays that way: the compiler never re-measures art. This
|
|
224
|
+
* decoder is for the measuring tools, which is where art measurement belongs -
|
|
225
|
+
* the same division that puts `mesh.center` in the manifest as a measured number
|
|
226
|
+
* rather than as something the compiler derives at build time.
|
|
227
|
+
*/
|
|
228
|
+
export function decodePng(buf: Uint8Array): Plate {
|
|
229
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
230
|
+
let at = 8;
|
|
231
|
+
let width = 0;
|
|
232
|
+
let height = 0;
|
|
233
|
+
let colourType = 6;
|
|
234
|
+
const idat: Uint8Array[] = [];
|
|
235
|
+
while (at < buf.length) {
|
|
236
|
+
const len = view.getUint32(at);
|
|
237
|
+
const type = String.fromCharCode(buf[at + 4], buf[at + 5], buf[at + 6], buf[at + 7]);
|
|
238
|
+
const body = buf.subarray(at + 8, at + 8 + len);
|
|
239
|
+
if (type === 'IHDR') {
|
|
240
|
+
width = view.getUint32(at + 8);
|
|
241
|
+
height = view.getUint32(at + 12);
|
|
242
|
+
if (body[8] !== 8) throw new Error(`unsupported bit depth ${body[8]}`);
|
|
243
|
+
colourType = body[9];
|
|
244
|
+
if (body[12] !== 0) throw new Error('interlaced PNG');
|
|
245
|
+
} else if (type === 'IDAT') idat.push(body);
|
|
246
|
+
at += 12 + len;
|
|
247
|
+
}
|
|
248
|
+
const bpp = colourType === 6 ? 4 : colourType === 2 ? 3 : 0;
|
|
249
|
+
if (!bpp) throw new Error(`unsupported colour type ${colourType}`);
|
|
250
|
+
const raw = new Uint8Array(inflateSync(Buffer.concat(idat.map((c) => Buffer.from(c)))));
|
|
251
|
+
const stride = width * bpp + 1;
|
|
252
|
+
const plate = new Plate(width, height);
|
|
253
|
+
let prev = new Uint8Array(width * bpp);
|
|
254
|
+
for (let y = 0; y < height; y++) {
|
|
255
|
+
const filter = raw[y * stride];
|
|
256
|
+
const line = raw.slice(y * stride + 1, (y + 1) * stride);
|
|
257
|
+
for (let x = 0; x < width * bpp; x++) {
|
|
258
|
+
const a = x >= bpp ? line[x - bpp] : 0;
|
|
259
|
+
const b = prev[x];
|
|
260
|
+
const c = x >= bpp ? prev[x - bpp] : 0;
|
|
261
|
+
if (filter === 1) line[x] = (line[x] + a) & 255;
|
|
262
|
+
else if (filter === 2) line[x] = (line[x] + b) & 255;
|
|
263
|
+
else if (filter === 3) line[x] = (line[x] + ((a + b) >> 1)) & 255;
|
|
264
|
+
else if (filter === 4) {
|
|
265
|
+
const p = a + b - c;
|
|
266
|
+
const pa = Math.abs(p - a);
|
|
267
|
+
const pb = Math.abs(p - b);
|
|
268
|
+
const pc = Math.abs(p - c);
|
|
269
|
+
line[x] = (line[x] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 255;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (let x = 0; x < width; x++) {
|
|
273
|
+
const o = (y * width + x) * 4;
|
|
274
|
+
plate.data[o] = line[x * bpp];
|
|
275
|
+
plate.data[o + 1] = line[x * bpp + 1];
|
|
276
|
+
plate.data[o + 2] = line[x * bpp + 2];
|
|
277
|
+
plate.data[o + 3] = bpp === 4 ? line[x * bpp + 3] : 255;
|
|
278
|
+
}
|
|
279
|
+
prev = line;
|
|
280
|
+
}
|
|
281
|
+
return plate;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function readPlate(path: string): Plate {
|
|
285
|
+
return decodePng(readFileSync(path));
|
|
286
|
+
}
|