UNPKG

neo-neo-bblessed

Version:

A fork of neo-blessed (which is a fork of blessed) with bug fixes and maintenance.

204 lines (167 loc) 5.04 kB
/** * image-processor.ts - modern PNG/GIF reader replacement for tng.js * Copyright (c) 2025, Contributors (MIT License). * Replaces the legacy vendor/tng.js with modern npm dependencies */ import { PNG } from 'pngjs'; import * as fs from 'fs'; interface ImageOptions { colors?: any; width?: number; height?: number; scale?: number; ascii?: boolean; speed?: number; filename?: string; } interface CellMap { [row: number]: any[]; length: number; } interface ImageResult { cellmap: CellMap; frames?: any[] | undefined; play?(callback: (bmp: any, cellmap: CellMap) => void): any; pause?(): any; stop?(): any; renderElement?(cellmap: CellMap, element: any): void; } class ModernImageProcessor { private options: ImageOptions; private colors: any; private png: PNG | null = null; constructor(file: string | Buffer, options: ImageOptions = {}) { this.options = options; this.colors = options.colors || require('./colors'); this.loadImage(file); } private loadImage(file: string | Buffer): void { let buf: Buffer; if (Buffer.isBuffer(file)) { buf = file; } else { buf = fs.readFileSync(file); } // Detect format and load image if (buf.readUInt32BE(0) === 0x89504e47) { // PNG format this.png = PNG.sync.read(buf); } else if (buf.slice(0, 3).toString('ascii') === 'GIF') { throw new Error('GIF format not yet supported in modern implementation'); } else if (buf.readUInt16BE(0) === 0xffd8) { throw new Error('JPEG format not yet supported in modern implementation'); } else { throw new Error('Unsupported image format'); } } createCellmap(): CellMap { if (!this.png) { throw new Error('No image loaded'); } const bmp = this.pngToBitmap(); return this.bitmapToCellmap(bmp); } private pngToBitmap(): number[][][] { if (!this.png) { throw new Error('No PNG loaded'); } const { width, height, data } = this.png; const bmp: number[][][] = []; for (let y = 0; y < height; y++) { const row: number[][] = []; for (let x = 0; x < width; x++) { const idx = (width * y + x) << 2; const r = data[idx] ?? 0; const g = data[idx + 1] ?? 0; const b = data[idx + 2] ?? 0; const a = data[idx + 3] ?? 255; row.push([r, g, b, a]); } bmp.push(row); } return bmp; } private bitmapToCellmap(bmp: number[][][]): CellMap { const cellmap: any[] = []; const scale = this.options.scale || 0.2; const height = bmp.length; const width = bmp[0]?.length ?? 0; let cmwidth = this.options.width || 0; let cmheight = this.options.height || 0; let actualScale = scale; if (cmwidth) { actualScale = cmwidth / width; } else if (cmheight) { actualScale = cmheight / height; } if (!cmheight) { cmheight = Math.round(height * actualScale); } if (!cmwidth) { cmwidth = Math.round(width * actualScale); } const ys = height / cmheight; const xs = width / cmwidth; for (let y = 0; y < bmp.length; y += ys) { const line: any[] = []; const yy = Math.round(y); if (!bmp[yy]) break; for (let x = 0; x < bmp[yy].length; x += xs) { const xx = Math.round(x); if (!bmp[yy][xx]) break; // Convert RGBA to terminal cell representation const pixel = bmp[yy][xx]; const cell = this.pixelToCell(pixel); line.push(cell); } cellmap.push(line); } // Add length property for compatibility Object.defineProperty(cellmap, 'length', { value: cellmap.length, writable: true, enumerable: false, configurable: true, }); return cellmap as CellMap; } private pixelToCell(pixel: number[]): any { // Convert RGBA pixel to terminal cell representation // This is a simplified version - the original tng.js had complex color matching const [r, g, b, a] = pixel; if ((a ?? 255) < 128) { // Transparent pixel return { char: ' ', fg: 'default', bg: 'default' }; } // Use the colors module to find the best matching color if (this.colors?.match) { const color = this.colors.match(r ?? 0, g ?? 0, b ?? 0); return { char: '█', // Full block character fg: color, bg: 'default', }; } // Fallback: basic grayscale const gray = Math.round(((r ?? 0) + (g ?? 0) + (b ?? 0)) / 3); const char = gray > 128 ? '█' : gray > 64 ? '▓' : gray > 32 ? '▒' : '░'; return { char: char, fg: 'white', bg: 'default', }; } } // Synchronous wrapper to match tng.js API function processImage( file: string | Buffer, options: ImageOptions = {} ): ImageResult { const processor = new ModernImageProcessor(file, options); const result: ImageResult = { cellmap: processor.createCellmap(), // GIF support would go here }; return result; } export = processImage;