This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## termimg test — verifies protocol detection, geometry, scaling,
## halfblock, quarterblock, dithering, and aspect-ratio preservation.
##
## Run: nim c -r --path:src tests/test_termimg.nim
import std/[strutils, math]
import termimg
when isMainModule:
echo "=== termimg Advanced Test Suite ==="
# ── Capabilities ──────────────────────────────────────────────
let caps = detectCapabilities()
echo "Terminal: ", caps.columns, "x", caps.rows, " cells"
echo "Cell: ", caps.cell.width, "x", caps.cell.height, " px"
echo "Protocols:"
for proto in [ptKitty, ptIterm2, ptSixel, ptQuarterBlock, ptHalfBlock]:
echo " ", proto, if caps.supports(proto): "" else: ""
# ── Build a test pattern ─────────────────────────────────────
# 32x16 RGBA: gradient bands + sharp edges to test scaling quality
const
pw = 32
ph = 16
var testPixels = newSeq[uint8](pw * ph * 4)
for y in 0 ..< ph:
for x in 0 ..< pw:
let off = (y * pw + x) * 4
let r = uint8(255 * x div (pw - 1))
let g = uint8(0)
let b = uint8(255 * (pw - 1 - x) div (pw - 1))
let brightness = uint8(128 + 127 * y div (ph - 1))
testPixels[off + 0] = uint8((int(r) * int(brightness)) div 255)
testPixels[off + 1] = uint8((int(g) * int(brightness)) div 255)
testPixels[off + 2] = uint8((int(b) * int(brightness)) div 255)
testPixels[off + 3] = 255'u8
# White cross for edge-testing
let cx = pw div 2
let cy = ph div 2
for i in 0 ..< pw:
let offH = (cy * pw + i) * 4
testPixels[offH + 0] = 255; testPixels[offH + 1] = 255
testPixels[offH + 2] = 255; testPixels[offH + 3] = 255
for i in 0 ..< ph:
let offV = (i * pw + cx) * 4
testPixels[offV + 0] = 255; testPixels[offV + 1] = 255
testPixels[offV + 2] = 255; testPixels[offV + 3] = 255
# ── Test 1: Default halfblock ────────────────────────────────
echo "\n── Test 1: Half-block (default, contain fit) ──"
block:
let opts = defaultOptions()
let geo = computeGeometry(pw, ph, caps, opts)
echo " geometry: ", geo.columns, " cols x ", geo.sampleHeight,
" samples (", geo.pixelWidth, "x", geo.pixelHeight, " px)"
let outStr = renderHalfBlock(
ImageData(width: pw, height: ph, data: testPixels),
opts.backgroundRgb, geo.columns, geo.sampleHeight)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
let lines = outStr.split("\n")
echo " output: ", lines.len, " lines, ",
(if lines.len > 0: lines[0].len else: 0), " chars first line"
# ── Test 2: Quarter-block thumbnail ──────────────────────────
echo "\n── Test 2: Quarter-block thumbnail ──"
block:
var opts = thumbnailOptions()
let geo = computeGeometry(pw, ph, caps, opts)
echo " geometry: ", geo.columns, " cols x ", geo.sampleHeight, " samples"
let outStr = renderQuarterBlock(
ImageData(width: pw, height: ph, data: testPixels),
opts.backgroundRgb, geo.columns, geo.sampleHeight, dither = true)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
let lines = outStr.split("\n")
echo " output: ", lines.len, " lines, ",
(if lines.len > 0: lines[0].len else: 0), " chars first line"
# ── Test 3: Full-density (bdFull) ────────────────────────────
echo "\n── Test 3: Full-density (bdFull, 10 cols) ──"
block:
var opts = defaultOptions()
opts.density = bdFull
opts.maxWidth = 10
let geo = computeGeometry(pw, ph, caps, opts)
echo " geometry: ", geo.columns, " cols x ", geo.sampleHeight, " samples"
let outStr = renderHalfBlock(
ImageData(width: pw, height: ph, data: testPixels),
opts.backgroundRgb, geo.columns, geo.sampleHeight)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
let lines = outStr.split("\n")
echo " output: ", lines.len, " lines"
# ── Test 4: Half-block with dithering ────────────────────────
echo "\n── Test 4: Half-block with dithering ──"
block:
var opts = defaultOptions()
opts.dither = true
opts.maxWidth = 20
let geo = computeGeometry(pw, ph, caps, opts)
echo " geometry: ", geo.columns, " cols x ", geo.sampleHeight, " samples"
let outStr = renderHalfBlock(
ImageData(width: pw, height: ph, data: testPixels),
opts.backgroundRgb, geo.columns, geo.sampleHeight, dither = true)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
let lines = outStr.split("\n")
echo " output: ", lines.len, " lines"
# ── Test 5: Aspect ratio preservation ────────────────────────
echo "\n── Test 5: Aspect ratio ──"
block:
let geo = computeGeometry(640, 200, caps, defaultOptions())
let ratio = float(geo.pixelWidth) / float(geo.pixelHeight)
echo " 640x200 → ", geo.pixelWidth, "x", geo.pixelHeight,
" px (ratio ", ratio.formatFloat(ffDecimal, 2), ", expected ~3.2)"
let geo2 = computeGeometry(200, 640, caps, defaultOptions())
let ratio2 = float(geo2.pixelWidth) / float(geo2.pixelHeight)
echo " 200x640 → ", geo2.pixelWidth, "x", geo2.pixelHeight,
" px (ratio ", ratio2.formatFloat(ffDecimal, 2), ", expected ~0.31)"
let geo3 = computeGeometry(500, 500, caps, defaultOptions())
let ratio3 = float(geo3.pixelWidth) / float(geo3.pixelHeight)
echo " 500x500 → ", geo3.pixelWidth, "x", geo3.pixelHeight,
" px (ratio ", ratio3.formatFloat(ffDecimal, 2), ", expected ~1.0)"
# ── Test 6: Fit modes ────────────────────────────────────────
echo "\n── Test 6: Fit modes ──"
block:
for fit in [fmContain, fmStretch, fmWidth, fmOriginal, fmCellExact]:
var opts = defaultOptions()
opts.fit = fit
opts.maxWidth = 30
let geo = computeGeometry(pw, ph, caps, opts)
echo " ", fit, ": ", geo.pixelWidth, "x", geo.pixelHeight,
" px, ", geo.columns, " cols, ", geo.sampleHeight, " samples"
# ── Test 7: Width-constrained render ─────────────────────────
echo "\n── Test 7: Width-constrained ──"
block:
var opts = defaultOptions()
opts.maxWidth = 8
opts.fit = fmWidth
let geo = computeGeometry(pw, ph, caps, opts)
echo " 32x16 forced to 8 cols → ", geo.columns, " cols x ",
geo.sampleHeight, " samples"
let outStr = renderHalfBlock(
ImageData(width: pw, height: ph, data: testPixels),
opts.backgroundRgb, geo.columns, geo.sampleHeight)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
let lines = outStr.split("\n")
echo " output: ", lines.len, " lines"
# ── Test 8: Bilinear scaling ────────────────────────────────
echo "\n── Test 8: Bilinear scaling ──"
block:
let img = ImageData(width: pw, height: ph, data: testPixels)
let scaled = bilinearResizeRgba(img, 64, 32)
let expected = 64 * 32 * 4
echo " ", pw, "x", ph, " → 64x32: ", scaled.len, " bytes (expected ", expected, ")"
doAssert scaled.len == expected
# ── Test 9: Scaling down ─────────────────────────────────────
echo "\n── Test 9: Scaling down ──"
block:
let img = ImageData(width: pw, height: ph, data: testPixels)
let scaled = bilinearResizeRgba(img, 8, 4)
let expected = 8 * 4 * 4
echo " ", pw, "x", ph, " → 8x4: ", scaled.len, " bytes (expected ", expected, ")"
doAssert scaled.len == expected
# ── Test 10: Rgba renderer pipeline ──────────────────────────
echo "\n── Test 10: Full renderImageRgba pipeline ──"
block:
let opts = defaultOptions()
let outStr = renderImageRgba(testPixels, pw, ph, caps, opts)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
let lines = outStr.split("\n")
echo " output: ", lines.len, " lines"
# ── Test 11: thumbnailOptions preset ──────────────────────────
echo "\n── Test 11: thumbnailOptions pipeline ──"
block:
let opts = thumbnailOptions()
let outStr = renderImageRgba(testPixels, pw, ph, caps, opts)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
let lines = outStr.split("\n")
echo " output: ", lines.len, " lines"
# ── Test 12: Zero dimensions ──────────────────────────────────
echo "\n── Test 12: Zero dimensions (should not crash) ──"
block:
let out1 = renderImageRgba(testPixels, 0, ph, caps, defaultOptions())
doAssert out1.len == 0, "zero width should return empty"
let out2 = renderImageRgba(testPixels, pw, 0, caps, defaultOptions())
doAssert out2.len == 0, "zero height should return empty"
let out3 = renderImageRgba(newSeq[uint8](0), 0, 0, caps, defaultOptions())
doAssert out3.len == 0, "zero dims empty data should return empty"
echo " all zero-dim cases returned empty string (pass)"
# ── Test 13: Short RGBA buffer ────────────────────────────────
echo "\n── Test 13: Short RGBA buffer (should not crash) ──"
block:
let shortData = newSeq[uint8](4) # only one pixel claimed as 10x10
let outStr = renderImageRgba(shortData, 10, 10, caps, defaultOptions())
doAssert outStr.len == 0, "short buffer should return empty (invalid input)"
echo " short buffer returned empty (safe): ", outStr.len, " chars (pass)"
# ── Test 14: Negative dimensions ──────────────────────────────
echo "\n── Test 14: Negative dimensions (should not crash) ──"
block:
let out1 = renderImageRgba(testPixels, -1, ph, caps, defaultOptions())
doAssert out1.len == 0
let out2 = renderImageRgba(testPixels, pw, -5, caps, defaultOptions())
doAssert out2.len == 0
echo " all negative-dim cases returned empty string (pass)"
# ── Test 15: Non-terminal environment ─────────────────────────
echo "\n── Test 15: Non-terminal environment (no crash) ──"
block:
let fakeCaps = TerminalCapabilities(
columns: 0, rows: 0,
cell: CellSize(width: 10, height: 20),
protocols: {ptHalfBlock},
)
let geo = computeGeometry(pw, ph, fakeCaps, defaultOptions())
doAssert geo.columns >= 1, "geometry must have at least 1 column"
doAssert geo.sampleHeight >= 1, "geometry must have at least 1 sample row"
echo " fake caps (0x0): ", geo.columns, " cols x ", geo.sampleHeight,
" samples (pass)"
# ── Test 16: Empty raw bytes ──────────────────────────────────
echo "\n── Test 16: Empty raw bytes (should not crash) ──"
block:
let outStr = renderImageRaw(newSeq[uint8](0), 100, 100, caps, defaultOptions())
echo " renderImageRaw returned ", outStr.len, " chars (no crash, pass)"
# ═══════════════════════════════════════════════════════════════
# ADVANCED VISUAL TESTS — Gallery, columns, comparison grids
# ═══════════════════════════════════════════════════════════════
# ── Pattern generators ────────────────────────────────────────
proc checkerboard(w, h, csize: int): seq[uint8] =
result = newSeq[uint8](w * h * 4)
for y in 0 ..< h:
for x in 0 ..< w:
let off = (y * w + x) * 4
let bright = if ((x div csize) + (y div csize)) mod 2 == 0: 220 else: 40
result[off + 0] = uint8(bright)
result[off + 1] = uint8(bright)
result[off + 2] = uint8(bright)
result[off + 3] = 255
proc rainbowBars(w, h: int): seq[uint8] =
result = newSeq[uint8](w * h * 4)
for y in 0 ..< h:
for x in 0 ..< w:
let off = (y * w + x) * 4
let band = float(x) / float(w) * 6.0
let iband = int(band)
let frac = band - float(iband)
var r, g, b: float
case iband
of 0: (r, g, b) = (1.0, frac, 0.0)
of 1: (r, g, b) = (1.0 - frac, 1.0, 0.0)
of 2: (r, g, b) = (0.0, 1.0, frac)
of 3: (r, g, b) = (0.0, 1.0 - frac, 1.0)
of 4: (r, g, b) = (frac, 0.0, 1.0)
else: (r, g, b) = (1.0, 0.0, 1.0 - frac)
let ymul = 0.6 + 0.4 * float(y) / float(h)
result[off + 0] = uint8(clamp(int(r * 255.0 * ymul), 0, 255))
result[off + 1] = uint8(clamp(int(g * 255.0 * ymul), 0, 255))
result[off + 2] = uint8(clamp(int(b * 255.0 * ymul), 0, 255))
result[off + 3] = 255
proc targetCircle(w, h: int): seq[uint8] =
result = newSeq[uint8](w * h * 4)
let cx = float(w) / 2.0
let cy = float(h) / 2.0
let maxR = min(cx, cy)
for y in 0 ..< h:
for x in 0 ..< w:
let off = (y * w + x) * 4
let dx = float(x) - cx
let dy = float(y) - cy
let r = sqrt(dx * dx + dy * dy)
let ring = int(r / (maxR / 5.0)) mod 2
if ring == 0:
result[off + 0] = 200; result[off + 1] = 50; result[off + 2] = 50
else:
result[off + 0] = 255; result[off + 1] = 255; result[off + 2] = 255
result[off + 3] = 255
echo "\n═══════════════════════════════════════════════"
echo " ADVANCED VISUAL TESTS"
echo "═══════════════════════════════════════════════"
let patternW = 64
let patternH = 48
let chk = checkerboard(patternW, patternH, 8)
let rnb = rainbowBars(patternW, patternH)
let tgt = targetCircle(patternW, patternH)
# ── Test 17: Gallery — three patterns side-by-side ────────────
echo "\n── Test 17: Gallery (three patterns, same width) ──"
block:
let colW = (caps.columns - 4) div 3
var opts = defaultOptions()
opts.maxWidth = colW
opts.fit = fmWidth
let geo = computeGeometry(patternW, patternH, caps, opts)
# Side-by-side: render left, mid, right and concatenate each row
let outChk = renderHalfBlock(
ImageData(width: patternW, height: patternH, data: chk),
opts.backgroundRgb, geo.columns, geo.sampleHeight)
let outRnb = renderHalfBlock(
ImageData(width: patternW, height: patternH, data: rnb),
opts.backgroundRgb, geo.columns, geo.sampleHeight)
let outTgt = renderHalfBlock(
ImageData(width: patternW, height: patternH, data: tgt),
opts.backgroundRgb, geo.columns, geo.sampleHeight)
let linesChk = outChk.split('\n')
let linesRnb = outRnb.split('\n')
let linesTgt = outTgt.split('\n')
for i in 0 ..< min(min(linesChk.len, linesRnb.len), linesTgt.len):
stdout.write(linesChk[i])
stdout.write(" ")
stdout.write(linesRnb[i])
stdout.write(" ")
stdout.write(linesTgt[i])
stdout.write("\n")
stdout.flushFile()
echo " gallery: ", geo.columns, " cols each × ",
linesChk.len, " rows (3 across) (pass)"
# ── Test 18: Column layout — same pattern at 3 densities ──────
echo "\n── Test 18: Column layout (3 densities stacked) ──"
block:
let colW = caps.columns - 2
for density in [bdFull, bdHalf, bdQuarter]:
var opts = defaultOptions()
opts.density = density
opts.maxWidth = colW
opts.fit = fmWidth
let geo = computeGeometry(patternW, patternH, caps, opts)
let outStr = renderHalfBlock(
ImageData(width: patternW, height: patternH, data: rnb),
opts.backgroundRgb, geo.columns, geo.sampleHeight)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
echo " density=$1: $2 cols × $3 samples (pass)" % [
$density, $geo.columns, $geo.sampleHeight]
echo " column layout: 3 densities stacked (pass)"
# ── Test 19: Fit-mode comparison grid ─────────────────────────
echo "\n── Test 19: Fit-mode comparison grid ──"
block:
let halfW = caps.columns div 2 - 1
for fit in [fmContain, fmWidth, fmOriginal, fmStretch]:
var opts = defaultOptions()
opts.fit = fit
opts.maxWidth = halfW
opts.density = bdQuarter
let geo = computeGeometry(patternW, patternH, caps, opts)
let outStr = renderQuarterBlock(
ImageData(width: patternW, height: patternH, data: tgt),
opts.backgroundRgb, geo.columns, geo.sampleHeight,
dither = true)
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
echo " $1: $2 px ($3 cols × $4 samples) (pass)" % [
$fit, $geo.pixelWidth & "x" & $geo.pixelHeight,
$geo.columns, $geo.sampleHeight]
echo " fit-mode grid: 4 fits stacked (pass)"
# ── Test 20: Checkerboard at ultra-wide vs tall ───────────────
echo "\n── Test 20: Extreme aspect ratios (wide vs tall) ──"
block:
var optsW = defaultOptions()
optsW.maxWidth = caps.columns - 2
optsW.fit = fmWidth
let geoW = computeGeometry(patternW, patternH, caps, optsW)
let outW = renderHalfBlock(
ImageData(width: patternW, height: patternH, data: chk),
optsW.backgroundRgb, geoW.columns, geoW.sampleHeight)
stdout.write(outW)
stdout.write("\n\n")
stdout.flushFile()
# Tall: swap dimensions (64x48 becomes 48x64 via the pattern? no — use a tall pattern)
let tallPixels = checkerboard(patternH, patternW, 6) # 48x64
var optsT = defaultOptions()
optsT.maxHeight = caps.rows - 4
optsT.fit = fmContain
let geoT = computeGeometry(patternH, patternW, caps, optsT)
let outT = renderHalfBlock(
ImageData(width: patternH, height: patternW, data: tallPixels),
optsT.backgroundRgb, geoT.columns, geoT.sampleHeight)
stdout.write(outT)
stdout.write("\n")
stdout.flushFile()
echo " wide: ", geoW.columns, " cols × ", geoW.sampleHeight, " samples"
echo " tall: ", geoT.columns, " cols × ", geoT.sampleHeight, " samples (pass)"
# ── Test 21: Pipeline with all three patterns ─────────────────
echo "\n── Test 21: Pipeline gallery (renderImageRgba) ──"
block:
for (name, pix) in [("checkerboard", chk), ("rainbow", rnb), ("target", tgt)]:
let outStr = renderImageRgba(pix, patternW, patternH, caps, thumbnailOptions())
stdout.write(outStr)
stdout.write("\n")
stdout.flushFile()
echo " $1: $2 lines rendered (pass)" % [name, $(outStr.split('\n').len)]
# ═══════════════════════════════════════════════════════════════
# WORST-CASE / CORRUPTION TESTS
# ═══════════════════════════════════════════════════════════════
echo "\n═══════════════════════════════════════════════"
echo " WORST-CASE / CORRUPTION TESTS"
echo "═══════════════════════════════════════════════"
# ── Test 22: Off-by-one data ──────────────────────────────────
echo "\n── Test 22: Off-by-one data (should not crash) ──"
block:
# Exactly 1 byte short (data.len == w*h*4 - 1)
let shortBy1 = newSeq[uint8](pw * ph * 4 - 1)
let out1 = renderImageRgba(shortBy1, pw, ph, caps, defaultOptions())
doAssert out1.len == 0,
"1-byte-short buffer should return empty, got " & $out1.len & " chars"
# Exactly 1 byte extra (data.len == w*h*4 + 1)
let extra1 = newSeq[uint8](pw * ph * 4 + 1)
let out2 = renderImageRgba(extra1, pw, ph, caps, defaultOptions())
# Extra byte is harmless — we only read w*h*4 from front
doAssert out2.len > 0 or caps.protocols == {ptHalfBlock},
"1-byte-extra buffer should render normally (output len=" & $out2.len & ")"
echo " 1 byte short → empty (safe), 1 byte extra → renders (pass)"
# ── Test 23: Tiny images ──────────────────────────────────────
echo "\n── Test 23: Tiny images (1x1, 10x1, 1x10) ──"
block:
# 1x1
let px1 = @[128'u8, 64, 192, 255]
let out1 = renderImageRgba(px1, 1, 1, caps, defaultOptions())
doAssert out1.len > 0 or caps.protocols == {ptHalfBlock},
"1x1 pixel should produce output, got " & $out1.len & " chars"
# 10x1 horizontal strip
var strip = newSeq[uint8](10 * 1 * 4)
for x in 0 ..< 10:
strip[(x) * 4 + 0] = uint8(x * 25)
strip[(x) * 4 + 1] = uint8(255 - x * 25)
strip[(x) * 4 + 2] = 128
strip[(x) * 4 + 3] = 255
let out2 = renderImageRgba(strip, 10, 1, caps, defaultOptions())
doAssert out2.len > 0 or caps.protocols == {ptHalfBlock},
"10x1 strip should produce output, got " & $out2.len & " chars"
# 1x10 vertical strip
var vstrip = newSeq[uint8](1 * 10 * 4)
for y in 0 ..< 10:
let off = y * 4
vstrip[off + 0] = 64
vstrip[off + 1] = uint8(y * 25)
vstrip[off + 2] = 192
vstrip[off + 3] = 255
let out3 = renderImageRgba(vstrip, 1, 10, caps, defaultOptions())
doAssert out3.len > 0 or caps.protocols == {ptHalfBlock},
"1x10 strip should produce output, got " & $out3.len & " chars"
echo " 1x1, 10x1, 1x10 all rendered (pass)"
# ── Test 24: Fully transparent image ──────────────────────────
echo "\n── Test 24: Fully transparent image (alpha=0 everywhere) ──"
block:
var transparent = newSeq[uint8](16 * 16 * 4)
for y in 0 ..< 16:
for x in 0 ..< 16:
let off = (y * 16 + x) * 4
transparent[off + 0] = 255
transparent[off + 1] = 0
transparent[off + 2] = 0
transparent[off + 3] = 0 # fully transparent
let outStr = renderImageRgba(transparent, 16, 16, caps, defaultOptions())
# Transparency composited over white background should produce white-ish output
doAssert outStr.len > 0 or caps.protocols == {ptHalfBlock},
"transparent image should not crash, got " & $outStr.len & " chars"
echo " transparent 16x16 rendered (pass)"
# ── Test 25: Monochrome image ─────────────────────────────────
echo "\n── Test 25: Monochrome image (all same color) ──"
block:
var mono = newSeq[uint8](32 * 24 * 4)
for i in countup(0, mono.len - 1, 4):
mono[i + 0] = 100; mono[i + 1] = 150; mono[i + 2] = 200; mono[i + 3] = 255
let outStr = renderImageRgba(mono, 32, 24, caps, defaultOptions())
let lines = outStr.split('\n')
doAssert lines.len >= 1, "monochrome should produce lines, got " & $lines.len
echo " monochrome 32x24: ", lines.len, " lines (pass)"
# ── Test 26: Extreme opts values ──────────────────────────────
echo "\n── Test 26: Extreme opts (maxWidth=0, maxWidth=-1, maxHeight=0) ──"
block:
var opts0 = defaultOptions()
opts0.maxWidth = 0
let out1 = renderImageRgba(chk, patternW, patternH, caps, opts0)
doAssert out1.len >= 0, "maxWidth=0 should not crash"
var optsNeg = defaultOptions()
optsNeg.maxWidth = -1
let out2 = renderImageRgba(chk, patternW, patternH, caps, optsNeg)
doAssert out2.len >= 0, "maxWidth=-1 should not crash"
var optsH0 = defaultOptions()
optsH0.maxHeight = 0
let out3 = renderImageRgba(chk, patternW, patternH, caps, optsH0)
doAssert out3.len >= 0, "maxHeight=0 should not crash"
echo " all extreme opts returned safely (pass)"
# ── Test 27: Empty protocol set ───────────────────────────────
echo "\n── Test 27: Empty protocol set (selectProtocol) ──"
block:
let emptyCaps = TerminalCapabilities(
columns: 80, rows: 24,
cell: CellSize(width: 10, height: 20),
protocols: {}, # no protocols at all
)
let selected = selectProtocol(emptyCaps, ptHalfBlock)
doAssert selected == ptHalfBlock,
"selectProtocol should fall back to ptHalfBlock, got " & $selected
let selected2 = selectProtocol(emptyCaps, ptAuto)
doAssert selected2 == ptHalfBlock,
"selectProtocol(ptAuto) with empty set should fall back to ptHalfBlock, got " & $selected2
echo " empty protocols fall back to half-block (pass)"
# ── Test 28: 1-pixel-high / 1-pixel-wide aspect extremes ─────
echo "\n── Test 28: Extreme aspect (200x1 and 1x200) ──"
block:
var thinW = newSeq[uint8](200 * 1 * 4)
for x in 0 ..< 200:
thinW[x * 4 + 0] = uint8(x)
thinW[x * 4 + 1] = uint8(255 - x)
thinW[x * 4 + 2] = 128
thinW[x * 4 + 3] = 255
let out1 = renderImageRgba(thinW, 200, 1, caps, defaultOptions())
doAssert out1.len >= 0, "200x1 image should not crash"
var thinH = newSeq[uint8](1 * 200 * 4)
for y in 0 ..< 200:
thinH[y * 4 + 0] = 64
thinH[y * 4 + 1] = uint8(y)
thinH[y * 4 + 2] = 192
thinH[y * 4 + 3] = 255
let out2 = renderImageRgba(thinH, 1, 200, caps, defaultOptions())
doAssert out2.len >= 0, "1x200 image should not crash"
echo " 200x1 and 1x200 rendered without crash (pass)"
# ── Test 29: Zero background and high-dither stress ───────────
echo "\n── Test 29: Zero background + dither on black ──"
block:
var opts = defaultOptions()
opts.backgroundRgb = (0'u8, 0'u8, 0'u8) # pure black bg
opts.dither = true
opts.density = bdQuarter
opts.fit = fmContain
let outStr = renderImageRgba(tgt, patternW, patternH, caps, opts)
doAssert outStr.len >= 0, "zero bg + dither should not crash"
echo " zero background + dither on target circles (pass)"
# ── Test 30: Random-stress renders (20 iterations) ────────────
echo "\n── Test 30: Random-stress (20 renders at random sizes / fit modes) ──"
block:
let sizes = [(1,1), (2,2), (3,5), (7,3), (10,10), (16,16),
(32,48), (64,64), (100,50), (50,100)]
let fits = [fmContain, fmStretch, fmWidth, fmOriginal, fmCellExact]
let densities = [bdFull, bdHalf, bdQuarter]
var rng = 42 # deterministic pseudo-random
proc nextRand(maxVal: int): int =
rng = (rng * 1103515245 + 12345) and 0x7fffffff
result = rng mod maxVal
for iter in 0 ..< 20:
let (sw, sh) = sizes[nextRand(sizes.len)]
var pixels = newSeq[uint8](sw * sh * 4)
for i in countup(0, pixels.len - 1, 4):
pixels[i + 0] = uint8(nextRand(256))
pixels[i + 1] = uint8(nextRand(256))
pixels[i + 2] = uint8(nextRand(256))
pixels[i + 3] = 255
var opts = defaultOptions()
opts.fit = fits[nextRand(fits.len)]
opts.density = densities[nextRand(densities.len)]
opts.maxWidth = 10 + nextRand(40)
opts.dither = nextRand(2) == 0
let outStr = renderImageRgba(pixels, sw, sh, caps, opts)
if outStr.len > 0:
discard
echo " 20 random renders completed without crash (pass)"
# ── Test 31: Tiny data + huge claimed dimensions ──────────────
echo "\n── Test 31: Tiny buffer claimed as huge image ──"
block:
let tiny = @[255'u8, 0, 0, 255] # 1 pixel, claim as 1000x1000
let out1 = renderImageRgba(tiny, 1000, 1000, caps, defaultOptions())
doAssert out1.len == 0,
"tiny buffer claimed as huge should return empty, got " & $out1.len & " chars"
let out2 = renderImageRgba(tiny, 999999, 999999, caps, defaultOptions())
doAssert out2.len == 0,
"tiny buffer claimed as absurdly huge should return empty, got " & $out2.len & " chars"
echo " tiny data + huge/silly dimensions → empty (pass)"
echo "\n=== All 31 tests passed ==="