about summary refs log blame commit diff stats
path: root/adapter/img/canvas.nim
blob: cfa2cd327bc58024bd4a1d1458fcd8e703a68016 (plain) (tree)
1
2
3
4
5
6
7
8
9







                                                                            
                    



                   
                 

                   
                        

                  
                   
                    
                   










                                                                              


































                                                                         
                                                    
                                           



                                                                       
                      







                                                                 






















































































































































































                                                                           











































































                                                                               
                                      
















                                                                   
# Very simple canvas renderer. At the moment, it uses an undocumented binary
# protocol for reading commands, and renders it whenever stdin is closed.
# So for now, it can only really render a single frame.
#
# It uses unifont for rendering text - currently I just store it as PNG
# and read it with stbi. (TODO: try switching to a more efficient format
# like qemacs fbf.)

import std/algorithm
import std/os
import std/posix
import std/strutils

import types/path
import io/bufreader
import io/dynstream
import types/canvastypes
import types/color
import types/line
import types/vector
import utils/sandbox
import utils/twtuni

{.compile: "canvas.c".}

{.passc: "-I" & currentSourcePath().parentDir().}

{.push header: "stb_image.h".}
proc stbi_load_from_memory(buffer: ptr uint8; len: cint; x, y, comp: ptr cint;
  req_comp: cint): ptr uint8
proc stbi_image_free(retval_from_stbi_load: pointer)
{.pop.}

type
  GlyphCacheItem = object
    u: uint32
    bmp: Bitmap

  Bitmap = ref object
    px: seq[RGBAColorBE]
    width: int
    height: int

proc newBitmap(width, height: int): Bitmap =
  return Bitmap(
    px: newSeq[RGBAColorBE](width * height),
    width: width,
    height: height
  )

proc setpx(bmp: Bitmap; x, y: int; color: RGBAColorBE) {.inline.} =
  bmp.px[bmp.width * y + x] = color

proc setpx(bmp: Bitmap; x, y: int; color: ARGBColor) {.inline.} =
  bmp.px[bmp.width * y + x] = rgba_be(color.r, color.g, color.b, color.a)

proc getpx(bmp: Bitmap; x, y: int): RGBAColorBE {.inline.} =
  return bmp.px[bmp.width * y + x]

proc setpxb(bmp: Bitmap; x, y: int; c: RGBAColorBE) {.inline.} =
  if c.a == 255:
    bmp.setpx(x, y, c)
  else:
    bmp.setpx(x, y, bmp.getpx(x, y).blend(c))

proc setpxb(bmp: Bitmap; x, y: int; c: ARGBColor) {.inline.} =
  bmp.setpxb(x, y, rgba_be(c.r, c.g, c.b, c.a))

const unifont = readFile"res/unifont_jp-15.0.05.png"
proc loadUnifont(unifont: string): Bitmap =
  var width, height, comp: cint
  let p = stbi_load_from_memory(cast[ptr uint8](unsafeAddr unifont[0]),
    cint(unifont.len), addr width, addr height, addr comp, 4)
  let len = width * height
  let bitmap = Bitmap(
    px: cast[seq[RGBAColorBE]](newSeqUninitialized[uint32](len)),
    width: int(width),
    height: int(height)
  )
  copyMem(addr bitmap.px[0], p, len)
  stbi_image_free(p)
  return bitmap

# https://en.wikipedia.org/wiki/Bresenham's_line_algorithm#All_cases
proc plotLineLow(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) =
  var dx = x2 - x1
  var dy = y2 - y1
  var yi = 1
  if dy < 0:
    yi = -1
    dy = -dy
  var D = 2 * dy - dx;
  var y = y1;
  for x in x1 ..< x2:
    if x < 0 or y < 0 or x >= bmp.width or y >= bmp.height:
      break
    bmp.setpxb(x, y, color)
    if D > 0:
       y = y + yi;
       D = D - 2 * dx;
    D = D + 2 * dy;

proc plotLineHigh(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) =
  var dx = x2 - x1
  var dy = y2 - y1
  var xi = 1
  if dx < 0:
    xi = -1
    dx = -dx
  var D = 2 * dx - dy
  var x = x1
  for y in y1 ..< y2:
    if x < 0 or y < 0 or x >= bmp.width or y >= bmp.height:
      break
    bmp.setpxb(x, y, color)
    if D > 0:
       x = x + xi
       D = D - 2 * dy
    D = D + 2 * dx

