diff options
author | bptato <nincsnevem662@gmail.com> | 2024-10-04 16:44:29 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-10-04 16:58:42 +0200 |
commit | 62586dc23790732e66add5b27d4d37f1a56b41e0 (patch) | |
tree | ed33d758156658b93f81de6b71449a70ef69d32b | |
parent | faa97429d651c76d86ad0c2ab530d9f666fb6927 (diff) | |
download | chawan-62586dc23790732e66add5b27d4d37f1a56b41e0.tar.gz |
sixel, term: reduce half-dump special casing
Makes it slightly easier to debug image output. Also, we stop sending dimension headers, and no longer check for the scheme env var to make CLI invocation a bit less annoying.
-rw-r--r-- | adapter/img/sixel.nim | 108 | ||||
-rw-r--r-- | src/local/container.nim | 2 | ||||
-rw-r--r-- | src/local/pager.nim | 10 | ||||
-rw-r--r-- | src/local/term.nim | 47 |
4 files changed, 83 insertions, 84 deletions
diff --git a/adapter/img/sixel.nim b/adapter/img/sixel.nim index b124e235..15a7166e 100644 --- a/adapter/img/sixel.nim +++ b/adapter/img/sixel.nim @@ -4,25 +4,22 @@ # Cha-Image-Sixel-Palette colors. If that isn't given, it's set # according to Cha-Image-Quality. # -# The encoder also has a "half-dump" mode, where the output is modified as -# follows: +# The encoder also has a "half-dump" mode, where a binary lookup table +# is appended to the file end to allow vertical cropping in ~constant +# time. # -# * DCS q set-raster-attributes is omitted. -# * 32-bit binary number in header indicates the end of the following -# palette. (Note: this includes this 32-bit number's length as well.) -# * 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 table is an array of 32-bit big-endian integers indicating the +# start index of every sixel, and finally a 32-bit big-endian integer +# indicating the number of sixels in the image. # -# This way, the image can be vertically cropped in ~constant time. +# Warning: we intentionally leak the final octree. Be careful if you +# want to integrate this module into a larger program. Deallocation +# would (currently) look like: # -# Warning: we intentionally leak the final octree. Be careful if you want to -# integrate this module into a larger program. -# -# (FWIW, deallocation would (currently) look like: -# * free the leaves first, since they might have been inserted more than once -# (iterate over "nodes" seq) -# * recurse to free the parent nodes (start from root, dealloc each node where -# idx == -1)) +# * Free the leaves first, since they might have been inserted more +# than once (iterate over "nodes" seq) +# * Recurse to free the parent nodes (start from root, dealloc each +# node where idx == -1) import std/algorithm import std/options @@ -38,20 +35,13 @@ import utils/twtstr proc puts(os: PosixStream; s: string) = os.sendDataLoop(s) -proc die(s: string) {.noreturn.} = - let os = newPosixStream(STDOUT_FILENO) +proc die(os: PosixStream; s: string) {.noreturn.} = os.puts(s) quit(1) const DCS = "\eP" const ST = "\e\\" -proc setU32BE(s: var string; n: uint32; at: int) = - s[at] = char((n shr 24) and 0xFF) - s[at + 1] = char((n shr 16) and 0xFF) - s[at + 2] = char((n shr 8) and 0xFF) - s[at + 3] = char(n and 0xFF) - proc putU32BE(s: var string; n: uint32) = s &= char((n shr 24) and 0xFF) s &= char((n shr 16) and 0xFF) @@ -380,32 +370,31 @@ proc createBands(bands: var seq[SixelBand]; activeChunks: seq[ptr SixelChunk]) = if not found: bands.add(SixelBand(head: chunk, tail: chunk)) -proc encode(img: openArray[RGBAColorBE]; width, height, offx, offy, cropw: int; - halfdump: bool; palette: int) = +proc encode(os: PosixStream; img: openArray[RGBAColorBE]; + width, height, offx, offy, cropw, palette: int; halfdump: bool) = var palette = uint(palette) var transparent = false var root = img.quantize(palette, transparent) # prelude - var outs = "Cha-Image-Dimensions: " & $width & 'x' & $height & "\n" - if transparent: - outs &= "Cha-Image-Sixel-Transparent: 1\n" - outs &= '\n' + var outs = "Cha-Image-Sixel-Transparent: " & $int(transparent) & "\n" + outs &= "Cha-Image-Sixel-Prelude-Len: " + const PreludePad = "666 666 666" let preludeLenPos = outs.len - if halfdump: # reserve size for prelude - outs &= "\0\0\0\0" - else: - outs &= DCS - if transparent: - outs &= "0;1" - outs &= 'q' - # set raster attributes - outs &= "\"1;1;" & $width & ';' & $height + outs &= PreludePad & "\n\n" + let dcsPos = outs.len + outs &= DCS + if transparent: + outs &= "0;1" # P2=1 -> image has transparency + outs &= 'q' + # set raster attributes + outs &= "\"1;1;" & $width & ';' & $height let nodes = root.flatten(outs, palette) - if halfdump: - # prepend prelude size - let L = outs.len - preludeLenPos - outs.setU32BE(uint32(L), preludeLenPos) - let os = newPosixStream(STDOUT_FILENO) + # prepend prelude size + var ps = $(outs.len - dcsPos) + while ps.len < PreludePad.len: + ps &= ' ' + for i, c in ps: + outs[preludeLenPos + i] = c let L = width * height let realw = cropw - offx var n = offy * width @@ -495,24 +484,21 @@ proc encode(img: openArray[RGBAColorBE]; width, height, offx, offy, cropw: int; os.sendDataLoop(outs) # Note: we leave octree deallocation to the OS. See the header for details. -proc parseDimensions(s: string): (int, int) = +proc parseDimensions(os: PosixStream; s: string): (int, int) = let s = s.split('x') if s.len != 2: - die("Cha-Control: ConnectionError InternalError wrong dimensions\n") + os.die("Cha-Control: ConnectionError InternalError wrong dimensions") let w = parseUInt32(s[0], allowSign = false) let h = parseUInt32(s[1], allowSign = false) if w.isNone or w.isNone: - die("Cha-Control: ConnectionError InternalError wrong dimensions\n") + os.die("Cha-Control: ConnectionError InternalError wrong dimensions") return (int(w.get), int(h.get)) proc main() = - let scheme = getEnv("MAPPED_URI_SCHEME") - let f = scheme.after('+') - if f != "x-sixel": - die("Cha-Control: ConnectionError InternalError unknown format " & f) + let os = newPosixStream(STDOUT_FILENO) case getEnv("MAPPED_URI_PATH") of "decode": - die("Cha-Control: ConnectionError InternalError not implemented\n") + os.die("Cha-Control: ConnectionError InternalError not implemented") of "encode": var width = 0 var height = 0 @@ -526,25 +512,25 @@ proc main() = let s = hdr.after(':').strip() case hdr.until(':') of "Cha-Image-Dimensions": - (width, height) = parseDimensions(s) + (width, height) = os.parseDimensions(s) of "Cha-Image-Offset": - (offx, offy) = parseDimensions(s) + (offx, offy) = os.parseDimensions(s) of "Cha-Image-Crop-Width": let q = parseUInt32(s, allowSign = false) if q.isNone: - die("Cha-Control: ConnectionError InternalError wrong palette\n") + os.die("Cha-Control: ConnectionError InternalError wrong crop width") 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: - die("Cha-Control: ConnectionError InternalError wrong palette\n") + os.die("Cha-Control: ConnectionError InternalError wrong palette") palette = int(q.get) of "Cha-Image-Quality": let q = parseUInt16(s, allowSign = false) if q.isNone: - die("Cha-Control: ConnectionError InternalError wrong quality\n") + os.die("Cha-Control: ConnectionError InternalError wrong quality") quality = int(q.get) if cropw == -1: cropw = width @@ -556,19 +542,17 @@ proc main() = else: palette = 1024 if width == 0 or height == 0: - let os = newPosixStream(STDOUT_FILENO) - os.sendDataLoop("Cha-Image-Dimensions: 0x0\n") quit(0) # done... let n = width * height let L = n * 4 let ps = newPosixStream(STDIN_FILENO) let src = ps.recvDataLoopOrMmap(L) if src == nil: - die("Cha-Control: ConnectionError InternalError failed to read input\n") + os.die("Cha-Control: ConnectionError InternalError failed to read input") enterNetworkSandbox() # don't swallow stat let p = cast[ptr UncheckedArray[RGBAColorBE]](src.p) - p.toOpenArray(0, n - 1).encode(width, height, offx, offy, cropw, halfdump, - palette) + os.encode(p.toOpenArray(0, n - 1), width, height, offx, offy, cropw, + palette, halfdump) dealloc(src) main() diff --git a/src/local/container.nim b/src/local/container.nim index bbd90f1e..88becc71 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -124,6 +124,8 @@ type erry*: int # same as CanvasImage.offy % 6 # whether the image has transparency, *disregarding the last row* transparent*: bool + # length of introducer, raster, palette data before pixel data + preludeLen*: int Container* = ref object of RootObj # note: this is not the same as source.request.url (but should be synced diff --git a/src/local/pager.nim b/src/local/pager.nim index 09951082..d3b4d860 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -632,8 +632,11 @@ proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap; cachedImage.data = blob cachedImage.state = cisLoaded cachedImage.cacheId = cacheId - let trns = response.headers.getOrDefault("Cha-Image-Sixel-Transparent", "0") - cachedImage.transparent = trns == "1" + cachedImage.transparent = + response.headers.getOrDefault("Cha-Image-Sixel-Transparent", "0") == "1" + let plens = response.headers.getOrDefault("Cha-Image-Sixel-Prelude-Len") + if (let plen = parseInt64(plens).get(0); plen <= int64(int.high)): + cachedImage.preludeLen = plen ) ) container.cachedImages.add(cachedImage) @@ -665,7 +668,8 @@ proc initImages(pager: Pager; container: Container) = let canvasImage = pager.term.loadImage(cached.data, container.process, imageId, image.x - container.fromx, image.y - container.fromy, image.width, image.height, image.x, image.y, pager.bufWidth, - pager.bufHeight, erry, offx, dispw, cached.transparent, redrawNext) + pager.bufHeight, erry, offx, dispw, cached.preludeLen, cached.transparent, + redrawNext) if canvasImage != nil: newImages.add(canvasImage) pager.term.clearImages(pager.bufHeight) diff --git a/src/local/term.nim b/src/local/term.nim index 2550ca69..c984e9d8 100644 --- a/src/local/term.nim +++ b/src/local/term.nim @@ -73,7 +73,8 @@ type damaged: bool marked*: bool dead: bool - transparent: bool # note: this is only set in outputSixelImage + transparent: bool + preludeLen: int kittyId: int # 0 if kitty erry: int @@ -760,7 +761,7 @@ proc checkImageDamage*(term: Terminal; maxw, maxh: int) = term.lineDamage[y] = mx proc loadImage*(term: Terminal; data: Blob; pid, imageId, x, y, width, height, - rx, ry, maxw, maxh, erry, offx, dispw: int; transparent: bool; + rx, ry, maxw, maxh, erry, offx, dispw, preludeLen: int; transparent: bool; redrawNext: var bool): CanvasImage = if (let image = term.findImage(pid, imageId, rx, ry, width, height, erry, offx, dispw); image != nil): @@ -787,7 +788,8 @@ proc loadImage*(term: Terminal; data: Blob; pid, imageId, x, y, width, height, width: width, height: height, erry: erry, - transparent: transparent + transparent: transparent, + preludeLen: preludeLen ) if term.positionImage(image, x, y, maxw, maxh): redrawNext = true @@ -801,6 +803,23 @@ func getU32BE(data: openArray[char]; i: int): uint32 = (uint32(data[i + 1]) shl 16) or (uint32(data[i]) shl 24) +proc appendSixelAttrs(outs: var string; data: openArray[char]; + realw, realh: int) = + var i = 0 + while i < data.len: + let c = data[i] + outs &= c + inc i + if c == '"': # set raster attrs + break + while i < data.len and data[i] != '#': # skip aspect ratio attrs + inc i + outs &= "1;1;" & $realw & ';' & $realh + if i < data.len: + let ol = outs.len + outs.setLen(ol + data.len - i) + copyMem(addr outs[ol], unsafeAddr data[i], data.len - i) + proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage; data: openArray[char]) = let offx = image.offx @@ -809,28 +828,18 @@ proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage; let disph = image.disph let realw = dispw - offx let realh = disph - offy - if data.len < 4: # bounds check + let preludeLen = image.preludeLen + if preludeLen > data.len or data.len < 4: return - let preludeLen = int(data.getU32BE(0)) - if preludeLen > data.len: + let L = data.len - int(data.getU32BE(data.len - 4)) - 4 + if L < 0: return var outs = term.cursorGoto(x, y) - # set transparency if the image has transparent sixels; omit it - # otherwise, for then some terminals (e.g. foot) handle the image more - # efficiently - let trans = image.transparent - outs &= DCS & "0;" & $int(trans) & "q" - # set raster attributes - outs &= "\"1;1;" & $realw & ';' & $realh + outs.appendSixelAttrs(data.toOpenArray(0, preludeLen - 1), realw, realh) term.write(outs) - term.write(data.toOpenArray(4, preludeLen - 1)) - let lookupTableLen = int(data.getU32BE(data.len - 4)) - let L = data.len - lookupTableLen - 4 # Note: we only crop images when it is possible to do so in near constant # time. Otherwise, the image is re-coded in a cropped form. - if preludeLen >= data.len or L < 0: # bounds check - term.write(ST) - elif realh == image.height: # don't crop + if realh == image.height: # don't crop term.write(data.toOpenArray(preludeLen, L - 1)) else: let si = preludeLen + int(data.getU32BE(L + (offy div 6) * 4)) |