arcane-os 0.59.1 → 0.60.0

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.
@@ -0,0 +1,159 @@
1
+ import {inflate} from 'node:zlib';
2
+ import {promisify} from 'node:util';
3
+ import {setImmediate} from 'node:timers/promises';
4
+
5
+ const inflatePNG = promisify(inflate);
6
+
7
+ function imageError(code, message) {
8
+ const error = new Error(message);
9
+ error.code = code;
10
+ return error;
11
+ }
12
+
13
+ function malformed(message) {
14
+ return imageError('ARCANE_IMAGE_DECODE_FAILED', message);
15
+ }
16
+
17
+ function unsupported(message) {
18
+ return imageError('ARCANE_IMAGE_INPUT_UNSUPPORTED', message);
19
+ }
20
+
21
+ /**
22
+ * Decode a complete encoded PNG in the shared image runtime's owning worker.
23
+ * The returned pixels are a separate allocation retained through native completion.
24
+ */
25
+ export async function decodeImage({data, mediaType, signal} = {}) {
26
+ signal?.throwIfAborted();
27
+ if (!(data instanceof Uint8Array)) {
28
+ throw new TypeError('Image decoding requires encoded Uint8Array data.');
29
+ }
30
+ if (mediaType !== undefined && mediaType !== '' && mediaType !== 'image/png') {
31
+ throw unsupported('Local image editing currently accepts PNG input.');
32
+ }
33
+
34
+ const encoded = Buffer.from(data);
35
+ const signature = [137, 80, 78, 71, 13, 10, 26, 10];
36
+ for (const [index, value] of signature.entries()) {
37
+ if (encoded[index] !== value) throw unsupported('The supplied image is not a PNG.');
38
+ }
39
+
40
+ const imageData = [];
41
+ let header;
42
+ let transparency;
43
+ let ended = false;
44
+ let offset = signature.length;
45
+ while (offset < encoded.length) {
46
+ signal?.throwIfAborted();
47
+ // These lengths describe PNG chunk framing, not image admission limits.
48
+ if (encoded.length - offset < 12) throw malformed('The PNG ends inside a chunk.');
49
+ const length = encoded.readUInt32BE(offset);
50
+ const type = encoded.toString('ascii', offset + 4, offset + 8);
51
+ const start = offset + 8;
52
+ const end = start + length;
53
+ if (end + 4 > encoded.length) throw malformed(`The PNG ${type} chunk is incomplete.`);
54
+ if (!header && type !== 'IHDR') throw malformed('The PNG is missing its initial IHDR chunk.');
55
+
56
+ if (type === 'IHDR') {
57
+ if (header || length !== 13) throw malformed('The PNG IHDR chunk is invalid.');
58
+ header = {
59
+ width: encoded.readUInt32BE(start),
60
+ height: encoded.readUInt32BE(start + 4),
61
+ depth: encoded[start + 8],
62
+ color: encoded[start + 9],
63
+ compression: encoded[start + 10],
64
+ filter: encoded[start + 11],
65
+ interlace: encoded[start + 12]
66
+ };
67
+ if (!header.width || !header.height) throw malformed('PNG dimensions must be nonzero.');
68
+ if (header.depth !== 8 || (header.color !== 2 && header.color !== 6)
69
+ || header.compression !== 0 || header.filter !== 0 || header.interlace !== 0) {
70
+ throw unsupported('Local image editing accepts non-interlaced 8-bit RGB or RGBA PNG images.');
71
+ }
72
+ } else if (type === 'IDAT') {
73
+ imageData.push(encoded.subarray(start, end));
74
+ } else if (type === 'tRNS') {
75
+ if (header.color !== 2 || length !== 6 || transparency) {
76
+ throw malformed('The PNG transparency chunk is invalid for its color type.');
77
+ }
78
+ // PNG uses only the low sample bits for an 8-bit transparent color.
79
+ transparency = [
80
+ encoded.readUInt16BE(start) & 255,
81
+ encoded.readUInt16BE(start + 2) & 255,
82
+ encoded.readUInt16BE(start + 4) & 255
83
+ ];
84
+ } else if (type === 'acTL' || type === 'fcTL' || type === 'fdAT') {
85
+ throw unsupported('Local image editing requires a still PNG; animated PNG input is unsupported.');
86
+ } else if (type === 'IEND') {
87
+ if (length !== 0) throw malformed('The PNG IEND chunk is invalid.');
88
+ ended = true;
89
+ break;
90
+ } else if (type !== 'PLTE' && (encoded[offset + 4] & 32) === 0) {
91
+ throw unsupported(`The PNG critical chunk ${type} is unsupported.`);
92
+ }
93
+ offset = end + 4;
94
+ }
95
+ if (!header || !ended || !imageData.length) throw malformed('The PNG image stream is incomplete.');
96
+
97
+ signal?.throwIfAborted();
98
+ // All IDAT chunks form one zlib stream. An abort still joins its completion.
99
+ const filtered = await inflatePNG(Buffer.concat(imageData));
100
+ signal?.throwIfAborted();
101
+ const {width, height} = header;
102
+ const sourceChannels = header.color === 6 ? 4 : 3;
103
+ const channel = transparency ? 4 : sourceChannels;
104
+ const rowLength = width * sourceChannels;
105
+ const pixels = new Uint8Array(width * height * channel);
106
+ let previous = new Uint8Array(rowLength);
107
+ let row = new Uint8Array(rowLength);
108
+ let position = 0;
109
+
110
+ for (let y = 0; y < height; y += 1) {
111
+ signal?.throwIfAborted();
112
+ if (position + 1 + rowLength > filtered.length) throw malformed('The PNG ends inside an image row.');
113
+ const filter = filtered[position++];
114
+ if (filter > 4) throw malformed(`The PNG row filter ${filter} is unsupported.`);
115
+ for (let x = 0; x < rowLength; x += 1) {
116
+ const left = x >= sourceChannels ? row[x - sourceChannels] : 0;
117
+ const above = previous[x];
118
+ const upperLeft = x >= sourceChannels ? previous[x - sourceChannels] : 0;
119
+ let prediction = 0;
120
+ if (filter === 1) prediction = left;
121
+ else if (filter === 2) prediction = above;
122
+ else if (filter === 3) prediction = Math.floor((left + above) / 2);
123
+ else if (filter === 4) prediction = paeth(left, above, upperLeft);
124
+ row[x] = (filtered[position++] + prediction) & 255;
125
+ }
126
+
127
+ if (transparency) {
128
+ for (let x = 0; x < width; x += 1) {
129
+ const source = x * sourceChannels;
130
+ const target = (y * width + x) * channel;
131
+ pixels[target] = row[source];
132
+ pixels[target + 1] = row[source + 1];
133
+ pixels[target + 2] = row[source + 2];
134
+ pixels[target + 3] = row[source] === transparency[0]
135
+ && row[source + 1] === transparency[1]
136
+ && row[source + 2] === transparency[2] ? 0 : 255;
137
+ }
138
+ } else {
139
+ pixels.set(row, y * width * channel);
140
+ }
141
+ const reusable = previous;
142
+ previous = row;
143
+ row = reusable;
144
+ // Let the owning worker receive cancellation between scanlines.
145
+ await setImmediate(undefined, {signal});
146
+ }
147
+ if (position !== filtered.length) throw malformed('The PNG contains data beyond its declared image rows.');
148
+ signal?.throwIfAborted();
149
+ return {width, height, channel, data: pixels};
150
+ }
151
+
152
+ function paeth(left, above, upperLeft) {
153
+ const prediction = left + above - upperLeft;
154
+ const fromLeft = Math.abs(prediction - left);
155
+ const fromAbove = Math.abs(prediction - above);
156
+ const fromUpperLeft = Math.abs(prediction - upperLeft);
157
+ if (fromLeft <= fromAbove && fromLeft <= fromUpperLeft) return left;
158
+ return fromAbove <= fromUpperLeft ? above : upperLeft;
159
+ }