proc plotLine(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) =
  if abs(y2 - y1) < abs(x2 - x1):
    if x1 > x2:
      bmp.plotLineLow(x2, y2, x1, y1, color)
    else:
      bmp.plotLineLow(x1, y1, x2, y2, color)
  else:
    if y1 > y2:
      bmp.plotLineHigh(x2, y2, x1, y1, color)
    else:
      bmp.plotLineHigh(x1, y1, x2, y2, color)

proc plotLine(bmp: Bitmap; a, b: Vector2D; color: ARGBColor) =
  bmp.plotLine(int(a.x), int(a.y), int(b.x), int(b.y), color)

proc plotLine(bmp: Bitmap; line: Line; color: ARGBColor) =
  bmp.plotLine(line.p0, line.p1, color)

proc strokePath(bmp: Bitmap; lines: seq[Line]; color: ARGBColor) =
  for line in lines:
    bmp.plotLine(line, color)

func isInside(windingNumber: int; fillRule: CanvasFillRule): bool =
  return case fillRule
  of cfrNonZero: windingNumber != 0
  of cfrEvenOdd: windingNumber mod 2 == 0

# Algorithm originally from SerenityOS.
proc fillPath(bmp: Bitmap; lines: PathLines; color: ARGBColor;
    fillRule: CanvasFillRule) =
  var i = 0
  var ylines: seq[LineSegment] = @[]
  for y in int(lines.miny) .. int(lines.maxy):
    for k in countdown(ylines.high, 0):
      if ylines[k].maxy < float64(y):
        ylines.del(k) # we'll sort anyways, so del is fine
    for j in i ..< lines.len:
      if lines[j].miny > float64(y):
        break
      if lines[j].maxy > float64(y):
        ylines.add(lines[j])
      inc i
    ylines.sort(cmpLineSegmentX)
    var w = if fillRule == cfrNonZero: 1 else: 0
    for k in 0 ..< ylines.high:
      let a = ylines[k]
      let b = ylines[k + 1]
      let sx = int(a.minyx)
      let ex = int(b.minyx)
      if w.isInside(fillRule) and y > 0:
        for x in sx .. ex:
          if x > 0:
            bmp.setpxb(x, y, color)
      if int(a.p0.y) != y and int(a.p1.y) != y and int(b.p0.y) != y and
          int(b.p1.y) != y and sx != ex or a.islope * b.islope < 0:
        case fillRule
        of cfrEvenOdd: inc w
        of cfrNonZero:
          if a.p0.y < a.p1.y:
            inc w
          else:
            dec w
      ylines[k].minyx += ylines[k].islope
    if ylines.len > 0:
      ylines[^1].minyx += ylines[^1].islope

proc fillRect(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) =
  for y in y1 ..< y2:
    for x in x1 ..< x2:
      bmp.setpxb(x, y, color)

proc strokeRect(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) =
  for x in x1 ..< x2:
    bmp.setpxb(x, y1, color)
    bmp.setpxb(x, y2, color)
  for y in y1 ..< y2:
    bmp.setpxb(x1, y, color)
    bmp.setpxb(x2, y, color)

var unifontBitmap: Bitmap = nil
var glyphCache: seq[GlyphCacheItem] = @[]
var glyphCacheI = 0
proc getCharBmp(u: uint32): Bitmap =
  # We only have the BMP.
  let u = if u <= 0xFFFF: u else: 0xFFFD
  for it in glyphCache:
    if it.u == u:
      return it.bmp
  # Unifont glyphs start at x: 32, y: 64, and are of 8x16/16x16 size
  let gx = int(32 + 16 * (u mod 0x100))
  let gy = int(64 + 16 * (u div 0x100))
  var fullwidth = false
  const white = rgba_be(255, 255, 255, 255)
  block loop:
    # hack to recognize full width characters
    for y in 0 ..< 16:
      for x in 8 ..< 16:
        if unifontBitmap.getpx(gx + x, gy + y) != white:
          fullwidth = true
          break loop
  let bmp = newBitmap(if fullwidth: 16 else: 8, 16)
  for y in 0 ..< bmp.height:
    for x in 0 ..< bmp.width:
      let c = unifontBitmap.getpx(gx + x, gy + y)
      if c != white:
        bmp.setpx(x, y, c)
  if glyphCache.len < 256:
    glyphCache.add(GlyphCacheItem(u: u, bmp: bmp))
  else:
    glyphCache[glyphCacheI] = GlyphCacheItem(u: u, bmp: bmp)
    inc glyphCacheI
    if glyphCacheI >= glyphCache.len:
      glyphCacheI = 0
  return bmp

