refactor: replace timg with jimp + ink-picture renderers for image display
Browse filesRemove external `timg` binary dependency in ImageShowTool by using `jimp`
for image decoding and adapted ink-picture renderers (halfBlock/braille/ascii)
for ANSI text-protocol output. The renderers are pure functions that work
with codev's Ink fork via RawAnsi, avoiding context-incompatibility with
the ink-picture React components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- src/tools/ImageShowTool/ImageShowTool.tsx +83 -20
- src/utils/imageRenderers.ts +191 -0
src/tools/ImageShowTool/ImageShowTool.tsx
CHANGED
|
@@ -2,12 +2,16 @@ import { readFile } from 'fs/promises'
|
|
| 2 |
import { homedir } from 'os'
|
| 3 |
import React from 'react'
|
| 4 |
import { z } from 'zod/v4'
|
|
|
|
| 5 |
import { RawAnsi, Text } from '../../ink.js'
|
| 6 |
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 7 |
import { logForDebugging } from '../../utils/debug.js'
|
| 8 |
import {
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
const IMAGE_TOOL_NAME = 'ImageShow'
|
| 13 |
|
|
@@ -114,14 +118,73 @@ async function loadImage(url: string): Promise<{ buffer: Buffer; format: string
|
|
| 114 |
}
|
| 115 |
|
| 116 |
/**
|
| 117 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
* virtual DOM so Ink knows the image dimensions and the cursor stays
|
| 119 |
* in sync with the terminal.
|
| 120 |
*/
|
| 121 |
-
function
|
| 122 |
-
|
| 123 |
-
const cleaned = output.replace(/\x1b\[\?25[hl]/g, '')
|
| 124 |
-
const lines = cleaned.split('\n').filter(l => l.length > 0)
|
| 125 |
if (lines.length === 0) {
|
| 126 |
return null
|
| 127 |
}
|
|
@@ -139,7 +202,7 @@ export const ImageShowTool = buildTool({
|
|
| 139 |
name: IMAGE_TOOL_NAME,
|
| 140 |
description:
|
| 141 |
'Display an image (PNG/JPEG/GIF/WebP) directly in the terminal. ' +
|
| 142 |
-
'Renders with
|
| 143 |
'so the cursor stays in sync and the image scrolls with conversation content. ' +
|
| 144 |
'Supports both URLs (https://) and local file paths.',
|
| 145 |
|
|
@@ -164,7 +227,7 @@ export const ImageShowTool = buildTool({
|
|
| 164 |
},
|
| 165 |
|
| 166 |
async prompt(_options): Promise<string> {
|
| 167 |
-
return `ImageShow displays a PNG/JPEG/GIF/WebP image directly in the terminal using
|
| 168 |
},
|
| 169 |
|
| 170 |
async checkPermissions(): Promise<{ behavior: 'allow' }> {
|
|
@@ -219,7 +282,7 @@ export const ImageShowTool = buildTool({
|
|
| 219 |
content: {
|
| 220 |
success: boolean
|
| 221 |
message: string
|
| 222 |
-
|
| 223 |
},
|
| 224 |
_progressMessages,
|
| 225 |
_options,
|
|
@@ -228,8 +291,8 @@ export const ImageShowTool = buildTool({
|
|
| 228 |
return null
|
| 229 |
}
|
| 230 |
|
| 231 |
-
if (content.
|
| 232 |
-
return <
|
| 233 |
}
|
| 234 |
|
| 235 |
return <Text dimColor>{content.message}</Text>
|
|
@@ -241,7 +304,7 @@ export const ImageShowTool = buildTool({
|
|
| 241 |
message: string
|
| 242 |
imageData?: { base64: string; mediaType: string }
|
| 243 |
base64?: string
|
| 244 |
-
|
| 245 |
}
|
| 246 |
}> {
|
| 247 |
const url = input.url
|
|
@@ -273,12 +336,12 @@ export const ImageShowTool = buildTool({
|
|
| 273 |
|
| 274 |
logForDebugging(`ImageShow: loaded ${buffer.length} byte ${format} image`)
|
| 275 |
|
| 276 |
-
//
|
| 277 |
-
//
|
| 278 |
-
// the image dimensions and the cursor stays in sync.
|
| 279 |
-
const
|
| 280 |
-
if (
|
| 281 |
-
logForDebugging('ImageShow: generated
|
| 282 |
}
|
| 283 |
|
| 284 |
return {
|
|
@@ -290,7 +353,7 @@ export const ImageShowTool = buildTool({
|
|
| 290 |
mediaType,
|
| 291 |
},
|
| 292 |
base64: buffer.toString('base64'),
|
| 293 |
-
|
| 294 |
},
|
| 295 |
}
|
| 296 |
},
|
|
|
|
| 2 |
import { homedir } from 'os'
|
| 3 |
import React from 'react'
|
| 4 |
import { z } from 'zod/v4'
|
| 5 |
+
import Jimp from 'jimp'
|
| 6 |
import { RawAnsi, Text } from '../../ink.js'
|
| 7 |
import { buildTool, type ToolDef } from '../../Tool.js'
|
| 8 |
import { logForDebugging } from '../../utils/debug.js'
|
| 9 |
import {
|
| 10 |
+
getBestTextProtocol,
|
| 11 |
+
renderImage,
|
| 12 |
+
type PixelData,
|
| 13 |
+
type TextImageProtocol,
|
| 14 |
+
} from '../../utils/imageRenderers.js'
|
| 15 |
|
| 16 |
const IMAGE_TOOL_NAME = 'ImageShow'
|
| 17 |
|
|
|
|
| 118 |
}
|
| 119 |
|
| 120 |
/**
|
| 121 |
+
* Render an image buffer to an ANSI-escaped string via jimp + text-based
|
| 122 |
+
* protocol (halfBlock / braille / ascii).
|
| 123 |
+
*
|
| 124 |
+
* Pure-JS equivalent of the old renderImageWithTimgSync that used the
|
| 125 |
+
* external `timg` binary.
|
| 126 |
+
*
|
| 127 |
+
* @returns ANSI-escaped string, or null on failure.
|
| 128 |
+
*/
|
| 129 |
+
async function renderImageWithJimp(
|
| 130 |
+
buffer: Buffer,
|
| 131 |
+
_format: string,
|
| 132 |
+
): Promise<string | null> {
|
| 133 |
+
try {
|
| 134 |
+
const cols = process.stdout.columns ?? 80
|
| 135 |
+
const termRows = process.stdout.rows ?? 40
|
| 136 |
+
// Use at most half the terminal height so the image doesn't dominate
|
| 137 |
+
const maxRows = Math.max(10, Math.floor(termRows * 0.5))
|
| 138 |
+
// Aim for roughly 2:1 cell ratio (cells are ~2x tall)
|
| 139 |
+
const maxCols = Math.min(cols, Math.round(maxRows * 2))
|
| 140 |
+
|
| 141 |
+
// Decode image via jimp
|
| 142 |
+
const image = await Jimp.read(buffer)
|
| 143 |
+
const origWidth = image.bitmap.width
|
| 144 |
+
const origHeight = image.bitmap.height
|
| 145 |
+
|
| 146 |
+
// Scale to fit terminal
|
| 147 |
+
const scale = Math.min(maxCols / origWidth, maxRows / origHeight, 1)
|
| 148 |
+
const targetWidth = Math.round(origWidth * scale)
|
| 149 |
+
const targetHeight = Math.round(origHeight * scale)
|
| 150 |
+
|
| 151 |
+
image.resize(targetWidth, targetHeight)
|
| 152 |
+
|
| 153 |
+
// Extract pixel data
|
| 154 |
+
const pixelData: PixelData = {
|
| 155 |
+
data: image.bitmap.data,
|
| 156 |
+
info: {
|
| 157 |
+
width: targetWidth,
|
| 158 |
+
height: targetHeight,
|
| 159 |
+
channels: 4,
|
| 160 |
+
},
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
// Detect terminal capabilities and pick the best text protocol
|
| 164 |
+
const supportsColor = process.env.NO_COLOR === undefined
|
| 165 |
+
const supportsUnicode = true // modern terminals all support Unicode
|
| 166 |
+
|
| 167 |
+
const protocol: TextImageProtocol = getBestTextProtocol({
|
| 168 |
+
supportsUnicode,
|
| 169 |
+
supportsColor,
|
| 170 |
+
})
|
| 171 |
+
|
| 172 |
+
logForDebugging(`ImageShow: rendering ${targetWidth}x${targetHeight} via ${protocol}`)
|
| 173 |
+
|
| 174 |
+
return renderImage(pixelData, protocol, supportsColor)
|
| 175 |
+
} catch (err) {
|
| 176 |
+
logForDebugging(`ImageShow: jimp render error ${err}`)
|
| 177 |
+
return null
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/**
|
| 182 |
+
* React component that renders ink-picture output within Ink's
|
| 183 |
* virtual DOM so Ink knows the image dimensions and the cursor stays
|
| 184 |
* in sync with the terminal.
|
| 185 |
*/
|
| 186 |
+
function ImageDisplay({ output }: { output: string }): React.ReactNode {
|
| 187 |
+
const lines = output.split('\n').filter(l => l.length > 0)
|
|
|
|
|
|
|
| 188 |
if (lines.length === 0) {
|
| 189 |
return null
|
| 190 |
}
|
|
|
|
| 202 |
name: IMAGE_TOOL_NAME,
|
| 203 |
description:
|
| 204 |
'Display an image (PNG/JPEG/GIF/WebP) directly in the terminal. ' +
|
| 205 |
+
'Renders with Unicode-block characters within Ink\'s virtual DOM ' +
|
| 206 |
'so the cursor stays in sync and the image scrolls with conversation content. ' +
|
| 207 |
'Supports both URLs (https://) and local file paths.',
|
| 208 |
|
|
|
|
| 227 |
},
|
| 228 |
|
| 229 |
async prompt(_options): Promise<string> {
|
| 230 |
+
return `ImageShow displays a PNG/JPEG/GIF/WebP image directly in the terminal using Unicode-block rendering. Supports local file paths (e.g. /tmp/image.png) and HTTPS URLs (e.g. https://example.com/image.png). The image is rendered within Ink's virtual DOM so the cursor stays in sync.`
|
| 231 |
},
|
| 232 |
|
| 233 |
async checkPermissions(): Promise<{ behavior: 'allow' }> {
|
|
|
|
| 282 |
content: {
|
| 283 |
success: boolean
|
| 284 |
message: string
|
| 285 |
+
imageOutput?: string
|
| 286 |
},
|
| 287 |
_progressMessages,
|
| 288 |
_options,
|
|
|
|
| 291 |
return null
|
| 292 |
}
|
| 293 |
|
| 294 |
+
if (content.imageOutput) {
|
| 295 |
+
return <ImageDisplay output={content.imageOutput} />
|
| 296 |
}
|
| 297 |
|
| 298 |
return <Text dimColor>{content.message}</Text>
|
|
|
|
| 304 |
message: string
|
| 305 |
imageData?: { base64: string; mediaType: string }
|
| 306 |
base64?: string
|
| 307 |
+
imageOutput?: string
|
| 308 |
}
|
| 309 |
}> {
|
| 310 |
const url = input.url
|
|
|
|
| 336 |
|
| 337 |
logForDebugging(`ImageShow: loaded ${buffer.length} byte ${format} image`)
|
| 338 |
|
| 339 |
+
// Render via jimp + text protocol within Ink's virtual DOM.
|
| 340 |
+
// ImageDisplay in renderToolResultMessage renders the output so Ink
|
| 341 |
+
// knows the image dimensions and the cursor stays in sync.
|
| 342 |
+
const imageOutput = await renderImageWithJimp(buffer, format)
|
| 343 |
+
if (imageOutput) {
|
| 344 |
+
logForDebugging('ImageShow: generated text-protocol output')
|
| 345 |
}
|
| 346 |
|
| 347 |
return {
|
|
|
|
| 353 |
mediaType,
|
| 354 |
},
|
| 355 |
base64: buffer.toString('base64'),
|
| 356 |
+
imageOutput,
|
| 357 |
},
|
| 358 |
}
|
| 359 |
},
|
src/utils/imageRenderers.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Terminal image renderers adapted from ink-picture.
|
| 3 |
+
*
|
| 4 |
+
* Pure functions that convert pixel data into ANSI-escaped strings
|
| 5 |
+
* for display in Ink's RawAnsi component. No Ink/React dependency.
|
| 6 |
+
*
|
| 7 |
+
* Source: https://github.com/endernoke/ink-picture
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import chalk from 'chalk'
|
| 11 |
+
|
| 12 |
+
// --- Types ---
|
| 13 |
+
|
| 14 |
+
export interface PixelData {
|
| 15 |
+
data: Buffer
|
| 16 |
+
info: {
|
| 17 |
+
width: number
|
| 18 |
+
height: number
|
| 19 |
+
channels: number
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export type TextImageProtocol = 'halfBlock' | 'braille' | 'ascii'
|
| 24 |
+
|
| 25 |
+
// --- Protocol detection ---
|
| 26 |
+
|
| 27 |
+
export interface TerminalCaps {
|
| 28 |
+
supportsUnicode: boolean
|
| 29 |
+
supportsColor: boolean
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Select the best text-based image protocol given terminal capabilities.
|
| 34 |
+
* Prioritises halfBlock (best quality), then braille, then ascii (lowest).
|
| 35 |
+
*/
|
| 36 |
+
export function getBestTextProtocol(caps: TerminalCaps): TextImageProtocol {
|
| 37 |
+
if (caps.supportsUnicode && caps.supportsColor) return 'halfBlock'
|
| 38 |
+
if (caps.supportsUnicode) return 'braille'
|
| 39 |
+
return 'ascii'
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// --- ASCII renderer ---
|
| 43 |
+
|
| 44 |
+
const ASCII_CHARS =
|
| 45 |
+
"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
|
| 46 |
+
|
| 47 |
+
export function renderAscii(
|
| 48 |
+
pixels: PixelData,
|
| 49 |
+
colored: boolean = true,
|
| 50 |
+
): string {
|
| 51 |
+
const { data, info } = pixels
|
| 52 |
+
const { width, height, channels } = info
|
| 53 |
+
|
| 54 |
+
let result = ''
|
| 55 |
+
for (let y = 0; y < height; y++) {
|
| 56 |
+
for (let x = 0; x < width; x++) {
|
| 57 |
+
const pixelIndex = (y * width + x) * channels
|
| 58 |
+
|
| 59 |
+
const r = data[pixelIndex]!
|
| 60 |
+
const g = data[pixelIndex + 1]!
|
| 61 |
+
const b = data[pixelIndex + 2]!
|
| 62 |
+
const a = channels === 4 ? (data[pixelIndex + 3] ?? 255) : 255
|
| 63 |
+
|
| 64 |
+
const sum = r + g + b + a
|
| 65 |
+
const intensity = sum === 0 ? 0 : sum / (255 * 4)
|
| 66 |
+
const char =
|
| 67 |
+
ASCII_CHARS[
|
| 68 |
+
ASCII_CHARS.length -
|
| 69 |
+
1 -
|
| 70 |
+
Math.floor(intensity * (ASCII_CHARS.length - 1))
|
| 71 |
+
]!
|
| 72 |
+
|
| 73 |
+
result += colored ? chalk.rgb(r, g, b)(char) : char
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
result += '\n'
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
return result.slice(0, -1)
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// --- Half-block renderer ---
|
| 83 |
+
|
| 84 |
+
const HALF_BLOCK = '\u2584'
|
| 85 |
+
|
| 86 |
+
export function renderHalfBlock(pixels: PixelData): string {
|
| 87 |
+
const { data, info } = pixels
|
| 88 |
+
const { width, height, channels } = info
|
| 89 |
+
|
| 90 |
+
let result = ''
|
| 91 |
+
for (let y = 0; y < height - 1; y += 2) {
|
| 92 |
+
for (let x = 0; x < width; x++) {
|
| 93 |
+
const topPixelIndex = (y * width + x) * channels
|
| 94 |
+
const bottomPixelIndex = ((y + 1) * width + x) * channels
|
| 95 |
+
|
| 96 |
+
const r = data[topPixelIndex]!
|
| 97 |
+
const g = data[topPixelIndex + 1]!
|
| 98 |
+
const b = data[topPixelIndex + 2]!
|
| 99 |
+
const a = channels === 4 ? (data[topPixelIndex + 3] ?? 255) : 255
|
| 100 |
+
|
| 101 |
+
const r2 = data[bottomPixelIndex]!
|
| 102 |
+
const g2 = data[bottomPixelIndex + 1]!
|
| 103 |
+
const b2 = data[bottomPixelIndex + 2]!
|
| 104 |
+
|
| 105 |
+
result +=
|
| 106 |
+
a === 0
|
| 107 |
+
? chalk.reset(' ')
|
| 108 |
+
: chalk.bgRgb(r, g, b).rgb(r2, g2, b2)(HALF_BLOCK)
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
result += '\n'
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
return result.slice(0, -1)
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// --- Braille renderer ---
|
| 118 |
+
|
| 119 |
+
export function renderBraille(pixels: PixelData): string {
|
| 120 |
+
const { data, info } = pixels
|
| 121 |
+
const { width, height, channels } = info
|
| 122 |
+
|
| 123 |
+
let result = ''
|
| 124 |
+
for (let y = 0; y < height - 3; y += 4) {
|
| 125 |
+
for (let x = 0; x < width - 1; x += 2) {
|
| 126 |
+
const dot1Index = (y * width + x) * channels
|
| 127 |
+
const dot2Index = ((y + 1) * width + x) * channels
|
| 128 |
+
const dot3Index = ((y + 2) * width + x) * channels
|
| 129 |
+
const dot4Index = (y * width + x + 1) * channels
|
| 130 |
+
const dot5Index = ((y + 1) * width + x + 1) * channels
|
| 131 |
+
const dot6Index = ((y + 2) * width + x + 1) * channels
|
| 132 |
+
const dot7Index = ((y + 3) * width + x) * channels
|
| 133 |
+
const dot8Index = ((y + 3) * width + x + 1) * channels
|
| 134 |
+
|
| 135 |
+
const getLuminance = (index: number) => {
|
| 136 |
+
const r = data[index]!
|
| 137 |
+
const g = data[index + 1]!
|
| 138 |
+
const b = data[index + 2]!
|
| 139 |
+
const a = channels === 4 ? (data[index + 3] ?? 255) : 255
|
| 140 |
+
const alpha = a / 255
|
| 141 |
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
| 142 |
+
const alphaAdjustedLuminance =
|
| 143 |
+
luminance * alpha + 255 * (1 - alpha)
|
| 144 |
+
return alphaAdjustedLuminance > 128 ? 1 : 0
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
const dot1 = getLuminance(dot1Index)
|
| 148 |
+
const dot2 = getLuminance(dot2Index)
|
| 149 |
+
const dot3 = getLuminance(dot3Index)
|
| 150 |
+
const dot4 = getLuminance(dot4Index)
|
| 151 |
+
const dot5 = getLuminance(dot5Index)
|
| 152 |
+
const dot6 = getLuminance(dot6Index)
|
| 153 |
+
const dot7 = getLuminance(dot7Index)
|
| 154 |
+
const dot8 = getLuminance(dot8Index)
|
| 155 |
+
|
| 156 |
+
const brailleChar = String.fromCharCode(
|
| 157 |
+
0x2800 +
|
| 158 |
+
(dot8 << 7) +
|
| 159 |
+
(dot7 << 6) +
|
| 160 |
+
(dot6 << 5) +
|
| 161 |
+
(dot5 << 4) +
|
| 162 |
+
(dot4 << 3) +
|
| 163 |
+
(dot3 << 2) +
|
| 164 |
+
(dot2 << 1) +
|
| 165 |
+
dot1,
|
| 166 |
+
)
|
| 167 |
+
result += brailleChar
|
| 168 |
+
}
|
| 169 |
+
result += '\n'
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
return result.slice(0, -1)
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/**
|
| 176 |
+
* Render pixel data as a string using the specified text protocol.
|
| 177 |
+
*/
|
| 178 |
+
export function renderImage(
|
| 179 |
+
pixels: PixelData,
|
| 180 |
+
protocol: TextImageProtocol,
|
| 181 |
+
colored: boolean = true,
|
| 182 |
+
): string {
|
| 183 |
+
switch (protocol) {
|
| 184 |
+
case 'halfBlock':
|
| 185 |
+
return renderHalfBlock(pixels)
|
| 186 |
+
case 'braille':
|
| 187 |
+
return renderBraille(pixels)
|
| 188 |
+
case 'ascii':
|
| 189 |
+
return renderAscii(pixels, colored)
|
| 190 |
+
}
|
| 191 |
+
}
|