|
## Half-block Unicode renderer - universal fallback.
|
|
##
|
|
## Uses ▀ (UPPER HALF BLOCK) with 24-bit foreground/background
|
|
## colors. Two vertical samples per character cell. Works in any
|
|
## truecolor terminal. Supports bilinear scaling and optional
|
|
## Floyd-Steinberg dithering for downscaled images.
|
|
|
|
import std/[strutils, math]
|
|
import types, scaling
|
|
|
|
const
|
|
Csi = "\e["
|
|
ResetColor = Csi & "0m"
|
|
UpperHalfBlock = "▀"
|
|
|
|
type
|
|
Color = tuple[r, g, b: uint8]
|
|
|
|
proc foreground(color: Color): string =
|
|
Csi & "38;2;" & $color.r & ";" & $color.g & ";" & $color.b & "m"
|
|
|
|
proc background(color: Color): string =
|
|
Csi & "48;2;" & $color.r & ";" & $color.g & ";" & $color.b & "m"
|
|
|
|
proc compositeOver(img: ImageData, bg: tuple[r, g, b: uint8]): seq[uint8] =
|
|
let total = img.width * img.height
|
|
result = newSeq[uint8](total * 3)
|
|
for i in 0 ..< total:
|
|
let off = i * 4
|
|
let a = float(img.data[off + 3]) / 255.0
|
|
let inva = 1.0 - a
|
|
result[i * 3 + 0] = uint8(float(img.data[off + 0]) * a + float(bg.r) * inva)
|
|
result[i * 3 + 1] = uint8(float(img.data[off + 1]) * a + float(bg.g) * inva)
|
|
result[i * 3 + 2] = uint8(float(img.data[off + 2]) * a + float(bg.b) * inva)
|
|
|
|
proc floydSteinbergHalfBlock(
|
|
buf: var seq[uint8], w, h: int,
|
|
) =
|
|
## In-place Floyd-Steinberg dithering of the resized RGB buffer.
|
|
## Distributes quantization error to smooth banding in downscaled images.
|
|
for y in 0 ..< h:
|
|
for x in 0 ..< w:
|
|
let off = (y * w + x) * 3
|
|
for c in 0 ..< 3:
|
|
var old = float(buf[off + c])
|
|
var newVal = round(old)
|
|
if newVal < 0.0: newVal = 0.0
|
|
if newVal > 255.0: newVal = 255.0
|
|
let err = old - newVal
|
|
buf[off + c] = uint8(newVal)
|
|
|
|
template d(xx, yy, wgt: untyped) =
|
|
if xx >= 0 and xx < w and yy >= 0 and yy < h:
|
|
let toff = (yy * w + xx) * 3
|
|
buf[toff + c] = uint8(clamp(
|
|
int(round(float(buf[toff + c]) + err * wgt)), 0, 255))
|
|
|
|
d(x + 1, y, 7.0 / 16.0)
|
|
d(x - 1, y + 1, 3.0 / 16.0)
|
|
d(x, y + 1, 5.0 / 16.0)
|
|
d(x + 1, y + 1, 1.0 / 16.0)
|
|
|
|
proc renderHalfBlock*(
|
|
img: ImageData,
|
|
background: tuple[r, g, b: uint8],
|
|
columns, sampleHeight: int,
|
|
dither: bool = false,
|
|
): string =
|
|
## Produce half-block Unicode output.
|
|
## Returns empty string if source or target dimensions are invalid.
|
|
if img.width <= 0 or img.height <= 0 or columns <= 0 or sampleHeight <= 0:
|
|
return ""
|
|
if img.data.len < img.width * img.height * 4:
|
|
return ""
|
|
|
|
let rgb = compositeOver(img, background)
|
|
let rgbScaled = bilinearResizeRgb(rgb, img.width, img.height, columns, sampleHeight)
|
|
|
|
var buf = rgbScaled
|
|
if dither:
|
|
floydSteinbergHalfBlock(buf, columns, sampleHeight)
|
|
|
|
var lines: seq[string]
|
|
for top in countup(0, sampleHeight - 2, 2):
|
|
var parts: seq[string]
|
|
var prevTop: Color = (0'u8, 0, 0)
|
|
var prevBot: Color = (0'u8, 0, 0)
|
|
var first = true
|
|
|
|
for col in 0 ..< columns:
|
|
let topOff = (top * columns + col) * 3
|
|
let botOff = ((top + 1) * columns + col) * 3
|
|
let upper: Color = (
|
|
buf[topOff + 0],
|
|
buf[topOff + 1],
|
|
buf[topOff + 2],
|
|
)
|
|
let lower: Color = (
|
|
buf[botOff + 0],
|
|
buf[botOff + 1],
|
|
buf[botOff + 2],
|
|
)
|
|
|
|
if first or upper != prevTop:
|
|
parts.add(foreground(upper))
|
|
prevTop = upper
|
|
if first or lower != prevBot:
|
|
parts.add(background(lower))
|
|
prevBot = lower
|
|
parts.add(UpperHalfBlock)
|
|
first = false
|
|
|
|
parts.add(ResetColor)
|
|
lines.add(join(parts))
|
|
|
|
result = join(lines, "\n")
|