proc drawBitmap(a, b: Bitmap; p: Vector2D) =
  for y in 0 ..< b.height:
    for x in 0 ..< b.width:
      let ax = int(p.x) + x
      let ay = int(p.y) + y
      if ax >= 0 and ay >= y and ax < a.width and ay < a.height:
        a.setpxb(ax, ay, b.getpx(x, y))

proc fillText(bmp: Bitmap; text: string; x, y: float64; color: ARGBColor;
    textAlign: CanvasTextAlign) =
  var w = 0f64
  var glyphs: seq[Bitmap] = @[]
  for u in text.points:
    let glyph = getCharBmp(u)
    glyphs.add(glyph)
    w += float64(glyph.width)
  var x = x
  #TODO rtl
  case textAlign
  of ctaLeft, ctaStart: discard
  of ctaRight, ctaEnd: x -= w
  of ctaCenter: x -= w / 2
  for glyph in glyphs:
    bmp.drawBitmap(glyph, Vector2D(x: x, y: y - 8))
    x += float64(glyph.width)

proc strokeText(bmp: Bitmap; text: string; x, y: float64; color: ARGBColor;
    textAlign: CanvasTextAlign) =
  #TODO
  bmp.fillText(text, x, y, color, textAlign)

proc main() =
  enterNetworkSandbox()
  let os = newPosixStream(STDOUT_FILENO)
  let ps = newPosixStream(STDIN_FILENO)
  if getEnv("MAPPED_URI_SCHEME") != "img-codec+x-cha-canvas":
    os.write("Cha-Control: ConnectionError 1 wrong scheme\n")
    quit(1)
  case getEnv("MAPPED_URI_PATH")
  of "decode":
    let headers = getEnv("REQUEST_HEADERS")
    for hdr in headers.split('\n'):
      if hdr.strip() == "Cha-Image-Info-Only: 1":
        #TODO this is a hack...
        # basically, we eat & discard all data from the buffer so it gets saved
        # to a cache file. then, actually render when the pager asks us to
        # do so.
        # obviously this is highly sub-optimal; a better solution would be to
        # leave stdin open & pass down the stream id from the buffer. (but then
        # you have to save canvas output too, so it doesn't have to be
        # re-coded, and handle that case in encoders... or implement on-demand
        # multi-frame output.)
        os.write("\n")
        discard ps.recvAll()
        quit(0)
    var cmd: PaintCommand
    var width: int
    var height: int
    ps.withPacketReader r:
      r.sread(cmd)
      if cmd != pcSetDimensions:
        os.write("Cha-Control: ConnectionError 1 wrong dimensions\n")
        quit(1)
      r.sread(width)
      r.sread(height)
    os.write("Cha-Image-Dimensions: " & $width & "x" & $height & "\n\n")
    let bmp = newBitmap(width, height)
    var alive = true
    while alive:
      try:
        ps.withPacketReader r:
          r.sread(cmd)
          case cmd
          of pcSetDimensions:
            alive = false
          of pcFillRect, pcStrokeRect:
            var x1, y1, x2, y2: int
            var color: ARGBColor
            r.sread(x1)
            r.sread(y1)
            r.sread(x2)
            r.sread(y2)
            r.sread(color)
            if cmd == pcFillRect:
              bmp.fillRect(x1, y1, x2, y2, color)
            else:
              bmp.strokeRect(x1, y1, x2, y2, color)
          of pcFillPath:
            var lines: PathLines
            var color: ARGBColor
            var fillRule: CanvasFillRule
            r.sread(lines)
            r.sread(color)
            r.sread(fillRule)
            bmp.fillPath(lines, color, fillRule)
          of pcStrokePath:
            var lines: seq[Line]
            var color: ARGBColor
            r.sread(lines)
            r.sread(color)
            bmp.strokePath(lines, color)
          of pcFillText, pcStrokeText:
            if unifontBitmap == nil:
              unifontBitmap = loadUnifont(unifont)
            var text: string
            var x, y: float64
            var color: ARGBColor
            var align: CanvasTextAlign
            r.sread(text)
            r.sread(x)
            r.sread(y)
            r.sread(color)
            r.sread(align)
            if cmd == pcFillText:
              bmp.fillText(text, x, y, color, align)
            else:
              bmp.strokeText(text, x, y, color, align)
      except EOFError, ErrorConnectionReset, ErrorBrokenPipe:
        break
    os.sendDataLoop(addr bmp.px[0], bmp.px.len * sizeof(bmp.px[0]))
  of "encode":
    os.write("Cha-Control: ConnectionError 1 not supported\n")
    quit(1)

main()