/
aqa
/
claude-code
Обзор
Документация
Войти
/
aqa
/
claude-code
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
utils/ansiToPng.ts
334 строки
210 KB
kuberwastaken
new initial commit (history rewritten)
01 апр 2026, 14:57
01 апр 2026, 14:57
c1996f2
Код
Авторство
О чём код?
/** * Render ANSI-escaped terminal text directly to a PNG image. * * Replaces the previous ansiToSvg → @resvg/resvg-wasm pipeline. The SVG was * just a lossy intermediate format for what is fundamentally a grid of * (char, fg-color, bold) cells on a flat background. This module skips SVG * entirely: it blits a bundled 24×48 bitmap font directly into an RGBA * Uint8Array, then encodes the result as a PNG using node:zlib. * * Why not resvg-wasm: 2.36MB of embedded WASM, a 2.1MB runtime font load * from a hardcoded system path (returning [] → blank screenshots when the * font isn't found), and ~224ms per render. This path is ~5–15ms, zero * external deps, identical output on mac/linux/windows. * * Font: Fira Code Regular rasterized at 24×48 with 8-bit anti-aliased alpha * (SIL OFL 1.1 — see scripts/LICENSE-FiraCode). Covers printable ASCII plus * the unicode chars used by /stats output. Regenerate with: * bun scripts/generate-bitmap-font.ts */ import { deflateSync } from 'zlib' import { stringWidth } from '../ink/stringWidth.js' import { type AnsiColor, DEFAULT_BG, type ParsedLine, parseAnsi, } from './ansiToSvg.js' // Glyph cell size — rasterized at output resolution so the default scale=1 // is crisp (no nearest-neighbor upscaling artifacts). const GLYPH_W = 24 const GLYPH_H = 48 const GLYPH_BYTES = GLYPH_W * GLYPH_H // Packed font rasterized from Fira Code Regular (SIL OFL 1.1). // Copyright (c) 2014-2021 The Fira Code Project Authors. // License: scripts/LICENSE-FiraCode // Format: [count:u16le][codepoint:u32le, alpha:GLYPH_W*GLYPH_H bytes]... const FONT_B64 = 'hQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA … [Строка слишком длинная. Вы можете скачать файл] // Dotted-box fallback for codepoints outside the bundled set. const FALLBACK_GLYPH = makeFallbackGlyph() function makeFallbackGlyph(): Uint8Array { const g = new Uint8Array(GLYPH_BYTES) for (let y = 2; y < GLYPH_H - 4; y++) { for (let x = 1; x < GLYPH_W - 1; x++) { const onBorder = y === 2 || y === GLYPH_H - 5 || x === 1 || x === GLYPH_W - 2 if (onBorder && (x + y) % 2 === 0) g[y * GLYPH_W + x] = 255 } } return g } const FONT: Map<number, Uint8Array> = decodeFont() function decodeFont(): Map<number, Uint8Array> { const buf = Buffer.from(FONT_B64, 'base64') const count = buf.readUInt16LE(0) const map = new Map<number, Uint8Array>() let off = 2 for (let i = 0; i < count; i++) { const cp = buf.readUInt32LE(off) off += 4 map.set(cp, buf.subarray(off, off + GLYPH_BYTES)) off += GLYPH_BYTES } return map } export type AnsiToPngOptions = { /** Integer zoom factor (nearest-neighbor). Default 1 — the font is already rasterized at output resolution. */ scale?: number /** Horizontal padding in 1× pixels. Default 48. */ paddingX?: number /** Vertical padding in 1× pixels. Default 48. */ paddingY?: number /** Corner radius in 1× pixels. Default 16. */ borderRadius?: number /** Background color. Default: dark gray (same as ansiToSvg). */ background?: AnsiColor } /** * Render ANSI-escaped text directly to a PNG buffer. * Returns a Buffer containing a valid PNG (RGBA, 8-bit). */ export function ansiToPng( ansiText: string, options: AnsiToPngOptions = {}, ): Buffer { const { scale = 1, paddingX = 48, paddingY = 48, borderRadius = 16, background = DEFAULT_BG, } = options const lines = parseAnsi(ansiText) // Trim trailing blank lines (same behavior as ansiToSvg). while ( lines.length > 0 && lines[lines.length - 1]!.every(span => span.text.trim() === '') ) { lines.pop() } if (lines.length === 0) { lines.push([{ text: '', color: background, bold: false }]) } const cols = Math.max(1, ...lines.map(lineWidthCells)) const rows = lines.length const width = (cols * GLYPH_W + paddingX * 2) * scale const height = (rows * GLYPH_H + paddingY * 2) * scale // RGBA buffer, pre-filled with the background color. const px = new Uint8Array(width * height * 4) fillBackground(px, background) if (borderRadius > 0) { roundCorners(px, width, height, borderRadius * scale) } // Blit glyphs. const padX = paddingX * scale const padY = paddingY * scale for (let row = 0; row < rows; row++) { let col = 0 for (const span of lines[row]!) { for (const ch of span.text) { const cp = ch.codePointAt(0)! const cellW = stringWidth(ch) if (cellW === 0) continue // zero-width (combining marks, etc.) const x = padX + col * GLYPH_W * scale const y = padY + row * GLYPH_H * scale const shade = SHADE_ALPHA[cp] if (shade !== undefined) { blitShade(px, width, x, y, span.color, background, shade, scale) } else { const glyph = FONT.get(cp) ?? FALLBACK_GLYPH blitGlyph(px, width, x, y, glyph, span.color, span.bold, scale) } col += cellW } } } return encodePng(px, width, height) } /** Terminal column width of a parsed line. */ function lineWidthCells(line: ParsedLine): number { let w = 0 for (const span of line) w += stringWidth(span.text) return w } function fillBackground(px: Uint8Array, bg: AnsiColor): void { for (let i = 0; i < px.length; i += 4) { px[i] = bg.r px[i + 1] = bg.g px[i + 2] = bg.b px[i + 3] = 255 } } // Modern terminals render shade chars (░▒▓█) as solid blocks with opacity, // not the classic VGA dither pattern. Alpha-blend toward background for the // same look. const SHADE_ALPHA: Record<number, number> = { 0x2591: 0.25, // ░ 0x2592: 0.5, // ▒ 0x2593: 0.75, // ▓ 0x2588: 1.0, // █ } function blitShade( px: Uint8Array, width: number, x: number, y: number, fg: AnsiColor, bg: AnsiColor, alpha: number, scale: number, ): void { const r = Math.round(fg.r * alpha + bg.r * (1 - alpha)) const g = Math.round(fg.g * alpha + bg.g * (1 - alpha)) const b = Math.round(fg.b * alpha + bg.b * (1 - alpha)) const cellW = GLYPH_W * scale const cellH = GLYPH_H * scale for (let dy = 0; dy < cellH; dy++) { const rowBase = ((y + dy) * width + x) * 4 for (let dx = 0; dx < cellW; dx++) { const i = rowBase + dx * 4 px[i] = r px[i + 1] = g px[i + 2] = b } } } /** * Blit one glyph into the RGBA buffer at (x,y), scaled by `scale` * (nearest-neighbor). Alpha-composites over the existing background. Bold is * synthesized by boosting alpha toward opaque — a cheap approximation that * reads as heavier weight without needing a second font. */ function blitGlyph( px: Uint8Array, width: number, x: number, y: number, glyph: Uint8Array, color: AnsiColor, bold: boolean, scale: number, ): void { for (let gy = 0; gy < GLYPH_H; gy++) { for (let gx = 0; gx < GLYPH_W; gx++) { let a = glyph[gy * GLYPH_W + gx]! if (a === 0) continue if (bold) a = Math.min(255, a * 1.4) const inv = 255 - a for (let sy = 0; sy < scale; sy++) { const rowBase = ((y + gy * scale + sy) * width + x + gx * scale) * 4 for (let sx = 0; sx < scale; sx++) { const i = rowBase + sx * 4 px[i] = (color.r * a + px[i]! * inv) >> 8 px[i + 1] = (color.g * a + px[i + 1]! * inv) >> 8 px[i + 2] = (color.b * a + px[i + 2]! * inv) >> 8 } } } } } /** * Zero out the alpha channel in the four corner regions outside a * quarter-circle of radius `r`. Produces rounded-rect corners. */ function roundCorners( px: Uint8Array, width: number, height: number, r: number, ): void { const r2 = r * r for (let dy = 0; dy < r; dy++) { for (let dx = 0; dx < r; dx++) { const ox = r - dx - 0.5 const oy = r - dy - 0.5 if (ox * ox + oy * oy <= r2) continue // Top-left, top-right, bottom-left, bottom-right. px[(dy * width + dx) * 4 + 3] = 0 px[(dy * width + (width - 1 - dx)) * 4 + 3] = 0 px[((height - 1 - dy) * width + dx) * 4 + 3] = 0 px[((height - 1 - dy) * width + (width - 1 - dx)) * 4 + 3] = 0 } } } // --- PNG encoding ----------------------------------------------------------- const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) const CRC_TABLE = makeCrcTable() function makeCrcTable(): Uint32Array { const t = new Uint32Array(256) for (let n = 0; n < 256; n++) { let c = n for (let k = 0; k < 8; k++) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 } t[n] = c >>> 0 } return t } function crc32(data: Uint8Array): number { let c = 0xffffffff for (let i = 0; i < data.length; i++) { c = CRC_TABLE[(c ^ data[i]!) & 0xff]! ^ (c >>> 8) } return (c ^ 0xffffffff) >>> 0 } function chunk(type: string, data: Uint8Array): Buffer { const body = Buffer.alloc(4 + data.length) body.write(type, 0, 'ascii') body.set(data, 4) const out = Buffer.alloc(12 + data.length) out.writeUInt32BE(data.length, 0) body.copy(out, 4) out.writeUInt32BE(crc32(body), 8 + data.length) return out } /** * Encode an RGBA pixel buffer as PNG. Minimal encoder: 8-bit depth, * color type 6 (RGBA), filter 0 (none) on every scanline, single IDAT. */ function encodePng(px: Uint8Array, width: number, height: number): Buffer { // IHDR 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: deflate ihdr[11] = 0 // filter method ihdr[12] = 0 // interlace: none // IDAT: each scanline prefixed with filter byte 0. const stride = width * 4 const raw = Buffer.alloc(height * (stride + 1)) for (let y = 0; y < height; y++) { const dst = y * (stride + 1) raw[dst] = 0 raw.set(px.subarray(y * stride, (y + 1) * stride), dst + 1) } const idat = deflateSync(raw) return Buffer.concat([ PNG_SIG, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', new Uint8Array(0)), ]) }