diff options
author | bptato <nincsnevem662@gmail.com> | 2024-09-01 01:03:50 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-09-01 01:46:38 +0200 |
commit | 55cfd29e961488a8c1ed9eb7801d237d27bc86c7 (patch) | |
tree | c74569e15ca72d777eadcfd19a0203cbb76c3e3f | |
parent | e9466c4c436f964b53034e28356aa3f5c957a068 (diff) | |
download | chawan-55cfd29e961488a8c1ed9eb7801d237d27bc86c7.tar.gz |
canvas: move to separate CGI script
* stream: and passFd is now client-based, and accessible for buffers * Bitmap's width & height is now int, not uint64 * no more non-network Bitmap special case in the pager for canvas I just shoehorned it into the static image model, so it still doesn't render changes after page load. But at least now it doesn't crash the browser.
-rw-r--r-- | Makefile | 11 | ||||
-rw-r--r-- | adapter/img/canvas.c | 5 | ||||
-rw-r--r-- | adapter/img/canvas.nim | 141 | ||||
-rw-r--r-- | adapter/img/jebp.nim | 5 | ||||
-rw-r--r-- | res/urimethodmap | 1 | ||||
-rw-r--r-- | src/css/cascade.nim | 16 | ||||
-rw-r--r-- | src/css/cssvalues.nim | 2 | ||||
-rw-r--r-- | src/html/dom.nim | 392 | ||||
-rw-r--r-- | src/img/bitmap.nim | 16 | ||||
-rw-r--r-- | src/img/painter.nim | 112 | ||||
-rw-r--r-- | src/img/path.nim | 2 | ||||
-rw-r--r-- | src/io/bufreader.nim | 36 | ||||
-rw-r--r-- | src/io/bufwriter.nim | 14 | ||||
-rw-r--r-- | src/io/promise.nim | 5 | ||||
-rw-r--r-- | src/layout/box.nim | 4 | ||||
-rw-r--r-- | src/layout/engine.nim | 2 | ||||
-rw-r--r-- | src/layout/renderdocument.nim | 2 | ||||
-rw-r--r-- | src/loader/loader.nim | 24 | ||||
-rw-r--r-- | src/loader/response.nim | 42 | ||||
-rw-r--r-- | src/local/container.nim | 2 | ||||
-rw-r--r-- | src/local/pager.nim | 55 | ||||
-rw-r--r-- | src/local/term.nim | 42 | ||||
-rw-r--r-- | src/server/buffer.nim | 4 |
23 files changed, 571 insertions, 364 deletions
diff --git a/Makefile b/Makefile index 369d79e7..c99165b4 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,8 @@ 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)/sixel \ + $(OUTDIR_CGI_BIN)/stbi $(OUTDIR_CGI_BIN)/jebp \ + $(OUTDIR_CGI_BIN)/canvas $(OUTDIR_CGI_BIN)/sixel \ $(OUTDIR_LIBEXEC)/urldec $(OUTDIR_LIBEXEC)/urlenc \ $(OUTDIR_LIBEXEC)/md2html $(OUTDIR_LIBEXEC)/ansi2html ln -sf "$(OUTDIR)/$(TARGET)/bin/cha" cha @@ -91,6 +92,7 @@ $(OUTDIR_CGI_BIN)/gmifetch: adapter/protocol/gmifetch.c $(CC) $(GMIFETCH_CFLAGS) adapter/protocol/gmifetch.c -o "$(OUTDIR_CGI_BIN)/gmifetch" $(GMIFETCH_LDFLAGS) twtstr = src/utils/twtstr.nim src/utils/charcategory.nim src/utils/map.nim +dynstream = src/io/dynstream.nim src/io/serversocket.nim $(OUTDIR_CGI_BIN)/man: lib/monoucha/monoucha/jsregex.nim \ lib/monoucha/monoucha/libregexp.nim src/types/opt.nim $(twtstr) $(OUTDIR_CGI_BIN)/http: adapter/protocol/curlwrap.nim \ @@ -109,6 +111,11 @@ $(OUTDIR_CGI_BIN)/stbi: adapter/img/stbi.nim adapter/img/stb_image.c \ adapter/img/stb_image.h src/utils/sandbox.nim $(OUTDIR_CGI_BIN)/jebp: adapter/img/jebp.c adapter/img/jebp.h \ src/utils/sandbox.nim +$(OUTDIR_CGI_BIN)/sixel: src/types/color.nim src/utils/sandbox.nim $(twtstr) +$(OUTDIR_CGI_BIN)/canvas: src/css/cssvalues.nim src/img/bitmap.nim \ + src/img/painter.nim src/img/path.nim src/io/bufreader.nim \ + src/types/color.nim src/types/line.nim src/utils/sandbox.nim \ + $(dynstream) $(OUTDIR_LIBEXEC)/urlenc: $(twtstr) $(OUTDIR_LIBEXEC)/gopher2html: adapter/gophertypes.nim $(twtstr) $(OUTDIR_LIBEXEC)/ansi2html: src/types/color.nim $(twtstr) @@ -164,7 +171,7 @@ manpages = $(manpages1) $(manpages5) .PHONY: manpage manpage: $(manpages:%=doc/%) -protocols = http about file ftp gopher gmifetch cha-finger man spartan stbi jebp sixel +protocols = http about file ftp gopher gmifetch cha-finger man spartan stbi jebp sixel canvas converters = gopher2html md2html ansi2html gmi2html tools = urlenc diff --git a/adapter/img/canvas.c b/adapter/img/canvas.c new file mode 100644 index 00000000..9e386ca1 --- /dev/null +++ b/adapter/img/canvas.c @@ -0,0 +1,5 @@ +#define STBI_ONLY_PNG +#define STBI_NO_STDIO +#define STBI_NO_LINEAR +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" diff --git a/adapter/img/canvas.nim b/adapter/img/canvas.nim new file mode 100644 index 00000000..2182e1d9 --- /dev/null +++ b/adapter/img/canvas.nim @@ -0,0 +1,141 @@ +# 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/os +import std/posix +import std/strutils + +import css/cssvalues +import img/bitmap +import img/painter +import img/path +import io/bufreader +import io/dynstream +import types/color +import types/line +import utils/sandbox + +{.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.} + +const unifont = readFile"res/unifont_jp-15.0.05.png" +proc loadUnifont(unifont: string): ImageBitmap = + 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 = ImageBitmap( + 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 + +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: CSSTextAlign + 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() diff --git a/adapter/img/jebp.nim b/adapter/img/jebp.nim index 03d72d59..afeb283e 100644 --- a/adapter/img/jebp.nim +++ b/adapter/img/jebp.nim @@ -6,9 +6,6 @@ import std/strutils import utils/sandbox import utils/twtstr -{.passc: "-fno-strict-aliasing".} -{.passl: "-fno-strict-aliasing".} - {.compile: "jebp.c".} when sizeof(cint) < 4: @@ -61,10 +58,12 @@ proc myRead(data: pointer; size: csize_t; user: pointer): csize_t {.cdecl.} = n += csize_t(i) return n +{.push header: "stb_image_resize.h".} proc stbir_resize_uint8(input_pixels: ptr uint8; input_w, input_h, input_stride_in_bytes: cint; output_pixels: ptr uint8; output_w, output_h, output_stride_in_bytes, num_channels: cint): cint {.importc.} +{.pop.} proc writeAll(data: pointer; size: int) = var n = 0 diff --git a/res/urimethodmap b/res/urimethodmap index b75dc2b0..356e1e4b 100644 --- a/res/urimethodmap +++ b/res/urimethodmap @@ -22,3 +22,4 @@ 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 +img-codec+x-cha-canvas: cgi-bin:canvas diff --git a/src/css/cascade.nim b/src/css/cascade.nim index efa9227f..1cee3399 100644 --- a/src/css/cascade.nim +++ b/src/css/cascade.nim @@ -416,13 +416,15 @@ proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet; let styledText = styledParent.newStyledReplacement(content, pseudo) styledParent.children.add(styledText) of peCanvas: - let content = CSSContent( - t: ContentImage, - s: "canvas://", - bmp: HTMLCanvasElement(styledParent.node).bitmap - ) - let styledText = styledParent.newStyledReplacement(content, pseudo) - styledParent.children.add(styledText) + let bmp = HTMLCanvasElement(styledParent.node).bitmap + if bmp.cacheId != 0: + let content = CSSContent( + t: ContentImage, + s: "canvas://", + bmp: bmp + ) + let styledText = styledParent.newStyledReplacement(content, pseudo) + styledParent.children.add(styledText) of peVideo: let content = CSSContent(t: ContentVideo) let styledText = styledParent.newStyledReplacement(content, pseudo) diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim index f0b3cbc8..f8a50723 100644 --- a/src/css/cssvalues.nim +++ b/src/css/cssvalues.nim @@ -318,7 +318,7 @@ type CSSContent* = object t*: CSSContentType s*: string - bmp*: Bitmap + bmp*: NetworkBitmap CSSQuotes* = object auto*: bool diff --git a/src/html/dom.nim b/src/html/dom.nim index 22462db2..c1abd4e3 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -2,6 +2,7 @@ import std/algorithm import std/deques import std/math import std/options +import std/posix import std/sets import std/strutils import std/tables @@ -21,6 +22,7 @@ import html/script import img/bitmap import img/painter import img/path +import io/bufwriter import io/dynstream import io/promise import js/console @@ -39,6 +41,7 @@ import monoucha/quickjs import monoucha/tojs import types/blob import types/color +import types/line import types/matrix import types/opt import types/referrer @@ -102,6 +105,8 @@ type styling*: bool # ID of the next image imageId: int + # list of streams that must be closed for canvas rendering on load + pendingCanvasCtls*: seq[CanvasRenderingContext2D] # Navigator stuff Navigator* = object @@ -343,8 +348,8 @@ type HTMLLabelElement* = ref object of HTMLElement HTMLCanvasElement* = ref object of HTMLElement - ctx2d: CanvasRenderingContext2D - bitmap*: Bitmap + ctx2d*: CanvasRenderingContext2D + bitmap*: NetworkBitmap DrawingState = object # CanvasTransform @@ -366,6 +371,7 @@ type bitmap: Bitmap state: DrawingState stateStack: seq[DrawingState] + ps*: PosixStream TextMetrics = ref object # x-direction @@ -384,7 +390,7 @@ type ideographicBaseline {.jsget.}: float64 HTMLImageElement* = ref object of HTMLElement - bitmap*: Bitmap + bitmap*: NetworkBitmap fetchStarted: bool HTMLVideoElement* = ref object of HTMLElement @@ -449,7 +455,48 @@ jsDestructor(CanvasRenderingContext2D) jsDestructor(TextMetrics) jsDestructor(CSSStyleDeclaration) +# Forward declarations +func attr*(element: Element; s: StaticAtom): string +func attrb*(element: Element; s: CAtom): bool +func baseURL*(document: Document): URL +proc attr*(element: Element; name: CAtom; value: string) +proc attr*(element: Element; name: StaticAtom; value: string) +proc delAttr(element: Element; i: int; keep = false) +proc getImageId(window: Window): int proc parseColor(element: Element; s: string): ARGBColor +proc reflectAttr(element: Element; name: CAtom; value: Option[string]) + +# Forward declaration hacks +# set in css/cascade +var appliesFwdDecl*: proc(mqlist: MediaQueryList; window: Window): bool + {.nimcall, noSideEffect.} +# set in css/match +var doqsa*: proc (node: Node; q: string): seq[Element] {.nimcall.} = nil +var doqs*: proc (node: Node; q: string): Element {.nimcall.} = nil +# set in html/chadombuilder +var domParseHTMLFragment*: proc(element: Element; s: string): seq[Node] + {.nimcall.} +# set in html/env +var windowFetch*: proc(window: Window; input: JSValue; + init = RequestInit(window: JS_UNDEFINED)): JSResult[FetchPromise] + {.nimcall.} = nil + +# For now, these are the same; on an API level however, getGlobal is guaranteed +# to be non-null, while getWindow may return null in the future. (This is in +# preparation for Worker support.) +func getGlobal*(ctx: JSContext): Window = + let global = JS_GetGlobalObject(ctx) + var window: Window + assert ctx.fromJS(global, window).isSome + JS_FreeValue(ctx, global) + return window + +func getWindow*(ctx: JSContext): Window = + let global = JS_GetGlobalObject(ctx) + var window: Window + assert ctx.fromJS(global, window).isSome + JS_FreeValue(ctx, global) + return window func console(window: Window): Console = return window.internalConsole @@ -457,20 +504,125 @@ func console(window: Window): Console = proc resetTransform(state: var DrawingState) = state.transformMatrix = newIdentityMatrix(3) -proc resetState(state: var DrawingState) = +proc reset(state: var DrawingState) = state.resetTransform() state.fillStyle = rgba(0, 0, 0, 255) state.strokeStyle = rgba(0, 0, 0, 255) state.path = newPath() proc create2DContext*(jctx: JSContext; target: HTMLCanvasElement; - options = JS_UNDEFINED): CanvasRenderingContext2D = - let ctx = CanvasRenderingContext2D( + options = JS_UNDEFINED) = + var pipefd: array[2, cint] + if pipe(pipefd) == -1: + return + let window = jctx.getWindow() + let imageId = target.bitmap.imageId + let loader = window.loader + loader.passFd("canvas-ctl-" & $imageId, FileHandle(pipefd[0])) + discard close(pipefd[0]) + let ps = newPosixStream(FileHandle(pipefd[1])) + let ctlreq = newRequest(newURL("stream:canvas-ctl-" & $imageId).get) + let ctlres = loader.doRequest(ctlreq) + doAssert ctlres.res == 0 + let cacheId = loader.addCacheFile(ctlres.outputId, loader.clientPid) + target.bitmap.cacheId = cacheId + let request = newRequest( + newURL("img-codec+x-cha-canvas:decode").get, + httpMethod = hmPost, + headers = newHeaders({"Cha-Image-Info-Only": "1"}), + body = RequestBody(t: rbtOutput, outputId: ctlres.outputId) + ) + let response = loader.doRequest(request) + if response.res != 0: + # no canvas module; give up + ps.sclose() + ctlres.resume() + ctlres.close() + return + ctlres.resume() + ctlres.close() + response.resume() + target.ctx2d = CanvasRenderingContext2D( bitmap: target.bitmap, - canvas: target + canvas: target, + ps: ps ) - ctx.state.resetState() - return ctx + window.pendingCanvasCtls.add(target.ctx2d) + ps.withPacketWriter w: + w.swrite(pcSetDimensions) + w.swrite(target.bitmap.width) + w.swrite(target.bitmap.height) + target.ctx2d.state.reset() + +proc fillRect(ctx: CanvasRenderingContext2D; x1, y1, x2, y2: int; + color: ARGBColor) = + if ctx.ps != nil: + ctx.ps.withPacketWriter w: + w.swrite(pcFillRect) + w.swrite(x1) + w.swrite(y1) + w.swrite(x2) + w.swrite(y2) + w.swrite(color) + +proc strokeRect(ctx: CanvasRenderingContext2D; x1, y1, x2, y2: int; + color: ARGBColor) = + if ctx.ps != nil: + ctx.ps.withPacketWriter w: + w.swrite(pcStrokeRect) + w.swrite(x1) + w.swrite(y1) + w.swrite(x2) + w.swrite(y2) + w.swrite(color) + +proc fillPath(ctx: CanvasRenderingContext2D; path: Path; color: ARGBColor; + fillRule: CanvasFillRule) = + if ctx.ps != nil: + let lines = path.getLineSegments() + ctx.ps.withPacketWriter w: + w.swrite(pcFillPath) + w.swrite(lines) + w.swrite(color) + w.swrite(fillRule) + +proc strokePath(ctx: CanvasRenderingContext2D; path: Path; color: ARGBColor) = + if ctx.ps != nil: + var lines: seq[Line] = @[] + for line in path.lines: + lines.add(line) + ctx.ps.withPacketWriter w: + w.swrite(pcStrokePath) + w.swrite(lines) + w.swrite(color) + +proc fillText(ctx: CanvasRenderingContext2D; text: string; x, y: float64; + color: ARGBColor; align: CSSTextAlign) = + if ctx.ps != nil: + ctx.ps.withPacketWriter w: + w.swrite(pcFillText) + w.swrite(text) + w.swrite(x) + w.swrite(y) + w.swrite(color) + w.swrite(align) + +proc strokeText(ctx: CanvasRenderingContext2D; text: string; x, y: float64; + color: ARGBColor; align: CSSTextAlign) = + if ctx.ps != nil: + ctx.ps.withPacketWriter w: + w.swrite(pcStrokeText) + w.swrite(text) + w.swrite(x) + w.swrite(y) + w.swrite(color) + w.swrite(align) + +proc clearRect(ctx: CanvasRenderingContext2D; x1, y1, x2, y2: int) = + ctx.fillRect(0, 0, ctx.bitmap.width, ctx.bitmap.height, rgba(0, 0, 0, 0)) + +proc clear(ctx: CanvasRenderingContext2D) = + ctx.clearRect(0, 0, ctx.bitmap.width, ctx.bitmap.height) # CanvasState proc save(ctx: CanvasRenderingContext2D) {.jsfunc.} = @@ -481,10 +633,9 @@ proc restore(ctx: CanvasRenderingContext2D) {.jsfunc.} = ctx.state = ctx.stateStack.pop() proc reset(ctx: CanvasRenderingContext2D) {.jsfunc.} = - ctx.bitmap.clear() - #TODO empty list of subpaths + ctx.clear() ctx.stateStack.setLen(0) - ctx.state.resetState() + ctx.state.reset() # CanvasTransform #TODO scale @@ -569,11 +720,11 @@ proc clearRect(ctx: CanvasRenderingContext2D; x, y, w, h: float64) {.jsfunc.} = #TODO clipping regions (right now we just clip to default) let bw = float64(ctx.bitmap.width) let bh = float64(ctx.bitmap.height) - let x0 = uint64(min(max(x, 0), bw)) - let x1 = uint64(min(max(x + w, 0), bw)) - let y0 = uint64(min(max(y, 0), bh)) - let y1 = uint64(min(max(y + h, 0), bh)) - ctx.bitmap.clearRect(x0, x1, y0, y1) + let x1 = int(min(max(x, 0), bw)) + let y1 = int(min(max(y, 0), bh)) + let x2 = int(min(max(x + w, 0), bw)) + let y2 = int(min(max(y + h, 0), bh)) + ctx.clearRect(x1, y1, x2, y2) proc fillRect(ctx: CanvasRenderingContext2D; x, y, w, h: float64) {.jsfunc.} = for v in [x, y, w, h]: @@ -584,11 +735,11 @@ proc fillRect(ctx: CanvasRenderingContext2D; x, y, w, h: float64) {.jsfunc.} = return let bw = float64(ctx.bitmap.width) let bh = float64(ctx.bitmap.height) - let x0 = uint64(min(max(x, 0), bw)) - let x1 = uint64(min(max(x + w, 0), bw)) - let y0 = uint64(min(max(y, 0), bh)) - let y1 = uint64(min(max(y + h, 0), bh)) - ctx.bitmap.fillRect(x0, x1, y0, y1, ctx.state.fillStyle) + let x1 = int(min(max(x, 0), bw)) + let y1 = int(min(max(y, 0), bh)) + let x2 = int(min(max(x + w, 0), bw)) + let y2 = int(min(max(y + h, 0), bh)) + ctx.fillRect(x1, y1, x2, y2, ctx.state.fillStyle) proc strokeRect(ctx: CanvasRenderingContext2D; x, y, w, h: float64) {.jsfunc.} = for v in [x, y, w, h]: @@ -599,11 +750,11 @@ proc strokeRect(ctx: CanvasRenderingContext2D; x, y, w, h: float64) {.jsfunc.} = return let bw = float64(ctx.bitmap.width) let bh = float64(ctx.bitmap.height) - let x0 = uint64(min(max(x, 0), bw)) - let x1 = uint64(min(max(x + w, 0), bw)) - let y0 = uint64(min(max(y, 0), bh)) - let y1 = uint64(min(max(y + h, 0), bh)) - ctx.bitmap.strokeRect(x0, x1, y0, y1, ctx.state.strokeStyle) + let x1 = int(min(max(x, 0), bw)) + let y1 = int(min(max(y, 0), bh)) + let x2 = int(min(max(x + w, 0), bw)) + let y2 = int(min(max(y + h, 0), bh)) + ctx.strokeRect(x1, y1, x2, y2, ctx.state.strokeStyle) # CanvasDrawPath proc beginPath(ctx: CanvasRenderingContext2D) {.jsfunc.} = @@ -612,11 +763,11 @@ proc beginPath(ctx: CanvasRenderingContext2D) {.jsfunc.} = proc fill(ctx: CanvasRenderingContext2D; fillRule = cfrNonZero) {.jsfunc.} = #TODO path ctx.state.path.tempClosePath() - ctx.bitmap.fillPath(ctx.state.path, ctx.state.fillStyle, fillRule) + ctx.fillPath(ctx.state.path, ctx.state.fillStyle, fillRule) ctx.state.path.tempOpenPath() proc stroke(ctx: CanvasRenderingContext2D) {.jsfunc.} = #TODO path - ctx.bitmap.strokePath(ctx.state.path, ctx.state.strokeStyle) + ctx.strokePath(ctx.state.path, ctx.state.strokeStyle) proc clip(ctx: CanvasRenderingContext2D; fillRule = cfrNonZero) {.jsfunc.} = #TODO path @@ -627,44 +778,14 @@ proc clip(ctx: CanvasRenderingContext2D; fillRule = cfrNonZero) {.jsfunc.} = # CanvasUserInterface # CanvasText -const unifont = readFile"res/unifont_jp-15.0.05.png" -proc loadUnifont(window: Window) = - #TODO this is very wrong, we should move the unifont file in a CGI script or - # something - if unifontBitmap != nil: - return - let request = newRequest( - newURL("img-codec+png:decode").get, - httpMethod = hmPost, - body = RequestBody(t: rbtString, s: unifont) - ) - let response = window.loader.doRequest(request) - assert response.res == 0 - let dims = response.headers.table["Cha-Image-Dimensions"][0] - let width = parseUInt64(dims.until('x'), allowSign = false).get - let height = parseUInt64(dims.after('x'), allowSign = false).get - let len = int(width) * int(height) - let bitmap = ImageBitmap( - px: cast[seq[RGBAColorBE]](newSeqUninitialized[uint32](len)), - width: width, - height: height - ) - window.loader.resume(response.outputId) - response.body.recvDataLoop(addr bitmap.px[0], len * 4) - response.body.sclose() - unifontBitmap = bitmap - #TODO maxwidth proc fillText(ctx: CanvasRenderingContext2D; text: string; x, y: float64) {.jsfunc.} = for v in [x, y]: if classify(v) in {fcInf, fcNegInf, fcNan}: return - #TODO should not be loaded here... - ctx.canvas.internalDocument.window.loadUnifont() let vec = ctx.transform(Vector2D(x: x, y: y)) - ctx.bitmap.fillText(text, vec.x, vec.y, ctx.state.fillStyle, - ctx.state.textAlign) + ctx.fillText(text, vec.x, vec.y, ctx.state.fillStyle, ctx.state.textAlign) #TODO maxwidth proc strokeText(ctx: CanvasRenderingContext2D; text: string; x, y: float64) @@ -672,11 +793,8 @@ proc strokeText(ctx: CanvasRenderingContext2D; text: string; x, y: float64) for v in [x, y]: if classify(v) in {fcInf, fcNegInf, fcNan}: return - #TODO should not be loaded here... - ctx.canvas.internalDocument.window.loadUnifont() let vec = ctx.transform(Vector2D(x: x, y: y)) - ctx.bitmap.strokeText(text, vec.x, vec.y, ctx.state.strokeStyle, - ctx.state.textAlign) + ctx.strokeText(text, vec.x, vec.y, ctx.state.strokeStyle, ctx.state.textAlign) proc measureText(ctx: CanvasRenderingContext2D; text: string): TextMetrics {.jsfunc.} = @@ -894,47 +1012,6 @@ const ReflectTable0 = [ makef("onclick", AllTagTypes, "click"), ] -# Forward declarations -func attr*(element: Element; s: StaticAtom): string -func attrb*(element: Element; s: CAtom): bool -proc attr*(element: Element; name: CAtom; value: string) -proc attr*(element: Element; name: StaticAtom; value: string) -func baseURL*(document: Document): URL -proc delAttr(element: Element; i: int; keep = false) -proc reflectAttr(element: Element; name: CAtom; value: Option[string]) - -# Forward declaration hacks -# set in css/cascade -var appliesFwdDecl*: proc(mqlist: MediaQueryList; window: Window): bool - {.nimcall, noSideEffect.} -# set in css/match -var doqsa*: proc (node: Node; q: string): seq[Element] {.nimcall.} = nil -var doqs*: proc (node: Node; q: string): Element {.nimcall.} = nil -# set in html/chadombuilder -var domParseHTMLFragment*: proc(element: Element; s: string): seq[Node] - {.nimcall.} -# set in html/env -var windowFetch*: proc(window: Window; input: JSValue; - init = RequestInit(window: JS_UNDEFINED)): JSResult[FetchPromise] - {.nimcall.} = nil - -# For now, these are the same; on an API level however, getGlobal is guaranteed -# to be non-null, while getWindow may return null in the future. (This is in -# preparation for Worker support.) -func getGlobal*(ctx: JSContext): Window = - let global = JS_GetGlobalObject(ctx) - var window: Window - assert ctx.fromJS(global, window).isSome - JS_FreeValue(ctx, global) - return window - -func getWindow*(ctx: JSContext): Window = - let global = JS_GetGlobalObject(ctx) - var window: Window - assert ctx.fromJS(global, window).isSome - JS_FreeValue(ctx, global) - return window - func document*(node: Node): Document = if node of Document: return Document(node) @@ -2763,7 +2840,15 @@ proc newHTMLElement*(document: Document; localName: CAtom; of TAG_LABEL: result = HTMLLabelElement() of TAG_CANVAS: - let bitmap = if document.scriptingEnabled: newBitmap(300, 150) else: nil + let bitmap = if document.scriptingEnabled: + NetworkBitmap( + contentType: "image/x-cha-canvas", + imageId: document.window.getImageId(), + width: 300, + height: 150 + ) + else: + nil result = HTMLCanvasElement(bitmap: bitmap) of TAG_IMG: result = HTMLImageElement() @@ -3034,7 +3119,10 @@ proc loadResource(window: Window; link: HTMLLinkElement) = let res = res.get if res.getContentType() == "text/css": return res.text() - res.unregisterFun() + res.close() + let p = newPromise[JSResult[string]]() + p.resolve(JSResult[string].err(res.error)) + return p ).then(proc(s: JSResult[string]) = if s.isSome: #TODO non-utf-8 css? @@ -3071,7 +3159,8 @@ proc loadResource(window: Window; image: HTMLImageElement) = return let cachedURL = CachedURLImage(expiry: -1, loading: true) window.imageURLCache[surl] = cachedURL - let p = window.loader.fetch(newRequest(url)).then( + let headers = newHeaders({"Accept": "*/*"}) + let p = window.loader.fetch(newRequest(url, headers = headers)).then( proc(res: JSResult[Response]): EmptyPromise = if res.isNone: return newResolvedPromise() @@ -3092,8 +3181,7 @@ proc loadResource(window: Window; image: HTMLImageElement) = ) let r = window.loader.fetch(request) response.resume() - response.unregisterFun() - response.body.sclose() + response.close() var expiry = -1i64 if "Cache-Control" in response.headers: for hdr in response.headers.table["Cache-Control"]: @@ -3113,8 +3201,7 @@ proc loadResource(window: Window; image: HTMLImageElement) = let response = res.get # close immediately; all data we're interested in is in the headers. response.resume() - response.unregisterFun() - response.body.sclose() + response.close() if "Cha-Image-Dimensions" notin response.headers.table: window.console.error("Cha-Image-Dimensions missing in", $response.url) @@ -3122,12 +3209,13 @@ proc loadResource(window: Window; image: HTMLImageElement) = let dims = response.headers.table["Cha-Image-Dimensions"][0] let width = parseUInt64(dims.until('x'), allowSign = false) let height = parseUInt64(dims.after('x'), allowSign = false) - if width.isNone or height.isNone: + if width.isNone or height.isNone or width.get > uint64(int.high) or + height.get > uint64(int.high): window.console.error("wrong Cha-Image-Dimensions in", $response.url) return let bmp = NetworkBitmap( - width: width.get, - height: height.get, + width: int(width.get), + height: int(height.get), cacheId: cacheId, imageId: window.getImageId(), contentType: contentType @@ -3244,10 +3332,24 @@ proc reflectAttr(element: Element; name: CAtom; value: Option[string]) = if element.scriptingEnabled and name in {satWidth, satHeight}: let w = element.attrul(satWidth).get(300) let h = element.attrul(satHeight).get(150) - let canvas = HTMLCanvasElement(element) - if canvas.bitmap == nil or canvas.bitmap.width != w or - canvas.bitmap.height != h: - canvas.bitmap = newBitmap(w, h) + if w <= uint64(int.high) and h <= uint64(int.high): + let w = int(w) + let h = int(h) + let canvas = HTMLCanvasElement(element) + if canvas.bitmap == nil or canvas.bitmap.width != w or + canvas.bitmap.height != h: + let window = element.document.window + if canvas.ctx2d != nil and canvas.ctx2d.ps != nil: + let i = window.pendingCanvasCtls.find(canvas.ctx2d) + window.pendingCanvasCtls.del(i) + canvas.ctx2d.ps.sclose() + canvas.ctx2d = nil + canvas.bitmap = NetworkBitmap( + contentType: "image/x-cha-canvas", + imageId: window.getImageId(), + width: w, + height: h + ) of TAG_IMG: let image = HTMLImageElement(element) # https://html.spec.whatwg.org/multipage/images.html#relevant-mutations @@ -4457,24 +4559,21 @@ func getElementReflectFunctions(): seq[TabGetSet] = proc getContext*(jctx: JSContext; this: HTMLCanvasElement; contextId: string; options = JS_UNDEFINED): RenderingContext {.jsfunc.} = if contextId == "2d": - if this.ctx2d != nil: - return this.ctx2d - return create2DContext(jctx, this, options) + if this.ctx2d == nil: + create2DContext(jctx, this, options) + return this.ctx2d return nil # Note: the standard says quality should be converted in a strange way for # backwards compat, but I don't care. proc toBlob(ctx: JSContext; this: HTMLCanvasElement; callback: JSValue; contentType = "image/png"; quality = none(float64)) {.jsfunc.} = - if not contentType.startsWith("image/"): + if not contentType.startsWith("image/") or this.bitmap.cacheId == 0: return - let url = newURL("img-codec+" & contentType.after('/') & ":encode") - if url.isNone: + let url0 = newURL("img-codec+" & contentType.after('/') & ":encode") + if url0.isNone: return - #TODO this is dumb (and slow) - var s = newString(this.bitmap.px.len * 4) - if s.len > 0: - copyMem(addr s[0], addr this.bitmap.px[0], s.len) + let url = url0.get let headers = newHeaders({ "Cha-Image-Dimensions": $this.bitmap.width & 'x' & $this.bitmap.height }) @@ -4482,17 +4581,38 @@ proc toBlob(ctx: JSContext; this: HTMLCanvasElement; callback: JSValue; quality *= 99 quality += 1 headers.add("Cha-Image-Quality", $quality) - let request = newRequest( - url.get, - httpMethod = hmPost, - headers = headers, - body = RequestBody(t: rbtString, s: s) - ) # callback will go out of scope when we return, so capture a new reference. let callback = JS_DupValue(ctx, callback) let window = this.document.window + let loader = window.loader let contentType = contentType.toLowerAscii() - window.loader.fetch(request).then(proc(res: JSResult[Response]) = + let cacheReq = newRequest(newURL("cache:" & $this.bitmap.cacheId).get) + loader.fetch(cacheReq).then(proc(res: JSResult[Response]): FetchPromise = + if res.isNone: + return newResolvedPromise(res) + let res = res.get + let p = loader.fetch(newRequest( + newURL("img-codec+x-cha-canvas:decode").get, + httpMethod = hmPost, + body = RequestBody(t: rbtOutput, outputId: res.outputId) + )) + res.resume() + res.close() + return p + ).then(proc(res: JSResult[Response]): FetchPromise = + if res.isNone: + return newResolvedPromise(res) + let res = res.get + let p = loader.fetch(newRequest( + url, + httpMethod = hmPost, + headers = headers, + body = RequestBody(t: rbtOutput, outputId: res.outputId) + )) + res.resume() + res.close() + return p + ).then(proc(res: JSResult[Response]) = if res.isNone: if contentType != "image/png": # redo as PNG. diff --git a/src/img/bitmap.nim b/src/img/bitmap.nim index c5df19fc..343e9ecf 100644 --- a/src/img/bitmap.nim +++ b/src/img/bitmap.nim @@ -3,8 +3,8 @@ import types/color type Bitmap* = ref object of RootObj px*: seq[RGBAColorBE] - width*: uint64 - height*: uint64 + width*: int + height*: int ImageBitmap* = ref object of Bitmap @@ -13,27 +13,27 @@ type imageId*: int contentType*: string -proc newBitmap*(width, height: uint64): ImageBitmap = +proc newBitmap*(width, height: int): ImageBitmap = return ImageBitmap( px: newSeq[RGBAColorBE](width * height), width: width, height: height ) -proc setpx*(bmp: Bitmap; x, y: uint64; color: RGBAColorBE) {.inline.} = +proc setpx*(bmp: Bitmap; x, y: int; color: RGBAColorBE) {.inline.} = bmp.px[bmp.width * y + x] = color -proc setpx*(bmp: Bitmap; x, y: uint64; color: ARGBColor) {.inline.} = +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: uint64): RGBAColorBE {.inline.} = +proc getpx*(bmp: Bitmap; x, y: int): RGBAColorBE {.inline.} = return bmp.px[bmp.width * y + x] -proc setpxb*(bmp: Bitmap; x, y: uint64; c: RGBAColorBE) {.inline.} = +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: uint64; c: ARGBColor) {.inline.} = +proc setpxb*(bmp: Bitmap; x, y: int; c: ARGBColor) {.inline.} = bmp.setpxb(x, y, rgba_be(c.r, c.g, c.b, c.a)) diff --git a/src/img/painter.nim b/src/img/painter.nim index 7b844447..0479bd77 100644 --- a/src/img/painter.nim +++ b/src/img/painter.nim @@ -12,64 +12,67 @@ type CanvasFillRule* = enum cfrNonZero = "nonzero" cfrEvenOdd = "evenodd" +type PaintCommand* = enum + pcSetDimensions, pcFillRect, pcStrokeRect, pcFillPath, pcStrokePath, + pcFillText, pcStrokeText + # https://en.wikipedia.org/wiki/Bresenham's_line_algorithm#All_cases -proc plotLineLow(bmp: Bitmap; x0, y0, x1, y1: int64; color: ARGBColor) = - var dx = x1 - x0 - var dy = y1 - y0 +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 = y0; - for x in x0 ..< x1: - if x < 0 or y < 0 or uint64(x) >= bmp.width or uint64(y) >= bmp.height: + 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(uint64(x), uint64(y), color) + bmp.setpxb(x, y, color) if D > 0: y = y + yi; D = D - 2 * dx; D = D + 2 * dy; -proc plotLineHigh(bmp: Bitmap; x0, y0, x1, y1: int64; color: ARGBColor) = - var dx = x1 - x0 - var dy = y1 - y0 +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 = x0 - for y in y0 ..< y1: - if x < 0 or y < 0 or uint64(x) >= bmp.width or uint64(y) >= bmp.height: + 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(uint64(x), uint64(y), color) + bmp.setpxb(x, y, color) if D > 0: x = x + xi D = D - 2 * dy D = D + 2 * dx -#TODO should be uint64... -proc plotLine(bmp: Bitmap; x0, y0, x1, y1: int64; color: ARGBColor) = - if abs(y1 - y0) < abs(x1 - x0): - if x0 > x1: - bmp.plotLineLow(x1, y1, x0, y0, color) +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(x0, y0, x1, y1, color) + bmp.plotLineLow(x1, y1, x2, y2, color) else: - if y0 > y1: - bmp.plotLineHigh(x1, y1, x0, y0, color) + if y1 > y2: + bmp.plotLineHigh(x2, y2, x1, y1, color) else: - bmp.plotLineHigh(x0, y0, x1, y1, color) + bmp.plotLineHigh(x1, y1, x2, y2, color) proc plotLine(bmp: Bitmap; a, b: Vector2D; color: ARGBColor) = - bmp.plotLine(int64(a.x), int64(a.y), int64(b.x), int64(b.y), color) + 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; path: Path; color: ARGBColor) = - for line in path.lines: +proc strokePath*(bmp: Bitmap; lines: seq[Line]; color: ARGBColor) = + for line in lines: bmp.plotLine(line, color) func isInside(windingNumber: int; fillRule: CanvasFillRule): bool = @@ -77,13 +80,12 @@ func isInside(windingNumber: int; fillRule: CanvasFillRule): bool = of cfrNonZero: windingNumber != 0 of cfrEvenOdd: windingNumber mod 2 == 0 -# Mainly adapted from SerenityOS. -proc fillPath*(bmp: Bitmap; path: Path; color: ARGBColor; +# Algorithm originally from SerenityOS. +proc fillPath*(bmp: Bitmap; lines: PathLines; color: ARGBColor; fillRule: CanvasFillRule) = - let lines = path.getLineSegments() var i = 0 - var ylines: seq[LineSegment] - for y in int64(lines.miny) .. int64(lines.maxy): + 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 @@ -98,14 +100,14 @@ proc fillPath*(bmp: Bitmap; path: Path; color: ARGBColor; for k in 0 ..< ylines.high: let a = ylines[k] let b = ylines[k + 1] - let sx = int64(a.minyx) - let ex = int64(b.minyx) + 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(uint64(x), uint64(y), color) - if int64(a.p0.y) != y and int64(a.p1.y) != y and int64(b.p0.y) != y and - int64(b.p1.y) != y and sx != ex or a.islope * b.islope < 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: @@ -117,26 +119,18 @@ proc fillPath*(bmp: Bitmap; path: Path; color: ARGBColor; if ylines.len > 0: ylines[^1].minyx += ylines[^1].islope -proc fillRect*(bmp: Bitmap; x0, x1, y0, y1: uint64, color: ARGBColor) = - for y in y0 ..< y1: - for x in x0 ..< x1: +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; x0, x1, y0, y1: uint64, color: ARGBColor) = - for x in x0 ..< x1: - bmp.setpxb(x, y0, color) +proc strokeRect*(bmp: Bitmap; x1, y1, x2, y2: int; color: ARGBColor) = + for x in x1 ..< x2: bmp.setpxb(x, y1, color) - for y in y0 ..< y1: - bmp.setpxb(x0, y, color) + bmp.setpxb(x, y2, color) + for y in y1 ..< y2: bmp.setpxb(x1, y, color) - -proc clearRect*(bmp: Bitmap; x0, x1, y0, y1: uint64) = - for y in y0 ..< y1: - for x in x0 ..< x1: - bmp.setpx(x, y, rgba(0, 0, 0, 0)) - -proc clear*(bmp: Bitmap) = - bmp.clearRect(0, bmp.width, 0, bmp.height) + bmp.setpxb(x2, y, color) type GlyphCacheItem = object u: uint32 @@ -152,14 +146,14 @@ proc getCharBmp(u: uint32): Bitmap = if it.u == u: return it.bmp # Unifont glyphs start at x: 32, y: 64, and are of 8x16/16x16 size - let gx = uint64(32 + 16 * (u mod 0x100)) - let gy = uint64(64 + 16 * (u div 0x100)) + 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 ..< 16u64: - for x in 8 ..< 16u64: + for y in 0 ..< 16: + for x in 8 ..< 16: if unifontBitmap.getpx(gx + x, gy + y) != white: fullwidth = true break loop @@ -181,15 +175,15 @@ proc getCharBmp(u: uint32): Bitmap = proc drawBitmap(a, b: Bitmap; p: Vector2D) = for y in 0 ..< b.height: for x in 0 ..< b.width: - let ax = uint64(p.x) + x - let ay = uint64(p.y) + y + 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: CSSTextAlign) = var w = 0f64 - var glyphs: seq[Bitmap] + var glyphs: seq[Bitmap] = @[] for r in text.runes: let glyph = getCharBmp(uint32(r)) glyphs.add(glyph) diff --git a/src/img/path.nim b/src/img/path.nim index d411eef6..7572ce20 100644 --- a/src/img/path.nim +++ b/src/img/path.nim @@ -215,7 +215,7 @@ iterator lines*(path: Path): Line {.inline.} = proc getLineSegments*(path: Path): PathLines = if path.subpaths.len == 0: - return + return PathLines() var miny = Inf var maxy = -Inf var segments: seq[LineSegment] diff --git a/src/io/bufreader.nim b/src/io/bufreader.nim index bc448edf..8dc7b0a2 100644 --- a/src/io/bufreader.nim +++ b/src/io/bufreader.nim @@ -34,7 +34,7 @@ proc sread*[T](reader: var BufferedReader; o: var Option[T]) proc sread*[T, E](reader: var BufferedReader; o: var Result[T, E]) proc sread*(reader: var BufferedReader; c: var ARGBColor) {.inline.} proc sread*(reader: var BufferedReader; o: var RequestBody) -proc sread*(reader: var BufferedReader; bmp: var Bitmap) +proc sread*(reader: var BufferedReader; bmp: var NetworkBitmap) proc initReader*(stream: DynStream; len, auxLen: int): BufferedReader = assert len != 0 @@ -215,30 +215,10 @@ proc sread*(reader: var BufferedReader; o: var RequestBody) = of rbtMultipart: reader.sread(o.multipart) of rbtOutput: reader.sread(o.outputId) -proc sread*(reader: var BufferedReader; bmp: var Bitmap) = - var isImageBitmap: bool - var width: uint64 - var height: uint64 - reader.sread(isImageBitmap) - reader.sread(width) - reader.sread(height) - if isImageBitmap: - bmp = ImageBitmap( - width: width, - height: height - ) - reader.sread(bmp.px) - else: - var cacheId: int - var imageId: int - var contentType: string - reader.sread(cacheId) - reader.sread(imageId) - reader.sread(contentType) - bmp = NetworkBitmap( - width: width, - height: height, - cacheId: cacheId, - imageId: imageId, - contentType: contentType - ) +proc sread*(reader: var BufferedReader; bmp: var NetworkBitmap) = + bmp = NetworkBitmap() + reader.sread(bmp.width) + reader.sread(bmp.height) + reader.sread(bmp.cacheId) + reader.sread(bmp.imageId) + reader.sread(bmp.contentType) diff --git a/src/io/bufwriter.nim b/src/io/bufwriter.nim index 241d77cc..ed4cb725 100644 --- a/src/io/bufwriter.nim +++ b/src/io/bufwriter.nim @@ -44,7 +44,7 @@ proc swrite*[T](writer: var BufferedWriter; o: Option[T]) proc swrite*[T, E](writer: var BufferedWriter; o: Result[T, E]) proc swrite*(writer: var BufferedWriter; c: ARGBColor) {.inline.} proc swrite*(writer: var BufferedWriter; o: RequestBody) -proc swrite*(writer: var BufferedWriter; bmp: Bitmap) +proc swrite*(writer: var BufferedWriter; bmp: NetworkBitmap) const InitLen = sizeof(int) * 2 const SizeInit = max(64, InitLen) @@ -199,13 +199,9 @@ proc swrite*(writer: var BufferedWriter; o: RequestBody) = of rbtMultipart: writer.swrite(o.multipart) of rbtOutput: writer.swrite(o.outputId) -proc swrite*(writer: var BufferedWriter; bmp: Bitmap) = - writer.swrite(bmp of ImageBitmap) +proc swrite*(writer: var BufferedWriter; bmp: NetworkBitmap) = writer.swrite(bmp.width) writer.swrite(bmp.height) - if bmp of ImageBitmap: - writer.swrite(bmp.px) - else: - writer.swrite(NetworkBitmap(bmp).cacheId) - writer.swrite(NetworkBitmap(bmp).imageId) - writer.swrite(NetworkBitmap(bmp).contentType) + writer.swrite(bmp.cacheId) + writer.swrite(bmp.imageId) + writer.swrite(bmp.contentType) diff --git a/src/io/promise.nim b/src/io/promise.nim index 55dcfbf0..8fb55a9b 100644 --- a/src/io/promise.nim +++ b/src/io/promise.nim @@ -75,6 +75,11 @@ proc newResolvedPromise*(): EmptyPromise = res.resolve() return res +proc newResolvedPromise*[T](x: T): Promise[T] = + let res = newPromise[T]() + res.resolve(x) + return res + func empty*(map: PromiseMap): bool = map.tab.len == 0 diff --git a/src/layout/box.nim b/src/layout/box.nim index 9812a9cd..cd94ca49 100644 --- a/src/layout/box.nim +++ b/src/layout/box.nim @@ -25,7 +25,7 @@ type of iatInlineBlock: innerbox*: BlockBox of iatImage: - bmp*: Bitmap + bmp*: NetworkBitmap RootInlineFragmentState* = object # offset relative to parent @@ -73,7 +73,7 @@ type of iftNewline: discard of iftBitmap: - bmp*: Bitmap + bmp*: NetworkBitmap of iftBox: box*: BlockBox diff --git a/src/layout/engine.nim b/src/layout/engine.nim index 9d40b9ca..6943958d 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -1439,7 +1439,7 @@ proc addInlineBlock(ictx: var InlineContext; state: var InlineState; ictx.whitespacenum = 0 proc addInlineImage(ictx: var InlineContext; state: var InlineState; - bmp: Bitmap; padding: LayoutUnit) = + bmp: NetworkBitmap; padding: LayoutUnit) = let atom = InlineAtom( t: iatImage, bmp: bmp, diff --git a/src/layout/renderdocument.nim b/src/layout/renderdocument.nim index a407cdfe..2ff8d484 100644 --- a/src/layout/renderdocument.nim +++ b/src/layout/renderdocument.nim @@ -235,7 +235,7 @@ type y*: int width*: int height*: int - bmp*: Bitmap + bmp*: NetworkBitmap AbsolutePos = object offset: Offset diff --git a/src/loader/loader.nim b/src/loader/loader.nim index c40c3cad..e3c36b01 100644 --- a/src/loader/loader.nim +++ b/src/loader/loader.nim @@ -108,7 +108,10 @@ type ClientData = ref object pid: int key: ClientKey + # List of cached resources. cacheMap: seq[CachedItem] + # List of file descriptors passed by the client. + passedFdMap: Table[string, FileHandle] # host -> fd config: LoaderClientConfig LoaderContext = ref object @@ -119,8 +122,6 @@ type handleMap: Table[int, LoaderHandle] outputMap: Table[int, OutputHandle] selector: Selector[int] - # List of file descriptors passed by the pager. - passedFdMap: Table[string, FileHandle] # host -> fd # List of existing clients (buffer or pager) that may make requests. clientData: Table[int, ClientData] # pid -> data # ID of next output. TODO: find a better allocation scheme @@ -356,8 +357,9 @@ proc loadStreamRegular(ctx: LoaderContext; handle, cachedHandle: LoaderHandle) = handle.outputs.setLen(0) handle.iclose() -proc loadStream(ctx: LoaderContext; handle: LoaderHandle; request: Request) = - ctx.passedFdMap.withValue(request.url.pathname, fdp): +proc loadStream(ctx: LoaderContext; client: ClientData; handle: LoaderHandle; + request: Request) = + client.passedFdMap.withValue(request.url.pathname, fdp): handle.sendResult(0) handle.sendStatus(200) handle.sendHeaders(newHeaders()) @@ -365,7 +367,7 @@ proc loadStream(ctx: LoaderContext; handle: LoaderHandle; request: Request) = var stats: Stat doAssert fstat(fdp[], stats) != -1 handle.istream = ps - ctx.passedFdMap.del(request.url.pathname) + client.passedFdMap.del(request.url.pathname) if S_ISCHR(stats.st_mode) or S_ISREG(stats.st_mode): # regular file: e.g. cha <file # or character device: e.g. cha </dev/null @@ -493,7 +495,7 @@ proc loadResource(ctx: LoaderContext; client: ClientData; assert ostream == nil handle.close() elif request.url.scheme == "stream": - ctx.loadStream(handle, request) + ctx.loadStream(client, handle, request) if handle.istream != nil: ctx.addFd(handle) else: @@ -658,11 +660,12 @@ proc shareCachedItem(ctx: LoaderContext; stream: SocketStream; targetClient.cacheMap.add(item) stream.sclose() -proc passFd(ctx: LoaderContext; stream: SocketStream; r: var BufferedReader) = +proc passFd(ctx: LoaderContext; stream: SocketStream; client: ClientData; + r: var BufferedReader) = var id: string r.sread(id) let fd = stream.recvFileHandle() - ctx.passedFdMap[id] = fd + client.passedFdMap[id] = fd stream.sclose() proc removeCachedItem(ctx: LoaderContext; stream: SocketStream; @@ -764,9 +767,6 @@ proc acceptConnection(ctx: LoaderContext) = of lcShareCachedItem: privileged_command ctx.shareCachedItem(stream, r) - of lcPassFd: - privileged_command - ctx.passFd(stream, r) of lcRedirectToFile: privileged_command ctx.redirectToFile(stream, r) @@ -780,6 +780,8 @@ proc acceptConnection(ctx: LoaderContext) = ctx.addCacheFile(stream, client, r) of lcRemoveCachedItem: ctx.removeCachedItem(stream, client, r) + of lcPassFd: + ctx.passFd(stream, client, r) of lcLoad: ctx.load(stream, client, r) of lcTee: diff --git a/src/loader/response.nim b/src/loader/response.nim index d41f62eb..1d7307cd 100644 --- a/src/loader/response.nim +++ b/src/loader/response.nim @@ -3,7 +3,6 @@ import std/tables import chagashi/charset import chagashi/decoder -import img/bitmap import io/dynstream import io/promise import loader/headers @@ -13,7 +12,6 @@ import monoucha/jserror import monoucha/quickjs import monoucha/tojs import types/blob -import types/color import types/opt import types/referrer import types/url @@ -228,46 +226,6 @@ proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} = response.resume() return opaque.bodyRead -type BitmapOpaque = ref object of RootObj - bmp: Bitmap - idx: int - bodyRead: EmptyPromise - -proc onReadBitmap(response: Response) = - let opaque = BitmapOpaque(response.opaque) - let bmp = opaque.bmp - while true: - try: - let p = cast[ptr UncheckedArray[uint8]](addr bmp.px[0]) - let L = bmp.px.len * 4 - opaque.idx - let n = response.body.recvData(addr p[opaque.idx], L) - opaque.idx += n - if n == 0: - break - except ErrorAgain: - break - -proc onFinishBitmap(response: Response; success: bool) = - let opaque = BitmapOpaque(response.opaque) - opaque.bodyRead.resolve() - -proc saveToBitmap*(response: Response; bmp: Bitmap): EmptyPromise = - assert not response.bodyUsed - let opaque = BitmapOpaque(bmp: bmp, idx: 0, bodyRead: EmptyPromise()) - let size = bmp.width * bmp.height - bmp.px = cast[seq[RGBAColorBE]](newSeqUninitialized[uint32](size)) - response.opaque = opaque - if size > 0: - response.onRead = onReadBitmap - response.onFinish = onFinishBitmap - else: - response.unregisterFun() - response.body.sclose() - opaque.bodyRead.resolve() - response.bodyUsed = true - response.resume() - return opaque.bodyRead - proc json(ctx: JSContext; this: Response): Promise[JSValue] {.jsfunc.} = return this.text().then(proc(s: JSResult[string]): JSValue = if s.isNone: diff --git a/src/local/container.nim b/src/local/container.nim index ebfc3940..2c12c4ae 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -2070,7 +2070,7 @@ proc highlightMarks*(container: Container; display: var FixedGrid; func findCachedImage*(container: Container; image: PosBitmap; offx, erry, dispw: int): CachedImage = - let imageId = NetworkBitmap(image.bmp).imageId + let imageId = image.bmp.imageId for it in container.cachedImages: if it.bmp.imageId == imageId and it.width == image.width and it.height == image.height and it.offx == offx and it.erry == erry and diff --git a/src/local/pager.nim b/src/local/pager.nim index 8abaf16f..cb3f751e 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -130,7 +130,6 @@ type forkserver*: ForkServer formRequestMap*: Table[string, FormRequestType] hasload*: bool # has a page been successfully loaded since startup? - imageId: int # hack to allocate a new ID for canvas each frame, TODO remove inputBuffer*: string # currently uninterpreted characters iregex: Result[Regex, string] isearchpromise: EmptyPromise @@ -484,8 +483,7 @@ proc redraw(pager: Pager) {.jsfunc.} = proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap; offx, erry, dispw: int) = - let bmp = NetworkBitmap() - bmp[] = NetworkBitmap(image.bmp)[] + let bmp = image.bmp let request = newRequest(newURL("cache:" & $bmp.cacheId).get) let cachedImage = CachedImage( bmp: bmp, @@ -505,7 +503,7 @@ proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap; return let response = res.get let headers = newHeaders() - if uint64(image.width) != bmp.width or uint64(image.height) != bmp.height: + if image.width != bmp.width or image.height != bmp.height: headers.add("Cha-Image-Target-Dimensions", $image.width & 'x' & $image.height) let request = newRequest( @@ -524,9 +522,6 @@ proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap; pager.loader.removeCachedItem(bmp.cacheId) return let response = res.get - # take target sizes - bmp.width = uint64(image.width) - bmp.height = uint64(image.height) let headers = newHeaders({ "Cha-Image-Dimensions": $image.width & 'x' & $image.height }) @@ -571,36 +566,28 @@ proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap; proc initImages(pager: Pager; container: Container) = var newImages: seq[CanvasImage] = @[] for image in container.images: - 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) - 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, offx, erry, dispw) - continue - bmp0 = cached.bmp - data = cached.data - if not cached.loaded: - continue # loading - else: - imageId = pager.imageId - inc pager.imageId - let canvasImage = pager.term.loadImage(bmp0, data, container.process, + 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 + #TODO this is wrong if term caps sixel width + 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) + let imageId = image.bmp.imageId + if cached == nil: + pager.loadCachedImage(container, image, offx, erry, dispw) + continue + if not cached.loaded: + continue # loading + let canvasImage = pager.term.loadImage(cached.data, container.process, imageId, image.x - container.fromx, image.y - container.fromy, - image.x, image.y, pager.bufWidth, pager.bufHeight, erry, offx, dispw) + image.width, image.height, image.x, image.y, pager.bufWidth, + pager.bufHeight, erry, offx, dispw) if canvasImage != nil: newImages.add(canvasImage) pager.term.clearImages(pager.bufHeight) @@ -1165,7 +1152,7 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset; proxy: pager.config.network.proxy, filter: newURLFilter( scheme = some(url.scheme), - allowschemes = @["data", "cache"], + allowschemes = @["data", "cache", "stream"], default = true ), insecureSSLNoVerify: false diff --git a/src/local/term.nim b/src/local/term.nim index 78c50efc..7cd43e02 100644 --- a/src/local/term.nim +++ b/src/local/term.nim @@ -11,7 +11,6 @@ import chagashi/charset import chagashi/decoder import chagashi/encoder import config/config -import img/bitmap import io/dynstream import js/base64 import types/blob @@ -57,17 +56,24 @@ type CanvasImage* = ref object pid: int imageId: int + # relative position on screen x: int y: int + # original dimensions (after resizing) + width: int + height: int + # offset (crop start) offx: int offy: int + # size cap (crop end) + # Note: this 0-based, so the final display size is + # (dispw - offx, disph - offy) dispw: int disph: int damaged: bool marked*: bool dead: bool kittyId: int - bmp: Bitmap # 0 if kitty erry: int # absolute x, y in container @@ -643,11 +649,11 @@ proc outputGrid*(term: Terminal) = term.cursorx = -1 term.cursory = -1 -func findImage(term: Terminal; pid, imageId: int; bmp: Bitmap; - rx, ry, erry, offx, dispw: int): CanvasImage = +func findImage(term: Terminal; pid, imageId: int; rx, ry, width, height, + erry, offx, dispw: int): CanvasImage = for it in term.canvasImages: if not it.dead and it.pid == pid and it.imageId == imageId and - it.bmp.width == bmp.width and it.bmp.height == bmp.height and + it.width == width and it.height == height and it.rx == rx and it.ry == ry and (term.imageMode != imSixel or it.erry == erry and it.dispw == dispw and it.offx == offx): @@ -669,8 +675,8 @@ proc positionImage(term: Terminal; image: CanvasImage; x, y, maxw, maxh: int): # origin (*not* offx/offy) let maxwpx = maxw * term.attrs.ppc let maxhpx = maxh * term.attrs.ppl - var width = int(image.bmp.width) - var height = int(image.bmp.height) + var width = image.width + var height = image.height if term.imageMode == imSixel: #TODO a better solution would be to split up the image here so that it # still gets fully displayed on the screen, or at least downscale it... @@ -686,7 +692,7 @@ proc clearImage*(term: Terminal; image: CanvasImage; maxh: int) = of imNone: discard of imSixel: # we must clear sixels the same way as we clear text. - let ey = min(image.y + int(image.bmp.height), maxh) + let ey = min(image.y + image.height, maxh) let x = max(image.x, 0) for y in max(image.y, 0) ..< ey: term.lineDamage[y] = min(x, term.lineDamage[y]) @@ -699,10 +705,10 @@ proc clearImages*(term: Terminal; maxh: int) = term.clearImage(image, maxh) image.marked = false -proc loadImage*(term: Terminal; bmp: Bitmap; data: Blob; pid, imageId, - 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): +proc loadImage*(term: Terminal; data: Blob; pid, imageId, x, y, width, height, + rx, ry, maxw, maxh, erry, offx, dispw: int): CanvasImage = + if (let image = term.findImage(pid, imageId, rx, ry, width, height, 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 @@ -714,7 +720,7 @@ proc loadImage*(term: Terminal; bmp: Bitmap; data: Blob; pid, imageId, return nil elif term.imageMode == imSixel: # check if any line of our image is damaged - let ey = min(image.y + int(image.bmp.height), maxh) + let ey = min(image.y + image.height, maxh) let mx = (image.offx + image.dispw) div term.attrs.ppc for y in max(image.y, 0) ..< ey: if term.lineDamage[y] < mx: @@ -726,12 +732,13 @@ proc loadImage*(term: Terminal; bmp: Bitmap; data: Blob; pid, imageId, return image # new image let image = CanvasImage( - bmp: bmp, pid: pid, imageId: imageId, data: data, rx: rx, ry: ry, + width: width, + height: height, erry: erry ) if term.positionImage(image, x, y, maxw, maxh): @@ -751,7 +758,6 @@ proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage; let offy = image.offy let dispw = image.dispw let disph = image.disph - let bmp = image.bmp var outs = term.cursorGoto(x, y) outs &= DCSSTART & 'q' # set raster attributes @@ -768,11 +774,11 @@ proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage; 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 realh == int(bmp.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)) - if disph == int(bmp.height): # crop top only + if disph == image.height: # crop top only term.write(data.toOpenArray(si, L - 1)) else: # crop both top & bottom let ed6 = (disph - image.erry) div 6 @@ -803,7 +809,7 @@ proc outputSixelImage(term: Terminal; x, y: int; image: CanvasImage) = proc outputKittyImage(term: Terminal; x, y: int; image: CanvasImage) = var outs = term.cursorGoto(x, y) & - APC & "GC=1,s=" & $image.bmp.width & ",v=" & $image.bmp.height & + APC & "GC=1,s=" & $image.width & ",v=" & $image.height & ",x=" & $image.offx & ",y=" & $image.offy & ",w=" & $(image.dispw - image.offx) & ",h=" & $(image.disph - image.offy) & diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 9370f90b..3f0b3a71 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -1134,6 +1134,10 @@ proc onload(buffer: Buffer) = buffer.document.readyState = rsComplete if buffer.config.scripting: buffer.dispatchLoadEvent() + for ctx in buffer.window.pendingCanvasCtls: + ctx.ps.sclose() + ctx.ps = nil + buffer.window.pendingCanvasCtls.setLen(0) if buffer.hasTask(bcGetTitle): buffer.resolveTask(bcGetTitle, buffer.document.title) if buffer.hasTask(bcLoad): |