feat(gateway): add agent image attachments + live probe
This commit is contained in:
206
src/gateway/live-image-probe.ts
Normal file
206
src/gateway/live-image-probe.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { deflateSync } from "node:zlib";
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i += 1) {
|
||||
let c = i;
|
||||
for (let k = 0; k < 8; k += 1) {
|
||||
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
}
|
||||
table[i] = c >>> 0;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
function crc32(buf: Buffer) {
|
||||
let crc = 0xffffffff;
|
||||
for (let i = 0; i < buf.length; i += 1) {
|
||||
crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function pngChunk(type: string, data: Buffer) {
|
||||
const typeBuf = Buffer.from(type, "ascii");
|
||||
const len = Buffer.alloc(4);
|
||||
len.writeUInt32BE(data.length, 0);
|
||||
const crc = crc32(Buffer.concat([typeBuf, data]));
|
||||
const crcBuf = Buffer.alloc(4);
|
||||
crcBuf.writeUInt32BE(crc, 0);
|
||||
return Buffer.concat([len, typeBuf, data, crcBuf]);
|
||||
}
|
||||
|
||||
function encodePngRgba(buffer: Buffer, width: number, height: number) {
|
||||
const stride = width * 4;
|
||||
const raw = Buffer.alloc((stride + 1) * height);
|
||||
for (let row = 0; row < height; row += 1) {
|
||||
const rawOffset = row * (stride + 1);
|
||||
raw[rawOffset] = 0; // filter: none
|
||||
buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride);
|
||||
}
|
||||
const compressed = deflateSync(raw);
|
||||
|
||||
const signature = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
]);
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(width, 0);
|
||||
ihdr.writeUInt32BE(height, 4);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 6; // color type RGBA
|
||||
ihdr[10] = 0; // compression
|
||||
ihdr[11] = 0; // filter
|
||||
ihdr[12] = 0; // interlace
|
||||
|
||||
return Buffer.concat([
|
||||
signature,
|
||||
pngChunk("IHDR", ihdr),
|
||||
pngChunk("IDAT", compressed),
|
||||
pngChunk("IEND", Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
|
||||
function fillPixel(
|
||||
buf: Buffer,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a = 255,
|
||||
) {
|
||||
if (x < 0 || y < 0) return;
|
||||
if (x >= width) return;
|
||||
const idx = (y * width + x) * 4;
|
||||
if (idx < 0 || idx + 3 >= buf.length) return;
|
||||
buf[idx] = r;
|
||||
buf[idx + 1] = g;
|
||||
buf[idx + 2] = b;
|
||||
buf[idx + 3] = a;
|
||||
}
|
||||
|
||||
const GLYPH_ROWS_5X7: Record<string, number[]> = {
|
||||
"0": [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
|
||||
"1": [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
|
||||
"2": [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
|
||||
"3": [0b11110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b11110],
|
||||
"4": [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
|
||||
"5": [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
|
||||
"6": [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
|
||||
"7": [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
|
||||
"8": [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
|
||||
"9": [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
|
||||
|
||||
A: [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
|
||||
B: [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
|
||||
C: [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
|
||||
D: [0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110],
|
||||
E: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
|
||||
F: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
|
||||
T: [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
|
||||
};
|
||||
|
||||
function drawGlyph5x7(params: {
|
||||
buf: Buffer;
|
||||
width: number;
|
||||
x: number;
|
||||
y: number;
|
||||
char: string;
|
||||
scale: number;
|
||||
color: { r: number; g: number; b: number; a?: number };
|
||||
}) {
|
||||
const rows = GLYPH_ROWS_5X7[params.char];
|
||||
if (!rows) return;
|
||||
for (let row = 0; row < 7; row += 1) {
|
||||
const bits = rows[row] ?? 0;
|
||||
for (let col = 0; col < 5; col += 1) {
|
||||
const on = (bits & (1 << (4 - col))) !== 0;
|
||||
if (!on) continue;
|
||||
for (let dy = 0; dy < params.scale; dy += 1) {
|
||||
for (let dx = 0; dx < params.scale; dx += 1) {
|
||||
fillPixel(
|
||||
params.buf,
|
||||
params.x + col * params.scale + dx,
|
||||
params.y + row * params.scale + dy,
|
||||
params.width,
|
||||
params.color.r,
|
||||
params.color.g,
|
||||
params.color.b,
|
||||
params.color.a ?? 255,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawText(params: {
|
||||
buf: Buffer;
|
||||
width: number;
|
||||
x: number;
|
||||
y: number;
|
||||
text: string;
|
||||
scale: number;
|
||||
color: { r: number; g: number; b: number; a?: number };
|
||||
}) {
|
||||
const text = params.text.toUpperCase();
|
||||
let cursorX = params.x;
|
||||
for (const raw of text) {
|
||||
const ch = raw in GLYPH_ROWS_5X7 ? raw : raw.toUpperCase();
|
||||
drawGlyph5x7({
|
||||
buf: params.buf,
|
||||
width: params.width,
|
||||
x: cursorX,
|
||||
y: params.y,
|
||||
char: ch,
|
||||
scale: params.scale,
|
||||
color: params.color,
|
||||
});
|
||||
cursorX += 6 * params.scale;
|
||||
}
|
||||
}
|
||||
|
||||
function measureTextWidthPx(text: string, scale: number) {
|
||||
return text.length * 6 * scale - scale; // 5px glyph + 1px space
|
||||
}
|
||||
|
||||
export function renderCatNoncePngBase64(nonce: string): string {
|
||||
const top = "CAT";
|
||||
const bottom = nonce.toUpperCase();
|
||||
|
||||
const scale = 12;
|
||||
const pad = 18;
|
||||
const gap = 18;
|
||||
|
||||
const topWidth = measureTextWidthPx(top, scale);
|
||||
const bottomWidth = measureTextWidthPx(bottom, scale);
|
||||
const width = Math.max(topWidth, bottomWidth) + pad * 2;
|
||||
const height = pad * 2 + 7 * scale + gap + 7 * scale;
|
||||
|
||||
const buf = Buffer.alloc(width * height * 4, 255);
|
||||
const black = { r: 0, g: 0, b: 0 };
|
||||
|
||||
drawText({
|
||||
buf,
|
||||
width,
|
||||
x: Math.floor((width - topWidth) / 2),
|
||||
y: pad,
|
||||
text: top,
|
||||
scale,
|
||||
color: black,
|
||||
});
|
||||
|
||||
drawText({
|
||||
buf,
|
||||
width,
|
||||
x: Math.floor((width - bottomWidth) / 2),
|
||||
y: pad + 7 * scale + gap,
|
||||
text: bottom,
|
||||
scale,
|
||||
color: black,
|
||||
});
|
||||
|
||||
const png = encodePngRgba(buf, width, height);
|
||||
return png.toString("base64");
|
||||
}
|
||||
Reference in New Issue
Block a user