blecsd-media
@blecsd/media Package Skill
The @blecsd/media package provides image parsing, rendering, and video playback for blECSd terminal applications. It includes complete GIF and PNG parsers, ANSI rendering to 256-color terminal, animated image widgets, video player integration (mpv/mplayer), and W3M overlay support.
Install: pnpm add @blecsd/media
Peer dependency: blecsd >= 0.7.0
Subpath Imports
The package exposes focused subpath imports:
import { parseGIF, frameToRGBA } from '@blecsd/media/gif';
import { parsePNG, extractPixels } from '@blecsd/media/png';
import { renderToAnsi, scaleBitmap, rgbTo256Color } from '@blecsd/media/render';
import { createImage, setImageData, play, pause } from '@blecsd/media/widgets/image';
import { createVideo, detectVideoPlayer } from '@blecsd/media/widgets/video';
import { createW3MOverlay } from '@blecsd/media/overlay';
Or use the namespace API from the main entry:
import { gif, png, ansiRender, imageWidget, videoWidget, w3m } from '@blecsd/media';
GIF Parser
Complete GIF parser with LZW decompression and animation support.
import { parseGIF, frameToRGBA, validateGIFSignature } from '@blecsd/media/gif';
// Read file
const buffer = await fs.readFile('animation.gif');
// Validate
if (!validateGIFSignature(buffer)) throw new Error('Not a GIF');
// Parse
const result = parseGIF(buffer);
// result: { header, frames[], globalColorTable }
// Get RGBA pixels for a frame
const rgba = frameToRGBA(result.frames[0]);
// rgba: Uint8Array of [r, g, b, a, r, g, b, a, ...]
Key functions:
parseGIF(buffer)- Parse full GIF with animation framesparseGIFHeader(buffer)- Parse header onlyvalidateGIFSignature(buffer)- Check GIF87a/GIF89a signatureframeToRGBA(frame)- Convert frame to RGBA pixel datadeinterlace(pixels, width, height)- Deinterlace interlaced framesparseColorTable(buffer, offset, size)- Parse color tablereadSubBlocks(buffer, offset)- Read GIF sub-blocksdecompressLZW(data, minCodeSize)- LZW decompressioncreateBitReader(data)/readCode(reader, codeSize)- Bit-level reading
PNG Parser
Complete PNG parser with filter reconstruction.
import { parsePNG, extractPixels, parseChunks } from '@blecsd/media/png';
const buffer = await fs.readFile('image.png');
const result = parsePNG(buffer);
// result: { header (IHDR), pixels, chunks[] }
// Or parse step by step
const chunks = parseChunks(buffer);
const header = parseIHDR(chunks[0].data);
const pixels = extractPixels(chunks);
Key functions:
parsePNG(buffer)- Parse full PNGparseChunks(buffer)- Parse PNG chunksparseIHDR(data)- Parse image header (width, height, bitDepth, colorType)reconstructFilters(scanlines, width, height, bitDepth)- Reconstruct PNG filterspaethPredictor(a, b, c)- PNG Paeth predictorextractPixels(chunks)- Extract raw pixel dataparsePLTE(buffer)- Parse palette chunk
ANSI Rendering
Convert bitmap data to terminal-renderable ANSI output.
import { renderToAnsi, scaleBitmap, cellMapToString, rgbTo256Color } from '@blecsd/media/render';
// Render bitmap to ANSI cells
const cellMap = renderToAnsi(bitmap, {
width: 40, // Target width in terminal columns
height: 20, // Target height in terminal rows
mode: 'halfblock' // 'halfblock' | 'braille' | 'ascii'
});
// Convert to string for output
const output = cellMapToString(cellMap);
// Scale a bitmap
const scaled = scaleBitmap(bitmap, 40, 20);
// Color conversion
const color256 = rgbTo256Color(255, 128, 0); // RGB to 256-color palette
const lum = rgbLuminance(255, 128, 0); // Compute luminance
const char = luminanceToChar(lum); // Map to ASCII char
// Blend colors
const blended = blendWithBackground([255, 128, 0], [0, 0, 0]);
RenderMode options:
'halfblock'- Uses half-block characters for 2 pixels per cell vertically'braille'- Uses braille characters for 2x4 pixels per cell (highest density)'ascii'- Uses ASCII characters mapped by luminance
Image Widget
High-level widget for displaying static and animated images.
import { createImage, setImageData, play, pause, stop } from '@blecsd/media/widgets/image';
// Create image widget
const eid = createImage(world, {
position: { x: 0, y: 0 },
dimensions: { width: 40, height: 20 },
renderMode: 'halfblock',
});
// Set image data (from parsed GIF/PNG)
setImageData(eid, {
frames: gifResult.frames.map(f => frameToRGBA(f)),
width: gifResult.header.width,
height: gifResult.header.height,
frameCount: gifResult.frames.length,
delays: gifResult.frames.map(f => f.delay),
});
// Animation control (for animated GIFs)
play(eid);
pause(eid);
stop(eid);
// Set specific frame
setFrame(eid, 3);
// Get current state
const bitmap = getImageBitmap(eid);
const cellMap = getImageCellMap(eid);
// Aspect ratio helper
const { width, height } = calculateAspectRatioDimensions(
sourceWidth, sourceHeight,
targetWidth, targetHeight
);
// Cleanup
clearImageCache(eid);
clearAllImageCaches();
// Type guard
if (isImage(world, eid)) { /* ... */ }
Video Widget
Video playback using external players (mpv or mplayer).
import { createVideo, detectVideoPlayer, getVideoPlaybackState } from '@blecsd/media/widgets/video';
// Auto-detect available video player
const player = detectVideoPlayer(); // 'mpv' | 'mplayer' | undefined
// Create video widget
const eid = createVideo(world, {
position: { x: 0, y: 0 },
dimensions: { width: 80, height: 24 },
source: '/path/to/video.mp4',
player: player, // 'mpv' | 'mplayer'
autoplay: true,
});
// Check state
const state = getVideoPlaybackState(eid); // 'stopped' | 'playing' | 'paused'
// Get detected player
const detectedPlayer = getVideoPlayer(eid);
// Build command args (for manual control)
const args = buildMpvArgs({ source: 'video.mp4', width: 80, height: 24 });
const args2 = buildMplayerArgs({ source: 'video.mp4' });
const args3 = buildPlayerArgs({ source: 'video.mp4', player: 'mpv' });
// Send commands to running player
sendPauseCommand(handle);
sendSeekCommand(handle, 10); // Seek 10 seconds
// Type guard
if (isVideo(world, eid)) { /* ... */ }
W3M Overlay
Use W3M's terminal graphics protocol for high-quality image display.
import { createW3MOverlay } from '@blecsd/media/overlay';
const eid = createW3MOverlay(world, {
position: { x: 0, y: 0 },
dimensions: { width: 40, height: 20 },
imagePath: '/path/to/image.png',
});
Common Patterns
Display a GIF in Terminal
import { parseGIF, frameToRGBA } from '@blecsd/media/gif';
import { renderToAnsi, cellMapToString, scaleBitmap } from '@blecsd/media/render';
const buffer = await fs.readFile('cat.gif');
const gif = parseGIF(buffer);
for (const frame of gif.frames) {
const rgba = frameToRGBA(frame);
const bitmap = { data: rgba, width: gif.header.width, height: gif.header.height };
const scaled = scaleBitmap(bitmap, 40, 20);
const cellMap = renderToAnsi(scaled, { mode: 'halfblock' });
console.log(cellMapToString(cellMap));
}
Display a PNG
import { parsePNG } from '@blecsd/media/png';
import { renderToAnsi, cellMapToString } from '@blecsd/media/render';
const buffer = await fs.readFile('photo.png');
const png = parsePNG(buffer);
const cellMap = renderToAnsi(
{ data: png.pixels, width: png.header.width, height: png.header.height },
{ width: 60, height: 30, mode: 'braille' }
);
console.log(cellMapToString(cellMap));
Best Practices
- Choose the right render mode.
braillegives highest density (2x4 pixels per cell),halfblockis the default and works well for most images,asciiis most compatible. - Scale before rendering. Use
scaleBitmapto fit the image to your terminal dimensions before converting to ANSI. - Handle animated GIFs with the image widget. Don't manually loop frames; the widget handles animation timing.
- Video requires external players.
detectVideoPlayer()checks for mpv or mplayer. If neither is found, video won't work. - W3M overlay requires w3mimgdisplay. This is a separate binary from w3m.
- Clean up image caches. Call
clearImageCache(eid)orclearAllImageCaches()to free memory. - Use subpath imports for tree-shaking. Import from
@blecsd/media/gifinstead of@blecsd/mediato only pull in what you need.