about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--adapter/img/sixel.nim202
-rw-r--r--res/urimethodmap1
-rw-r--r--src/local/container.nim16
-rw-r--r--src/local/pager.nim82
-rw-r--r--src/local/term.nim184
6 files changed, 355 insertions, 134 deletions
diff --git a/Makefile b/Makefile
index e5b94be4..48483754 100644
--- a/Makefile
+++ b/Makefile
@@ -51,7 +51,7 @@ all: $(OUTDIR_BIN)/cha $(OUTDIR_BIN)/mancha $(OUTDIR_CGI_BIN)/http \
 	$(OUTDIR_CGI_BIN)/cha-finger $(OUTDIR_CGI_BIN)/about \
 	$(OUTDIR_CGI_BIN)/file $(OUTDIR_CGI_BIN)/ftp \
 	$(OUTDIR_CGI_BIN)/man $(OUTDIR_CGI_BIN)/spartan \
-	$(OUTDIR_CGI_BIN)/stbi $(OUTDIR_CGI_BIN)/jebp \
+	$(OUTDIR_CGI_BIN)/stbi $(OUTDIR_CGI_BIN)/jebp $(OUTDIR_CGI_BIN)/sixel \
 	$(OUTDIR_LIBEXEC)/urldec $(OUTDIR_LIBEXEC)/urlenc \
 	$(OUTDIR_LIBEXEC)/md2html $(OUTDIR_LIBEXEC)/ansi2html
 	ln -sf "$(OUTDIR)/$(TARGET)/bin/cha" cha
@@ -164,7 +164,7 @@ manpages = $(manpages1) $(manpages5)
 .PHONY: manpage
 manpage: $(manpages:%=doc/%)
 
-protocols = http about file ftp gopher gmifetch cha-finger man spartan stbi jebp
+protocols = http about file ftp gopher gmifetch cha-finger man spartan stbi jebp sixel
 converters = gopher2html md2html ansi2html gmi2html
 tools = urlenc
 
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()
diff --git a/res/urimethodmap b/res/urimethodmap
index 21e01546..b75dc2b0 100644
--- a/res/urimethodmap
+++ b/res/urimethodmap
@@ -21,3 +21,4 @@ img-codec+gif:		cgi-bin:stbi
 img-codec+bmp:		cgi-bin:stbi
 img-codec+x-unknown:	cgi-bin:stbi
 img-codec+webp:		cgi-bin:jebp
+img-codec+x-sixel:	cgi-bin:sixel
diff --git a/src/local/container.nim b/src/local/container.nim
index 046e2bc8..ebfc3940 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -106,6 +106,16 @@ type
     height*: int
     data*: Blob
     bmp*: NetworkBitmap
+    # Following variables are always 0 in kitty mode; they exist to support
+    # sixel cropping.
+    # We can easily crop images where we just have to exclude some lines prior
+    # to/after the image, but we must re-encode if
+    # * offx > 0, dispw < width or
+    # * offy % 6 != previous offy % 6 (currently only happens when cell height
+    #   is not a multiple of 6).
+    offx*: int # same as CanvasImage.offx
+    dispw*: int # same as CanvasImage.dispw
+    erry*: int # same as CanvasImage.offy % 6
 
   Container* = ref object
     # note: this is not the same as source.request.url (but should be synced
@@ -2058,11 +2068,13 @@ proc highlightMarks*(container: Container; display: var FixedGrid;
       hlformat.bgcolor = hlcolor
       display[y * display.width + x].format = hlformat
 
-func findCachedImage*(container: Container; image: PosBitmap): CachedImage =
+func findCachedImage*(container: Container; image: PosBitmap;
+    offx, erry, dispw: int): CachedImage =
   let imageId = NetworkBitmap(image.bmp).imageId
   for it in container.cachedImages:
     if it.bmp.imageId == imageId and it.width == image.width and
-        it.height == image.height:
+        it.height == image.height and it.offx == offx and it.erry == erry and
+        it.dispw == dispw:
       return it
   return nil
 
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 7c2dde27..d81d4986 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -482,14 +482,18 @@ proc redraw(pager: Pager) {.jsfunc.} =
     if pager.container.select != nil:
       pager.container.select.redraw = true
 
-proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap) =
+proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap;
+    offx, erry, dispw: int) =
   let bmp = NetworkBitmap()
   bmp[] = NetworkBitmap(image.bmp)[]
   let request = newRequest(newURL("cache:" & $bmp.cacheId).get)
   let cachedImage = CachedImage(
     bmp: bmp,
     width: image.width,
-    height: image.height
+    height: image.height,
+    offx: offx,
+    erry: erry,
+    dispw: dispw
   )
   pager.loader.shareCachedItem(bmp.cacheId, pager.loader.clientPid,
     container.process)
