630 lines
29 KiB
Nim
Raw Normal View History

2026-07-08 09:02:47 +02:00
## 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 ==="