@fasl-work/caos-app-shell 0.6.8 → 0.7.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.
package/gate/png.mjs ADDED
@@ -0,0 +1,149 @@
1
+ // A minimal PNG decoder for the gate's own screenshots (8-bit RGB or RGBA, not interlaced: what Chromium writes).
2
+ // The gate decides on painted pixels (G6), never on the host element's box, so it reads the pixels itself instead of
3
+ // trusting what a renderer says it drew. No dependency: zlib is in Node.
4
+
5
+ import { inflateSync } from 'node:zlib';
6
+
7
+ const SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
8
+
9
+ /** @returns {{ width: number, height: number, data: Buffer }} RGBA, 4 bytes per pixel, row-major. */
10
+ export function decodePng(buf) {
11
+ if (!buf.subarray(0, 8).equals(SIGNATURE)) throw new Error('decodePng: not a PNG');
12
+ let pos = 8;
13
+ let width = 0;
14
+ let height = 0;
15
+ let bitDepth = 0;
16
+ let colorType = 0;
17
+ let interlace = 0;
18
+ const idat = [];
19
+ while (pos < buf.length) {
20
+ const len = buf.readUInt32BE(pos);
21
+ const type = buf.toString('ascii', pos + 4, pos + 8);
22
+ const data = buf.subarray(pos + 8, pos + 8 + len);
23
+ pos += 12 + len;
24
+ if (type === 'IHDR') {
25
+ width = data.readUInt32BE(0);
26
+ height = data.readUInt32BE(4);
27
+ bitDepth = data[8];
28
+ colorType = data[9];
29
+ interlace = data[12];
30
+ } else if (type === 'IDAT') idat.push(data);
31
+ else if (type === 'IEND') break;
32
+ }
33
+ if (bitDepth !== 8 || interlace !== 0 || (colorType !== 2 && colorType !== 6)) {
34
+ throw new Error(`decodePng: unsupported PNG (bit depth ${bitDepth}, colour type ${colorType}, interlace ${interlace})`);
35
+ }
36
+ const bpp = colorType === 6 ? 4 : 3;
37
+ const stride = width * bpp;
38
+ const raw = inflateSync(Buffer.concat(idat));
39
+ const out = Buffer.alloc(width * height * 4);
40
+ let prev = Buffer.alloc(stride);
41
+ for (let y = 0; y < height; y += 1) {
42
+ const filter = raw[y * (stride + 1)];
43
+ const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
44
+ const cur = Buffer.alloc(stride);
45
+ for (let x = 0; x < stride; x += 1) {
46
+ const a = x >= bpp ? cur[x - bpp] : 0;
47
+ const b = prev[x];
48
+ const c = x >= bpp ? prev[x - bpp] : 0;
49
+ let v = line[x];
50
+ if (filter === 1) v += a;
51
+ else if (filter === 2) v += b;
52
+ else if (filter === 3) v += (a + b) >> 1;
53
+ else if (filter === 4) {
54
+ const p = a + b - c;
55
+ const pa = Math.abs(p - a);
56
+ const pb = Math.abs(p - b);
57
+ const pc = Math.abs(p - c);
58
+ v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
59
+ }
60
+ cur[x] = v & 255;
61
+ }
62
+ for (let x = 0; x < width; x += 1) {
63
+ const o = (y * width + x) * 4;
64
+ out[o] = cur[x * bpp];
65
+ out[o + 1] = cur[x * bpp + 1];
66
+ out[o + 2] = cur[x * bpp + 2];
67
+ out[o + 3] = bpp === 4 ? cur[x * bpp + 3] : 255;
68
+ }
69
+ prev = cur;
70
+ }
71
+ return { width, height, data: out };
72
+ }
73
+
74
+ /**
75
+ * The painted extent of a region of an image: the bounding box of the pixels that differ from the region's
76
+ * background, taken as the most frequent colour along the region's border ring. A uniform region (nothing drawn)
77
+ * returns null.
78
+ * @param {{ width: number, height: number, data: Buffer }} img
79
+ * @param {{ x: number, y: number, w: number, h: number }} r region in image pixels
80
+ */
81
+ export function paintedBox(img, r, threshold = 24) {
82
+ const x0 = Math.max(0, Math.floor(r.x));
83
+ const y0 = Math.max(0, Math.floor(r.y));
84
+ const x1 = Math.min(img.width, Math.ceil(r.x + r.w));
85
+ const y1 = Math.min(img.height, Math.ceil(r.y + r.h));
86
+ if (x1 - x0 < 2 || y1 - y0 < 2) return null;
87
+ const d = img.data;
88
+ const counts = new Map();
89
+ const vote = (x, y) => {
90
+ const o = (y * img.width + x) * 4;
91
+ const key = ((d[o] >> 3) << 10) | ((d[o + 1] >> 3) << 5) | (d[o + 2] >> 3);
92
+ const e = counts.get(key);
93
+ if (e) {
94
+ e.n += 1;
95
+ e.r += d[o];
96
+ e.g += d[o + 1];
97
+ e.b += d[o + 2];
98
+ } else counts.set(key, { n: 1, r: d[o], g: d[o + 1], b: d[o + 2] });
99
+ };
100
+ for (let x = x0; x < x1; x += 1) {
101
+ vote(x, y0);
102
+ vote(x, y1 - 1);
103
+ }
104
+ for (let y = y0; y < y1; y += 1) {
105
+ vote(x0, y);
106
+ vote(x1 - 1, y);
107
+ }
108
+ let best = null;
109
+ for (const e of counts.values()) if (!best || e.n > best.n) best = e;
110
+ const br = best.r / best.n;
111
+ const bg = best.g / best.n;
112
+ const bb = best.b / best.n;
113
+ let minX = Infinity;
114
+ let minY = Infinity;
115
+ let maxX = -1;
116
+ let maxY = -1;
117
+ let painted = 0;
118
+ for (let y = y0; y < y1; y += 1) {
119
+ let o = (y * img.width + x0) * 4;
120
+ for (let x = x0; x < x1; x += 1, o += 4) {
121
+ if (Math.abs(d[o] - br) > threshold || Math.abs(d[o + 1] - bg) > threshold || Math.abs(d[o + 2] - bb) > threshold) {
122
+ painted += 1;
123
+ if (x < minX) minX = x;
124
+ if (x > maxX) maxX = x;
125
+ if (y < minY) minY = y;
126
+ if (y > maxY) maxY = y;
127
+ }
128
+ }
129
+ }
130
+ if (maxX < 0) return null;
131
+ return { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1, painted };
132
+ }
133
+
134
+ /** The area of a union of rectangles, rasterised on a grid of `cell` pixels. */
135
+ export function unionArea(rects, width, height, cell = 4) {
136
+ const cols = Math.ceil(width / cell);
137
+ const rows = Math.ceil(height / cell);
138
+ const grid = new Uint8Array(cols * rows);
139
+ for (const r of rects) {
140
+ const c0 = Math.max(0, Math.floor(r.x / cell));
141
+ const c1 = Math.min(cols, Math.ceil((r.x + r.w) / cell));
142
+ const r0 = Math.max(0, Math.floor(r.y / cell));
143
+ const r1 = Math.min(rows, Math.ceil((r.y + r.h) / cell));
144
+ for (let y = r0; y < r1; y += 1) grid.fill(1, y * cols + c0, y * cols + c1);
145
+ }
146
+ let n = 0;
147
+ for (let i = 0; i < grid.length; i += 1) n += grid[i];
148
+ return n * cell * cell;
149
+ }