@@ -523,38 +527,44 @@ proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap) =
     # take target sizes
     bmp.width = uint64(image.width)
     bmp.height = uint64(image.height)
+    let headers = newHeaders({
+      "Cha-Image-Dimensions": $image.width & 'x' & $image.height
+    })
+    var url: URL = nil
     case imageMode
     of imSixel:
-      #TODO we should only cache the final output in memory, not the full
-      # bitmap.
-      response.saveToBitmap(bmp).then(proc() =
-        container.redraw = true
-        cachedImage.loaded = true
-        pager.loader.removeCachedItem(bmp.cacheId)
-      )
+      url = newURL("img-codec+x-sixel:encode").get
+      headers.add("Cha-Image-Sixel-Halfdump", "1")
+      headers.add("Cha-Image-Sixel-Palette", $pager.term.sixelRegisterNum)
+      headers.add("Cha-Image-Background-Color", $pager.term.defaultBackground)
+      headers.add("Cha-Image-Offset", $offx & 'x' & $erry)
+      headers.add("Cha-Image-Crop-Width", $dispw)
     of imKitty:
-      let headers = newHeaders({
-        "Cha-Image-Dimensions": $image.width & 'x' & $image.height
-      })
-      let request = newRequest(
-        newURL("img-codec+png:encode").get,
-        httpMethod = hmPost,
-        headers = headers,
-        body = RequestBody(t: rbtOutput, outputId: response.outputId),
-      )
-      let r = pager.loader.fetch(request)
-      response.resume()
-      response.unregisterFun()
-      response.body.sclose()
-      r.then(proc(res: JSResult[Response]): Promise[JSResult[Blob]] =
-        return res.get.blob()
-      ).then(proc(res: JSResult[Blob]) =
+      url = newURL("img-codec+png:encode").get
+    of imNone: assert false
+    let request = newRequest(
+      url,
+      httpMethod = hmPost,
+      headers = headers,
+      body = RequestBody(t: rbtOutput, outputId: response.outputId),
+    )
+    let r = pager.loader.fetch(request)
+    response.resume()
+    response.unregisterFun()
+    response.body.sclose()
+    r.then(proc(res: JSResult[Response]): Promise[JSResult[Blob]] =
+      if res.isNone:
+        let p = newPromise[JSResult[Blob]]()
+        p.resolve(JSResult[Blob].err(res.error))
+        return p
+      return res.get.blob()
+    ).then(proc(res: JSResult[Blob]) =
+      if res.isSome:
         container.redraw = true
         cachedImage.data = res.get
         cachedImage.loaded = true
-        pager.loader.removeCachedItem(bmp.cacheId)
-      )
-    of imNone: assert false
+      pager.loader.removeCachedItem(bmp.cacheId)
+    )
   )
   container.cachedImages.add(cachedImage)
 
@@ -564,12 +574,22 @@ proc initImages(pager: Pager; container: Container) =
     var imageId = -1
     var data: Blob = nil
     var bmp0 = image.bmp
+    var erry = 0
+    var offx = 0
+    var dispw = 0
     if image.bmp of NetworkBitmap:
       let bmp = NetworkBitmap(image.bmp)
-      let cached = container.findCachedImage(image)
+      if pager.term.imageMode == imSixel:
+        let xpx = (image.x - container.fromx) * pager.attrs.ppc
+        offx = -min(xpx, 0)
+        let maxwpx = pager.bufWidth * pager.attrs.ppc
+        dispw = min(int(image.width) + xpx, maxwpx) - xpx
+        let ypx = (image.y - container.fromy) * pager.attrs.ppl
+        erry = -min(ypx, 0) mod 6
+      let cached = container.findCachedImage(image, offx, erry, dispw)
       imageId = bmp.imageId
       if cached == nil:
-        pager.loadCachedImage(container, image)
+        pager.loadCachedImage(container, image, offx, erry, dispw)
         continue
       bmp0 = cached.bmp
       data = cached.data
@@ -580,7 +600,7 @@ proc initImages(pager: Pager; container: Container) =
       inc pager.imageId
     let canvasImage = pager.term.loadImage(bmp0, data, container.process,
       imageId, image.x - container.fromx, image.y - container.fromy,
-      image.x, image.y, pager.bufWidth, pager.bufHeight)
+      image.x, image.y, pager.bufWidth, pager.bufHeight, erry, offx, dispw)
     if canvasImage != nil:
       newImages.add(canvasImage)
   pager.term.clearImages(pager.bufHeight)
diff --git a/src/local/term.nim b/src/local/term.nim
index 71d32343..3b2b8050 100644
--- a/src/local/term.nim
+++ b/src/local/term.nim
@@ -67,6 +67,8 @@ type
     marked*: bool
     kittyId: int
     bmp: Bitmap
