about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/local/container.nim16
-rw-r--r--src/local/pager.nim82
-rw-r--r--src/local/term.nim184
3 files changed, 150 insertions, 132 deletions
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