- Auto-convert on image load (0ms) and flag change (400ms debounce) - SizeControl: linked width/height sliders with aspect-ratio lock and 0.5 font correction factor - Full flag exposure for all 4 tools (chafa, jp2a, ascii-image-converter, img2txt) - ChafaOptions: symbols/fill dropdowns, dither controls, work/threshold/font-ratio sliders, format select, toggles - Jp2aOptions: color-depth, RGB weight sliders, edge controls, 8 toggles - ImgTxtOptions: dither select with valid libcaca values, gamma slider - OutputDisplay: ansi-to-html rendering for colored chafa output - ShellBridge: abort-previous pattern, conversion-start/end lifecycle events - Test API (ENABLE_TEST_API=true): /test/health, /test/convert, /test/flags/:tool, /test/imagemagick - buildArgs: space-separated args (not = format); full schemas in SCHEMAS export - runChafa: width/height destructured and combined into --size WxH - Port changed to 3050; Vite on 0.0.0.0 with allowedHosts for production domain - 98 unit tests passing across 12 test files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.7 KiB
JavaScript
103 lines
3.7 KiB
JavaScript
import { execa } from 'execa'
|
|
|
|
const TIMEOUT = 30_000
|
|
|
|
export const SCHEMAS = {
|
|
chafa: {
|
|
size: { type: 'value' },
|
|
colors: { type: 'value', valid: ['none', '2', '8', '16/8', '16', '240', '256', 'full'] },
|
|
'color-extractor': { type: 'value', valid: ['average', 'median', 'mode'] },
|
|
'color-space': { type: 'value', valid: ['rgb', 'din99d'] },
|
|
symbols: { type: 'value' },
|
|
fill: { type: 'value' },
|
|
dither: { type: 'value', valid: ['none', 'ordered', 'diffusion', 'noise'] },
|
|
'dither-grain': { type: 'value', valid: ['1x1', '2x2', '4x4', '8x8'] },
|
|
'dither-intensity':{ type: 'value' },
|
|
threshold: { type: 'value' },
|
|
'font-ratio': { type: 'value' },
|
|
work: { type: 'value' },
|
|
zoom: { type: 'bool' },
|
|
stretch: { type: 'bool' },
|
|
'fg-only': { type: 'bool' },
|
|
format: { type: 'value', valid: ['symbols', 'utf8'] },
|
|
},
|
|
jp2a: {
|
|
width: { type: 'value' },
|
|
height: { type: 'value' },
|
|
chars: { type: 'value' },
|
|
background: { type: 'value', valid: ['light', 'dark'] },
|
|
colors: { type: 'bool' },
|
|
'color-depth': { type: 'value', valid: ['4', '8', '24'] },
|
|
fill: { type: 'bool' },
|
|
grayscale: { type: 'bool' },
|
|
invert: { type: 'bool' },
|
|
border: { type: 'bool' },
|
|
flipx: { type: 'bool' },
|
|
flipy: { type: 'bool' },
|
|
red: { type: 'value' },
|
|
green: { type: 'value' },
|
|
blue: { type: 'value' },
|
|
'edge-threshold': { type: 'value' },
|
|
'edges-only': { type: 'bool' },
|
|
},
|
|
'ascii-image-converter': {
|
|
width: { type: 'value' },
|
|
height: { type: 'value' },
|
|
color: { type: 'bool' },
|
|
braille: { type: 'bool' },
|
|
threshold: { type: 'value' },
|
|
},
|
|
img2txt: {
|
|
width: { type: 'value' },
|
|
height: { type: 'value' },
|
|
format: { type: 'value', valid: ['ansi', 'utf8', 'html'] },
|
|
dither: { type: 'value', valid: ['none', 'ordered2', 'ordered4', 'ordered8', 'random', 'fstein'] },
|
|
gamma: { type: 'value' },
|
|
},
|
|
}
|
|
|
|
export function buildArgs(tool, flags) {
|
|
const schema = SCHEMAS[tool]
|
|
if (!schema) throw new Error(`Unknown tool: ${tool}`)
|
|
|
|
const args = []
|
|
for (const [key, value] of Object.entries(flags)) {
|
|
const def = schema[key]
|
|
if (!def) throw new Error(`Unknown flag for ${tool}: --${key}`)
|
|
if (def.valid && !def.valid.includes(String(value))) {
|
|
throw new Error(`Invalid value for --${key}: ${value}. Expected one of: ${def.valid.join(', ')}`)
|
|
}
|
|
if (def.type === 'bool') {
|
|
if (value) args.push(`--${key}`)
|
|
} else {
|
|
args.push(`--${key}`, String(value))
|
|
}
|
|
}
|
|
return args
|
|
}
|
|
|
|
export async function runChafa(imagePath, flags) {
|
|
const { width, height, ...rest } = flags
|
|
const processed = { ...rest }
|
|
if (width && height) processed.size = `${width}x${height}`
|
|
else if (width) processed.size = `${width}x`
|
|
else if (height) processed.size = `x${height}`
|
|
const args = buildArgs('chafa', processed)
|
|
return execa('chafa', [...args, imagePath], { timeout: TIMEOUT })
|
|
}
|
|
|
|
export async function runJp2a(imagePath, flags) {
|
|
const args = buildArgs('jp2a', flags)
|
|
return execa('jp2a', [...args, imagePath], { timeout: TIMEOUT })
|
|
}
|
|
|
|
export async function runAsciiImageConverter(imagePath, flags) {
|
|
const args = buildArgs('ascii-image-converter', flags)
|
|
return execa('ascii-image-converter', [...args, imagePath], { timeout: TIMEOUT })
|
|
}
|
|
|
|
export async function runImgToTxt(imagePath, flags) {
|
|
const args = buildArgs('img2txt', flags)
|
|
return execa('img2txt', [...args, imagePath], { timeout: TIMEOUT })
|
|
}
|