+    # 0 if kitty
+    erry: int
     # absolute x, y in container
     rx: int
     ry: int
@@ -92,10 +94,10 @@ type
     stdinUnblocked: bool
     stdinWasUnblocked: bool
     origTermios: Termios
-    defaultBackground: RGBColor
+    defaultBackground*: RGBColor
     defaultForeground: RGBColor
     ibuf*: string # buffer for chars when we can't process them
-    sixelRegisterNum: int
+    sixelRegisterNum*: int
     sixelMaxWidth: int
     sixelMaxHeight: int
     kittyId: int # counter for kitty image (*not* placement) ids.
@@ -212,21 +214,16 @@ when TermcapFound:
   func cap(term: Terminal; c: TermcapCap): string = $term.tc.caps[c]
   func ccap(term: Terminal; c: TermcapCap): cstring = term.tc.caps[c]
 
-  var goutfile: File
-  proc putc(c: char): cint {.cdecl.} =
-    goutfile.write(c)
+proc write(term: Terminal; s: openArray[char]) =
+  # write() calls $ on s, so we must writeBuffer
+  if s.len > 0:
+    discard term.outfile.writeBuffer(unsafeAddr s[0], s.len)
 
-  proc write(term: Terminal; s: cstring) =
-    discard tputs(s, 1, putc)
+proc write(term: Terminal; s: string) =
+  term.outfile.write(s)
 
-  proc write(term: Terminal; s: string) =
-    if term.tc != nil:
-      term.write(cstring(s))
-    else:
-      term.outfile.write(s)
-else:
-  proc write(term: Terminal; s: string) =
-    term.outfile.write(s)
+proc write(term: Terminal; s: cstring) =
+  term.outfile.write(s)
 
 proc readChar*(term: Terminal): char =
   if term.ibuf.len == 0:
@@ -645,12 +642,14 @@ proc outputGrid*(term: Terminal) =
   term.cursorx = -1
   term.cursory = -1
 
-func findImage(term: Terminal; pid, imageId: int; bmp: Bitmap; rx, ry: int):
-    CanvasImage =
+func findImage(term: Terminal; pid, imageId: int; bmp: Bitmap;
+    rx, ry, erry, offx, dispw: int): CanvasImage =
   for it in term.canvasImages:
     if it.pid == pid and it.imageId == imageId and
         it.bmp.width == bmp.width and it.bmp.height == bmp.height and
-        it.rx == rx and it.ry == ry:
+        it.rx == rx and it.ry == ry and
+        (term.imageMode != imSixel or it.erry == erry and it.dispw == dispw and
+          it.offx == offx):
       return it
   return nil
 
@@ -700,8 +699,9 @@ proc clearImages*(term: Terminal; maxh: int) =
     image.marked = false
 
 proc loadImage*(term: Terminal; bmp: Bitmap; data: Blob; pid, imageId,
-    x, y, rx, ry, maxw, maxh: int): CanvasImage =
-  if (let image = term.findImage(pid, imageId, bmp, rx, ry); image != nil):
+    x, y, rx, ry, maxw, maxh, erry, offx, dispw: int): CanvasImage =
+  if (let image = term.findImage(pid, imageId, bmp, rx, ry, erry, offx, dispw);
+      image != nil):
     # reuse image on screen
     if image.x != x or image.y != y:
       # only clear sixels; with kitty we just move the existing image
@@ -729,45 +729,23 @@ proc loadImage*(term: Terminal; bmp: Bitmap; data: Blob; pid, imageId,
     imageId: imageId,
     data: data,
     rx: rx,
-    ry: ry
+    ry: ry,
+    erry: erry
   )
   if term.positionImage(image, x, y, maxw, maxh):
     return image
   # no longer on screen
   return nil
 
-# data is binary 0..63; the output is the final ASCII form.
-proc compressSixel(data: openArray[uint8]): string =
-  var outs = newStringOfCap(data.len div 4)
-  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
+func getOffYIdx(data: openArray[char]; y, starti: int): int32 =
+  let i = starti + (y div 6) * 4
+  return int32(data[i]) or
+    (int32(data[i + 1]) shl 8) or
+    (int32(data[i + 2]) shl 16) or
+    (int32(data[i + 3]) shl 24)
 
-proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage) =
+proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage;
+    data: openArray[char]) =
   let offx = image.offx
   let offy = image.offy
   let dispw = image.dispw
@@ -776,53 +754,62 @@ proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage) =
   var outs = term.cursorGoto(x, y)
   outs &= DCSSTART & 'q'
   # set raster attributes
