From b6e10287b23241fb8933f311a72ebe95976b174b Mon Sep 17 00:00:00 2001 From: bptato Date: Sat, 24 Aug 2024 19:48:55 +0200 Subject: sixel: break out into a separate cgi script This caches sixel output. Works best when the line height is a multiple of 6px, but should still be faster than the previous solution everywhere else too (simply by virtue of encoding separate images in parallel). Next step: actual color quantization --- adapter/img/sixel.nim | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 adapter/img/sixel.nim (limited to 'adapter/img/sixel.nim') diff --git a/adapter/img/sixel.nim b/adapter/img/sixel.nim new file mode 100644 index 00000000..02bb356e --- /dev/null +++ b/adapter/img/sixel.nim @@ -0,0 +1,202 @@ +# Sixel codec. I'm lazy, so no decoder yet. +# +# "Regular" mode just encodes the image as a sixel image, with +# Cha-Image-Sixel-Palette colors. (TODO: maybe adjust this based on quality?) +# The encoder also has a "half-dump" mode, where the output is modified as +# follows: +# +# * DCS q set-raster-attributes is omitted. +# * 32-bit binary number in header indicates length of following palette. +# * A lookup table is appended to the file end, which includes (height + 5) / 6 +# 32-bit binary numbers indicating the start index of every 6th row. +# +# This way, the image can be vertically cropped in ~constant time. + +import std/options +import std/os +import std/strutils + +import types/color +import utils/sandbox +import utils/twtstr + +const DCSSTART = "\eP" +const ST = "\e\\" + +# data is binary 0..63; the output is the final ASCII form. +proc compressSixel(data: openArray[uint8]; c: uint8): string = + var outs = newStringOfCap(data.len div 4 + 3) + outs &= '#' + outs &= $c + var n = 0 + var c = char(0) + for u in data: + let cc = char(u + 0x3F) + if c != cc: + if n > 3: + outs &= '!' & $n & c + else: # for char(0) n is also 0, so it is ignored. + outs &= c.repeat(n) + c = cc + n = 0 + inc n + if n > 3: + outs &= '!' & $n & c + else: + outs &= c.repeat(n) + return outs + +type SixelBand = object + c: uint8 + data: seq[uint8] + +func find(bands: seq[SixelBand]; c: uint8): int = + for i, band in bands: + if band.c == c: + return i + -1 + +proc setU32BE(s: var string; n: uint32) = + s[0] = char(n and 0xFF) + s[1] = char((n shr 8) and 0xFF) + s[2] = char((n shr 16) and 0xFF) + s[3] = char((n shr 24) and 0xFF) + +proc putU32BE(s: var string; n: uint32) = + s &= char(n and 0xFF) + s &= char((n shr 8) and 0xFF) + s &= char((n shr 16) and 0xFF) + s &= char((n shr 24) and 0xFF) + +proc encode(s: string; width, height, offx, offy, cropw: int; halfdump: bool; + bgcolor: ARGBColor; palette: int) = + if width == 0 or height == 0: + return # done... + # prelude + var outs = "" + if halfdump: # reserve size for prelude + outs &= "\0\0\0\0" + else: + outs &= DCSSTART & 'q' + # set raster attributes + outs &= "\"1;1;" & $width & ';' & $height + for b in 16 ..< 256: + # laziest possible color register allocation scheme + #TODO obviously this produces sub-optimal results + let rgb = EightBitColor(b).toRGB() + let rgbq = RGBColor(uint32(rgb).fastmul(100)) + let n = b - 15 + # 2 is RGB + outs &= '#' & $n & ";2;" & $rgbq.r & ';' & $rgbq.g & ';' & $rgbq.b + if halfdump: + # prepend prelude size + let L = outs.len - 4 # subtract length field + outs.setU32BE(uint32(L)) + stdout.write(outs) + let W = width * 4 + let H = W * height + var n = offy * W + var ymap = "" + var totalLen = 0 + while n < H: + if halfdump: + ymap.putU32BE(uint32(totalLen)) + var bands = newSeq[SixelBand]() + for i in 0 ..< 6: + if n >= H: + break + let mask = 1u8 shl i + let realw = cropw - offx + for j in 0 ..< realw: + let m = n + (j + offx) * 4 + let r = uint8(s[m]) + let g = uint8(s[m + 1]) + let b = uint8(s[m + 2]) + let a = uint8(s[m + 3]) + var c0 = RGBAColorBE(r: r, g: g, b: b, a: a) + if c0.a != 255: + let c1 = bgcolor.blend(c0) + c0 = RGBAColorBE(r: c1.r, g: c1.g, b: c1.b, a: c1.a) + let c = uint8(c0.toEightBit()) + if (let k = bands.find(c); k != -1): + bands[k].data[j] = bands[k].data[j] or mask + else: + bands.add(SixelBand(c: c, data: newSeq[uint8](realw))) + bands[^1].data[^1] = mask + n += W + outs.setLen(0) + for i in 0 ..< bands.high: + outs &= bands[i].data.compressSixel(bands[i].c - 15) & '$' + outs &= bands[^1].data.compressSixel(bands[^1].c - 15) + if n >= H: + outs &= ST + else: + outs &= '-' + totalLen += outs.len + stdout.write(outs) + if halfdump: + ymap.putU32BE(uint32(totalLen)) + stdout.write(ymap) + +proc parseDimensions(s: string): (int, int) = + let s = s.split('x') + if s.len != 2: + stdout.writeLine("Cha-Control: ConnectionError 1 wrong dimensions") + return + let w = parseUInt32(s[0], allowSign = false) + let h = parseUInt32(s[1], allowSign = false) + if w.isNone or w.isNone: + stdout.writeLine("Cha-Control: ConnectionError 1 wrong dimensions") + return + return (int(w.get), int(h.get)) + +proc main() = + enterNetworkSandbox() + let scheme = getEnv("MAPPED_URI_SCHEME") + let f = scheme.after('+') + if f != "x-sixel": + stdout.writeLine("Cha-Control: ConnectionError 1 unknown format " & f) + return + case getEnv("MAPPED_URI_PATH") + of "decode": + stdout.writeLine("Cha-Control: ConnectionError 1 not implemented") + of "encode": + let headers = getEnv("REQUEST_HEADERS") + var width = 0 + var height = 0 + var offx = 0 + var offy = 0 + var halfdump = false + var palette = -1 + var bgcolor = rgb(0, 0, 0) + var cropw = -1 + for hdr in headers.split('\n'): + let s = hdr.after(':').strip() + case hdr.until(':') + of "Cha-Image-Dimensions": + (width, height) = parseDimensions(s) + of "Cha-Image-Offset": + (offx, offy) = parseDimensions(s) + of "Cha-Image-Crop-Width": + let q = parseUInt32(s, allowSign = false) + if q.isNone: + stdout.writeLine("Cha-Control: ConnectionError 1 wrong palette") + return + cropw = int(q.get) + of "Cha-Image-Sixel-Halfdump": + halfdump = true + of "Cha-Image-Sixel-Palette": + let q = parseUInt16(s, allowSign = false) + if q.isNone: + stdout.writeLine("Cha-Control: ConnectionError 1 wrong palette") + return + palette = int(q.get) + of "Cha-Image-Background-Color": + bgcolor = parseLegacyColor0(s) + if cropw == -1: + cropw = width + let s = stdin.readAll() + stdout.write("Cha-Image-Dimensions: " & $width & 'x' & $height & "\n\n") + s.encode(width, height, offx, offy, cropw, halfdump, bgcolor, palette) + +main() -- cgit 1.4.1-2-gfad0