about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-10-04 16:44:29 +0200
committerbptato <nincsnevem662@gmail.com>2024-10-04 16:58:42 +0200
commit62586dc23790732e66add5b27d4d37f1a56b41e0 (patch)
treeed33d758156658b93f81de6b71449a70ef69d32b
parentfaa97429d651c76d86ad0c2ab530d9f666fb6927 (diff)
downloadchawan-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.nim108
-rw-r--r--src/local/container.nim2
-rw-r--r--src/local/pager.nim10
-rw-r--r--src/local/term.nim47
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))