-  outs &= "\"1;1;" & $(dispw - offx) & ';' & $(disph - offy)
-  for b in 16 ..< 256:
-    # laziest possible 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
-  var n = offy * int(bmp.width) # start at offy
-  let H = disph * int(bmp.width) # end at disph
-  var m = y * term.attrs.width * term.attrs.ppl # track absolute y
   let realw = dispw - offx
-  let cx0 = x * term.attrs.ppc
-  while n < H:
-    var bands = newSeq[SixelBand]()
-    for i in 0 ..< 6:
-      if n >= H:
-        break
-      let mask = 1u8 shl i
-      let my = m div term.attrs.ppl
-      for bx in 0 ..< realw:
-        let cx = (cx0 + bx) div term.attrs.ppc
-        var c0 = bmp.px[n + bx + offx]
-        if c0.a != 255:
-          let bgcolor0 = term.canvas[my + cx].format.bgcolor
-          let bgcolor = term.getRGB(bgcolor0, term.defaultBackground)
-          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 j = bands.find(c); j != -1):
-          bands[j].data[bx] = bands[j].data[bx] or mask
-        else:
-          bands.add(SixelBand(c: c, data: newSeq[uint8](realw)))
-          bands[^1].data[^1] = mask
-      n += int(bmp.width)
-      m += term.attrs.width
-    term.write(outs)
-    outs = ""
-    for i, band in bands:
-      let t = if i != bands.high: '$' else: '-'
-      let n = band.c - 15
-      outs &= '#' & $n & band.data.compressSixel() & t
-  if outs.len > 0 and outs[^1] == '-':
-    outs.setLen(outs.len - 1)
-  outs &= ST
+  var realh = disph - offy
+  #if disph < int(bmp.height):
+  #  realh -= image.erry
+  outs &= "\"1;1;" & $realw & ';' & $realh
   term.write(outs)
+  let sraLen = uint32(data[0]) or
+    (uint32(data[1]) shl 8) or
+    (uint32(data[2]) shl 16) or
+    (uint32(data[3]) shl 24)
+  let preludeLen = int(sraLen + 4)
+  term.write(data.toOpenArray(4, 4 + int(sraLen) - 1))
+  let lookupTableLen = ((int(bmp.height) + 5) div 6 + 1) * 4
+  let L = data.len - lookupTableLen
+  # 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 realh == int(bmp.height):
+    term.write(data.toOpenArray(preludeLen, L - 1))
+  else:
+    let offyi = data.getOffYIdx(offy, L)
+    var e = disph
+    if disph < int(bmp.height):
+      e -= image.erry
+    let endyi = data.getOffYIdx(e, L)
+    if endyi <= offyi:
+      return
+    let si = preludeLen + int(offyi)
+    let ei = preludeLen + int(endyi) - 1
+    assert offyi < endyi
+    assert ei <= data.len - lookupTableLen
+    term.write(data.toOpenArray(si, ei - 1))
+    var ndash = 0
+    for c in data.toOpenArray(si, ei - 1):
+      if c == '-':
+        inc ndash
+    let herry = realh - (realh div 6) * 6
+    if herry > 0 and disph < int(bmp.height):
+      # can't write out the last row completely; mask off the bottom part.
+      let mask = (1u8 shl herry) - 1
+      var s = "-"
+      var i = ei + 1
+      inc ndash
+      while i < L and (let c = data[i]; c notin {'-', '\e'}): # newline or ST
+        let u = uint8(c) - 0x3F # may underflow, but that's no problem
+        if u < 0x40:
+          s &= char((u and mask) + 0x3F)
+        else:
+          s &= c
+        inc i
+      term.write(s)
+    term.write(ST)
+
+proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage) =
+  var p = cast[ptr UncheckedArray[char]](image.data.buffer)
+  let H = int(image.data.size - 1)
+  term.outputSixelImage(x, y, image, p.toOpenArray(0, H))
 
 proc outputKittyImage(term: Terminal; x, y: int; image: CanvasImage) =
   var outs = term.cursorGoto(x, y) &
@@ -944,8 +931,6 @@ when TermcapFound:
     if res == 0: # retry as dosansi
       res = tgetent(cast[cstring](addr tc.bp), "dosansi")
     if res > 0: # success
-      assert goutfile == nil
-      goutfile = term.outfile
       term.tc = tc
       for id in TermcapCap:
         tc.caps[id] = tgetstr(cstring($id), cast[ptr cstring](addr tc.funcstr))
@@ -1213,6 +1198,7 @@ proc detectTermAttributes(term: Terminal; windowOnly: bool): TermStartResult =
       for (n, rgb) in r.colorMap:
         term.colorMap[n] = rgb
     else:
+      term.sixelRegisterNum = 256
       # something went horribly wrong. set result to DA1 fail, pager will
       # alert the user
       res = tsrDA1Fail