// Live image probe helpers build tiny image fixtures for live provider tests. import { encodePngRgba, fillPixel } from "../../src/media/png-encode.js"; const GLYPH_ROWS_5X7: Record = { "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 renderBitmapTextPngBase64( text: string, options: { background?: { r: number; g: number; b: number; a?: number }; foreground?: { r: number; g: number; b: number; a?: number }; padding?: number; scale?: number; } = {}, ): string { const normalized = text.trim().toUpperCase(); if (!normalized) { throw new Error("bitmap text image requires non-empty text"); } const unsupported = Array.from(normalized).filter((ch) => !(ch in GLYPH_ROWS_5X7)); if (unsupported.length > 0) { throw new Error(`bitmap text image contains unsupported glyphs: ${unsupported.join(",")}`); } const scale = Math.max(1, Math.floor(options.scale ?? 4)); const padding = Math.max(0, Math.floor(options.padding ?? 8)); const width = measureTextWidthPx(normalized, scale) + padding * 2; const height = 7 * scale + padding * 2; const background = options.background ?? { r: 245, g: 247, b: 250, a: 255 }; const foreground = options.foreground ?? { r: 18, g: 24, b: 33, a: 255 }; const buf = Buffer.alloc(width * height * 4); fillRect({ buf, width, height, x: 0, y: 0, w: width, h: height, color: background, }); drawText({ buf, width, x: padding, y: padding, text: normalized, scale, color: foreground, }); return encodePngRgba(buf, width, height).toString("base64"); } function fillRect(params: { buf: Buffer; width: number; height: number; x: number; y: number; w: number; h: number; color: { r: number; g: number; b: number; a?: number }; }) { const startX = Math.max(0, params.x); const startY = Math.max(0, params.y); const endX = Math.min(params.width, params.x + params.w); const endY = Math.min(params.height, params.y + params.h); for (let y = startY; y < endY; y += 1) { for (let x = startX; x < endX; x += 1) { fillPixel( params.buf, x, y, params.width, params.color.r, params.color.g, params.color.b, params.color.a ?? 255, ); } } } function fillEllipse(params: { buf: Buffer; width: number; height: number; cx: number; cy: number; rx: number; ry: number; color: { r: number; g: number; b: number; a?: number }; }) { for ( let y = Math.max(0, params.cy - params.ry); y <= Math.min(params.height - 1, params.cy + params.ry); y += 1 ) { for ( let x = Math.max(0, params.cx - params.rx); x <= Math.min(params.width - 1, params.cx + params.rx); x += 1 ) { const dx = (x - params.cx) / params.rx; const dy = (y - params.cy) / params.ry; if (dx * dx + dy * dy <= 1) { fillPixel( params.buf, x, y, params.width, params.color.r, params.color.g, params.color.b, params.color.a ?? 255, ); } } } } function fillTriangle(params: { buf: Buffer; width: number; height: number; a: { x: number; y: number }; b: { x: number; y: number }; c: { x: number; y: number }; color: { r: number; g: number; b: number; a?: number }; }) { const minX = Math.max(0, Math.min(params.a.x, params.b.x, params.c.x)); const maxX = Math.min(params.width - 1, Math.max(params.a.x, params.b.x, params.c.x)); const minY = Math.max(0, Math.min(params.a.y, params.b.y, params.c.y)); const maxY = Math.min(params.height - 1, Math.max(params.a.y, params.b.y, params.c.y)); const area = (params.b.x - params.a.x) * (params.c.y - params.a.y) - (params.b.y - params.a.y) * (params.c.x - params.a.x); if (area === 0) { return; } for (let y = minY; y <= maxY; y += 1) { for (let x = minX; x <= maxX; x += 1) { const w0 = (params.b.x - params.a.x) * (y - params.a.y) - (params.b.y - params.a.y) * (x - params.a.x); const w1 = (params.c.x - params.b.x) * (y - params.b.y) - (params.c.y - params.b.y) * (x - params.b.x); const w2 = (params.a.x - params.c.x) * (y - params.c.y) - (params.a.y - params.c.y) * (x - params.c.x); if ((w0 <= 0 && w1 <= 0 && w2 <= 0) || (w0 >= 0 && w1 >= 0 && w2 >= 0)) { fillPixel( params.buf, x, y, params.width, params.color.r, params.color.g, params.color.b, params.color.a ?? 255, ); } } } } function drawBlockCatLabel(params: { buf: Buffer; width: number; height: number; x: number; y: number; color: { r: number; g: number; b: number; a?: number }; }) { const t = 12; const h = 78; const w = 58; const gap = 20; const cX = params.x; const aX = cX + w + gap; const tX = aX + w + gap; fillRect({ ...params, x: cX, y: params.y, w, h: t, color: params.color }); fillRect({ ...params, x: cX, y: params.y, w: t, h, color: params.color }); fillRect({ ...params, x: cX, y: params.y + h - t, w, h: t, color: params.color }); fillRect({ ...params, x: aX, y: params.y, w, h: t, color: params.color }); fillRect({ ...params, x: aX, y: params.y, w: t, h, color: params.color }); fillRect({ ...params, x: aX + w - t, y: params.y, w: t, h, color: params.color }); fillRect({ ...params, x: aX, y: params.y + Math.floor((h - t) / 2), w, h: t, color: params.color, }); fillRect({ ...params, x: tX, y: params.y, w, h: t, color: params.color }); fillRect({ ...params, x: tX + Math.floor((w - t) / 2), y: params.y, w: t, h, color: params.color, }); } 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"); } export function renderSolidColorPngBase64(color: { r: number; g: number; b: number }): string { const width = 192; const height = 192; const buf = Buffer.alloc(width * height * 4, 255); fillRect({ buf, width, height, x: 0, y: 0, w: width, h: height, color, }); const png = encodePngRgba(buf, width, height); return png.toString("base64"); } export function renderCatFacePngBase64(): string { const width = 256; const height = 288; const buf = Buffer.alloc(width * height * 4, 255); const outline = { r: 40, g: 40, b: 40 }; const innerEar = { r: 245, g: 182, b: 193 }; const nose = { r: 222, g: 102, b: 138 }; const whisker = { r: 30, g: 30, b: 30 }; fillTriangle({ buf, width, height, a: { x: 62, y: 74 }, b: { x: 106, y: 12 }, c: { x: 134, y: 88 }, color: outline, }); fillTriangle({ buf, width, height, a: { x: 194, y: 74 }, b: { x: 150, y: 12 }, c: { x: 122, y: 88 }, color: outline, }); fillTriangle({ buf, width, height, a: { x: 80, y: 70 }, b: { x: 106, y: 34 }, c: { x: 122, y: 80 }, color: innerEar, }); fillTriangle({ buf, width, height, a: { x: 176, y: 70 }, b: { x: 150, y: 34 }, c: { x: 134, y: 80 }, color: innerEar, }); fillEllipse({ buf, width, height, cx: 128, cy: 112, rx: 78, ry: 66, color: outline, }); fillEllipse({ buf, width, height, cx: 98, cy: 100, rx: 9, ry: 12, color: { r: 255, g: 255, b: 255 }, }); fillEllipse({ buf, width, height, cx: 158, cy: 100, rx: 9, ry: 12, color: { r: 255, g: 255, b: 255 }, }); fillEllipse({ buf, width, height, cx: 128, cy: 130, rx: 22, ry: 17, color: { r: 255, g: 255, b: 255 }, }); fillTriangle({ buf, width, height, a: { x: 128, y: 122 }, b: { x: 118, y: 136 }, c: { x: 138, y: 136 }, color: nose, }); fillRect({ buf, width, height, x: 127, y: 136, w: 2, h: 15, color: whisker }); fillRect({ buf, width, height, x: 74, y: 134, w: 42, h: 2, color: whisker }); fillRect({ buf, width, height, x: 140, y: 134, w: 42, h: 2, color: whisker }); fillRect({ buf, width, height, x: 80, y: 146, w: 34, h: 2, color: whisker }); fillRect({ buf, width, height, x: 142, y: 146, w: 34, h: 2, color: whisker }); drawBlockCatLabel({ buf, width, height, x: 21, y: 190, color: outline, }); const png = encodePngRgba(buf, width, height); return png.toString("base64"); }