about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/css/cascade.nim5
-rw-r--r--src/html/dom.nim60
-rw-r--r--src/img/bitmap.nim3
-rw-r--r--src/io/bufreader.nim11
-rw-r--r--src/io/bufwriter.nim3
-rw-r--r--src/layout/engine.nim30
-rw-r--r--src/layout/renderdocument.nim4
-rw-r--r--src/loader/loader.nim96
-rw-r--r--src/loader/loaderhandle.nim19
-rw-r--r--src/loader/response.nim13
-rw-r--r--src/local/pager.nim45
-rw-r--r--src/server/buffer.nim1
-rw-r--r--src/utils/sandbox.nim2
13 files changed, 174 insertions, 118 deletions
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 78953303..f39a335c 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -570,8 +570,9 @@ proc appendChildren(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
   let elem = Element(styledChild.node)
   styledStack.stackAppend(frame, styledChild, peAfter, idx, parentDeclMap)
   case elem.tagType
-  of TAG_TEXTAREA: styledStack.stackAppend(frame, styledChild, peTextareaText, idx)
-  of TAG_IMG, TAG_IMAGE: styledStack.stackAppend(frame, styledChild, peImage, idx)
+  of TAG_TEXTAREA:
+    styledStack.stackAppend(frame, styledChild, peTextareaText, idx)
+  of TAG_IMG: styledStack.stackAppend(frame, styledChild, peImage, idx)
   of TAG_VIDEO: styledStack.stackAppend(frame, styledChild, peVideo, idx)
   of TAG_AUDIO: styledStack.stackAppend(frame, styledChild, peAudio, idx)
   of TAG_BR: styledStack.stackAppend(frame, styledChild, peNewline, idx)
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 3e5dddd0..503bdf27 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -2911,46 +2911,52 @@ proc loadResource(window: Window; image: HTMLImageElement) =
       # mixed content :/
       #TODO maybe do this in loader?
       url.scheme = "https"
-    let p = window.loader.fetch(newRequest(url))
-      .then(proc(res: JSResult[Response]): Promise[JSResult[Response]] =
+    let p = window.loader.fetch(newRequest(url)).then(
+      proc(res: JSResult[Response]): EmptyPromise =
         if res.isNone:
           return
         let response = res.get
         let contentType = response.getContentType("image/x-unknown")
         if contentType.until('/') != "image":
           return
+        let cacheId = window.loader.addCacheFile(response.outputId,
+          window.loader.clientPid)
         let request = newRequest(
           newURL("img-codec+" & contentType.after('/') & ":decode").get,
           httpMethod = hmPost,
-          body = RequestBody(t: rbtOutput, outputId: response.outputId)
+          headers = newHeaders({"Cha-Image-Info-Only": "1"}),
+          body = RequestBody(t: rbtOutput, outputId: response.outputId),
         )
         let r = window.loader.fetch(request)
-        window.loader.resume(response.outputId)
+        response.resume()
         response.unregisterFun()
         response.body.sclose()
-        return r
-      ).then(proc(res: JSResult[Response]): EmptyPromise =
-        if res.isNone:
-          return
-        let response = res.get
-        # we can close immediately; loader will not clean this output up until
-        # the `resume' command in pager.
-        response.unregisterFun()
-        response.body.sclose()
-        if "Cha-Image-Dimensions" notin response.headers.table:
-          window.console.error("Cha-Image-Dimensions missing in", $response.url)
-          return
-        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:
-          window.console.error("wrong Cha-Image-Dimensions in", $response.url)
-          return
-        image.bitmap = NetworkBitmap(
-          width: width.get,
-          height: height.get,
-          outputId: response.outputId,
-          imageId: window.getImageId()
+        return r.then(proc(res: JSResult[Response]): EmptyPromise =
+          if res.isNone:
+            window.console.error("Failed to decode", $response.url)
+            return
+          let response = res.get
+          # close immediately; all data we're interested in is in the headers.
+          response.resume()
+          response.unregisterFun()
+          response.body.sclose()
+          if "Cha-Image-Dimensions" notin response.headers.table:
+            window.console.error("Cha-Image-Dimensions missing in",
+              $response.url)
+            return
+          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:
+            window.console.error("wrong Cha-Image-Dimensions in", $response.url)
+            return
+          image.bitmap = NetworkBitmap(
+            width: width.get,
+            height: height.get,
+            cacheId: cacheId,
+            imageId: window.getImageId(),
+            contentType: contentType
+          )
         )
       )
     window.loadingResourcePromises.add(p)
diff --git a/src/img/bitmap.nim b/src/img/bitmap.nim
index 70244643..c5df19fc 100644
--- a/src/img/bitmap.nim
+++ b/src/img/bitmap.nim
@@ -9,8 +9,9 @@ type
   ImageBitmap* = ref object of Bitmap
 
   NetworkBitmap* = ref object of Bitmap
-    outputId*: int
+    cacheId*: int
     imageId*: int
+    contentType*: string
 
 proc newBitmap*(width, height: uint64): ImageBitmap =
   return ImageBitmap(
diff --git a/src/io/bufreader.nim b/src/io/bufreader.nim
index bd2486be..5f522666 100644
--- a/src/io/bufreader.nim
+++ b/src/io/bufreader.nim
@@ -230,13 +230,16 @@ proc sread*(reader: var BufferedReader; bmp: var Bitmap) =
     )
     reader.sread(bmp.px)
   else:
-    var outputId: int
+    var cacheId: int
     var imageId: int
-    reader.sread(outputId)
+    var contentType: string
+    reader.sread(cacheId)
     reader.sread(imageId)
+    reader.sread(contentType)
     bmp = NetworkBitmap(
       width: width,
       height: height,
-      outputId: outputId,
-      imageId: imageId
+      cacheId: cacheId,
+      imageId: imageId,
+      contentType: contentType
     )
diff --git a/src/io/bufwriter.nim b/src/io/bufwriter.nim
index 56e30f5b..8166eab8 100644
--- a/src/io/bufwriter.nim
+++ b/src/io/bufwriter.nim
@@ -208,5 +208,6 @@ proc swrite*(writer: var BufferedWriter; bmp: Bitmap) =
   if bmp of ImageBitmap:
     writer.swrite(bmp.px)
   else:
-    writer.swrite(NetworkBitmap(bmp).outputId)
+    writer.swrite(NetworkBitmap(bmp).cacheId)
     writer.swrite(NetworkBitmap(bmp).imageId)
+    writer.swrite(NetworkBitmap(bmp).contentType)
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index a764faad..3acc958e 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -1370,7 +1370,7 @@ proc addInlineBlock(ictx: var InlineContext; state: var InlineState;
   ictx.whitespacenum = 0
 
 proc addInlineImage(ictx: var InlineContext; state: var InlineState;
-    bmp: Bitmap) =
+    bmp: Bitmap; padding: LayoutUnit) =
   let h = int(bmp.height).toLayoutUnit().ceilTo(ictx.cellHeight)
   let iastate = InlineAtomState(
     vertalign: state.fragment.computed{"vertical-align"},
@@ -1381,6 +1381,20 @@ proc addInlineImage(ictx: var InlineContext; state: var InlineState;
     bmp: bmp,
     size: size(w = int(bmp.width), h = h), #TODO overflow
   )
+  let computed = state.fragment.computed
+  let lctx = ictx.lctx
+  if computed{"width"}.canpx(ictx.space.w):
+    let w = computed{"width"}.spx(lctx, ictx.space.w, computed, padding)
+    if not computed{"height"}.canpx(ictx.space.h):
+      # maintain aspect ratio
+      atom.size.h = atom.size.h div atom.size.w * w
+    atom.size.w = w
+  if computed{"height"}.canpx(ictx.space.h):
+    let h = computed{"height"}.spx(lctx, ictx.space.h, computed, padding)
+    if not computed{"width"}.canpx(ictx.space.w):
+      # maintain aspect ratio
+      atom.size.w = atom.size.w div atom.size.h * h
+    atom.size.h = h
   discard ictx.addAtom(state, iastate, atom)
 
 func calcLineHeight(computed: CSSComputedValues; lctx: LayoutContext):
@@ -1421,7 +1435,7 @@ proc layoutInline(ictx: var InlineContext; fragment: InlineFragment) =
   case fragment.t
   of iftNewline: ictx.flushLine(state)
   of iftBox: ictx.addInlineBlock(state, fragment.box)
-  of iftBitmap: ictx.addInlineImage(state, fragment.bmp)
+  of iftBitmap: ictx.addInlineImage(state, fragment.bmp, padding.sum())
   of iftText: ictx.layoutText(state, fragment.text)
   of iftParent:
     for child in fragment.children:
@@ -2636,13 +2650,12 @@ proc pushInline(ctx: var InnerBlockContext; fragment: InlineFragment) =
 
 proc pushInlineText(ctx: var InnerBlockContext; computed: CSSComputedValues;
     styledNode: StyledNode; text: string) =
-  let box = InlineFragment(
+  ctx.pushInline(InlineFragment(
     t: iftText,
     computed: computed,
     node: styledNode,
     text: text
-  )
-  ctx.pushInline(box)
+  ))
 
 proc pushInlineBlock(ctx: var InnerBlockContext; styledNode: StyledNode;
     computed: CSSComputedValues) =
@@ -2788,13 +2801,12 @@ proc buildReplacement(ctx: var InnerBlockContext; child, parent: StyledNode;
     ctx.pushInlineText(computed, parent, child.content.s)
   of ContentImage:
     if child.content.bmp != nil:
-      let wrapper = InlineFragment(
+      ctx.pushInline(InlineFragment(
         t: iftBitmap,
-        computed: computed,
+        computed: parent.computed,
         node: parent,
         bmp: child.content.bmp
-      )
-      ctx.pushInline(wrapper)
+      ))
     else:
       ctx.pushInlineText(computed, parent, "[img]")
   of ContentVideo:
diff --git a/src/layout/renderdocument.nim b/src/layout/renderdocument.nim
index e45b8af4..89afde32 100644
--- a/src/layout/renderdocument.nim
+++ b/src/layout/renderdocument.nim
@@ -218,6 +218,8 @@ type
   PosBitmap* = ref object
     x*: int
     y*: int
+    width*: int
+    height*: int
     bmp*: Bitmap
 
   RenderState = object
@@ -379,6 +381,8 @@ proc renderInlineFragment(grid: var FlexibleGrid; state: var RenderState;
         state.images.add(PosBitmap(
           x: (offset.x div state.attrs.ppc).toInt,
           y: (offset.y div state.attrs.ppl).toInt,
+          width: atom.size.w.toInt,
+          height: atom.size.h.toInt,
           bmp: atom.bmp
         ))
   if fragment.computed{"position"} != PositionStatic:
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index 7c76ae59..11cf714f 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -174,6 +174,16 @@ func findCachedHandle(ctx: LoaderContext; cacheId: int): LoaderHandle =
 type PushBufferResult = enum
   pbrDone, pbrUnregister
 
+proc register(ctx: LoaderContext; output: OutputHandle) =
+  assert not output.registered
+  ctx.selector.registerHandle(output.ostream.fd, {Write}, 0)
+  output.registered = true
+
+proc unregister(ctx: LoaderContext; output: OutputHandle) =
+  assert output.registered
+  ctx.selector.unregister(output.ostream.fd)
+  output.registered = false
+
 # Either write data to the target output, or append it to the list of buffers to
 # write and register the output in our selector.
 proc pushBuffer(ctx: LoaderContext; output: OutputHandle; buffer: LoaderBuffer;
@@ -203,8 +213,7 @@ proc pushBuffer(ctx: LoaderContext; output: OutputHandle; buffer: LoaderBuffer;
     if n < buffer.len:
       output.currentBuffer = buffer
       output.currentBufferIdx = n
-      ctx.selector.registerHandle(output.ostream.fd, {Write}, 0)
-      output.registered = true
+      ctx.register(output)
   else:
     output.buffers.addLast(buffer)
   pbrDone
@@ -213,8 +222,11 @@ proc getOutputId(ctx: LoaderContext): int =
   result = ctx.outputNum
   inc ctx.outputNum
 
-proc redirectToStream(ctx: LoaderContext; output: OutputHandle;
-    ps: PosixStream): bool =
+proc redirectToFile(ctx: LoaderContext; output: OutputHandle;
+    targetPath: string): bool =
+  let ps = newPosixStream(targetPath, O_CREAT or O_WRONLY, 0o600)
+  if ps == nil:
+    return false
   try:
     if output.currentBuffer != nil:
       let n = ps.sendData(output.currentBuffer, output.currentBufferIdx)
@@ -227,7 +239,7 @@ proc redirectToStream(ctx: LoaderContext; output: OutputHandle;
         ps.sclose()
         return false
   except ErrorBrokenPipe:
-    # ps or output is dead; give up.
+    # ps is dead; give up.
     ps.sclose()
     return false
   if output.istreamAtEnd:
@@ -241,13 +253,6 @@ proc redirectToStream(ctx: LoaderContext; output: OutputHandle;
     ))
   return true
 
-proc redirectToFile(ctx: LoaderContext; output: OutputHandle;
-    targetPath: string): bool =
-  let ps = newPosixStream(targetPath, O_CREAT or O_WRONLY, 0o600)
-  if ps == nil:
-    return false
-  return ctx.redirectToStream(output, ps)
-
 proc addCacheFile(ctx: LoaderContext; client: ClientData; output: OutputHandle):
     int =
   if output.parent != nil and output.parent.cacheId != -1:
@@ -326,8 +331,7 @@ proc loadStreamRegular(ctx: LoaderContext; handle, cachedHandle: LoaderHandle) =
     output.parent = nil
     let i = handle.outputs.find(output)
     if output.registered:
-      ctx.selector.unregister(output.ostream.fd)
-      output.registered = false
+      ctx.unregister(output)
     handle.outputs.del(i)
   for output in handle.outputs:
     if r == hrrBrokenPipe:
@@ -397,8 +401,8 @@ proc loadFromCache(ctx: LoaderContext; client: ClientData; handle: LoaderHandle;
   else:
     handle.sendResult(ERROR_URL_NOT_IN_CACHE)
 
-proc loadResource(ctx: LoaderContext; client: ClientData; config: LoaderClientConfig;
-    request: Request; handle: LoaderHandle) =
+proc loadResource(ctx: LoaderContext; client: ClientData;
+    config: LoaderClientConfig; request: Request; handle: LoaderHandle) =
   var redo = true
   var tries = 0
   var prevurl: URL = nil
@@ -419,16 +423,18 @@ proc loadResource(ctx: LoaderContext; client: ClientData; config: LoaderClientCo
         config.insecureSSLNoVerify, ostream)
       if handle.istream != nil:
         if ostream != nil:
-          let output = ctx.findOutput(request.body.outputId, client)
-          if output != nil:
-            if not ctx.redirectToStream(output, ostream):
-              # give up.
-              handle.rejectHandle(ERROR_FAILED_TO_REDIRECT)
-              return
+          let outputIn = ctx.findOutput(request.body.outputId, client)
+          if outputIn != nil:
+            ostream.setBlocking(false)
+            let output = outputIn.tee(ostream, ctx.getOutputId(), client.pid)
+            ctx.outputMap[ostream.fd] = output
+            output.suspended = false
+            ctx.register(output)
           else:
             ostream.sclose()
         ctx.addFd(handle)
       else:
+        assert ostream == nil
         handle.close()
     elif request.url.scheme == "stream":
       ctx.loadStream(handle, request)
@@ -538,16 +544,15 @@ proc removeClient(ctx: LoaderContext; stream: SocketStream;
     ctx.clientData.del(pid)
   stream.sclose()
 
-proc addCacheFile(ctx: LoaderContext; stream: SocketStream;
+proc addCacheFile(ctx: LoaderContext; stream: SocketStream; client: ClientData;
     r: var BufferedReader) =
   var outputId: int
   var targetPid: int
-  var sourcePid: int
   r.sread(outputId)
+  #TODO get rid of targetPid
   r.sread(targetPid)
-  r.sread(sourcePid)
-  let sourceClient = ctx.clientData[sourcePid]
-  let output = ctx.findOutput(outputId, sourceClient)
+  doAssert ctx.isPrivileged(client) or client.pid == targetPid
+  let output = ctx.findOutput(outputId, client)
   assert output != nil
   let targetClient = ctx.clientData[targetPid]
   let id = ctx.addCacheFile(targetClient, output)
@@ -613,10 +618,11 @@ proc tee(ctx: LoaderContext; stream: SocketStream; client: ClientData;
   var targetPid: int
   r.sread(sourceId)
   r.sread(targetPid)
-  let output = ctx.findOutput(sourceId, client)
-  if output != nil:
+  let outputIn = ctx.findOutput(sourceId, client)
+  if outputIn != nil:
     let id = ctx.getOutputId()
-    output.tee(stream, id, targetPid)
+    let output = outputIn.tee(stream, id, targetPid)
+    ctx.outputMap[output.ostream.fd] = output
     stream.withPacketWriter w:
       w.swrite(id)
     stream.setBlocking(false)
@@ -635,8 +641,7 @@ proc suspend(ctx: LoaderContext; stream: SocketStream; client: ClientData;
       output.suspended = true
       if output.registered:
         # do not waste cycles trying to push into output
-        output.registered = false
-        ctx.selector.unregister(output.ostream.fd)
+        ctx.unregister(output)
 
 proc resume(ctx: LoaderContext; stream: SocketStream; client: ClientData;
     r: var BufferedReader) =
@@ -646,9 +651,7 @@ proc resume(ctx: LoaderContext; stream: SocketStream; client: ClientData;
     let output = ctx.findOutput(id, client)
     if output != nil:
       output.suspended = false
-      assert not output.registered
-      output.registered = true
-      ctx.selector.registerHandle(output.ostream.fd, {Write}, 0)
+      ctx.register(output)
 
 proc equalsConstantTime(a, b: ClientKey): bool =
   static:
@@ -690,9 +693,6 @@ proc acceptConnection(ctx: LoaderContext) =
       of lcRemoveClient:
         privileged_command
         ctx.removeClient(stream, r)
-      of lcAddCacheFile:
-        privileged_command
-        ctx.addCacheFile(stream, r)
       of lcShareCachedItem:
         privileged_command
         ctx.shareCachedItem(stream, r)
@@ -708,6 +708,8 @@ proc acceptConnection(ctx: LoaderContext) =
       of lcGetCacheFile:
         privileged_command
         ctx.getCacheFile(stream, client, r)
+      of lcAddCacheFile:
+        ctx.addCacheFile(stream, client, r)
       of lcRemoveCachedItem:
         ctx.removeCachedItem(stream, client, r)
       of lcLoad:
@@ -812,8 +814,7 @@ proc handleWrite(ctx: LoaderContext; output: OutputHandle;
       unregWrite.add(output)
     else:
       # all buffers sent, no need to select on this output again for now
-      output.registered = false
-      ctx.selector.unregister(output.ostream.fd)
+      ctx.unregister(output)
 
 proc finishCycle(ctx: LoaderContext; unregRead: var seq[LoaderHandle];
     unregWrite: var seq[OutputHandle]) =
@@ -835,7 +836,7 @@ proc finishCycle(ctx: LoaderContext; unregRead: var seq[LoaderHandle];
   for output in unregWrite:
     if output.ostream != nil:
       if output.registered:
-        ctx.selector.unregister(output.ostream.fd)
+        ctx.unregister(output)
       ctx.outputMap.del(output.ostream.fd)
       output.oclose()
       let handle = output.parent
@@ -973,20 +974,14 @@ proc tee*(loader: FileLoader; sourceId, targetPid: int): (SocketStream, int) =
   r.sread(outputId)
   return (stream, outputId)
 
-# sourcePid is the PID of the output's owner. This is used in pager for images,
-# so that we can be sure that a container only loads images on the page that
-# it owns.
-proc addCacheFile*(loader: FileLoader; outputId, targetPid: int;
-    sourcePid = -1): int =
+proc addCacheFile*(loader: FileLoader; outputId, targetPid: int): int =
   let stream = loader.connect()
   if stream == nil:
     return -1
-  let sourcePid = if sourcePid == -1: loader.clientPid else: sourcePid
   stream.withLoaderPacketWriter loader, w:
     w.swrite(lcAddCacheFile)
     w.swrite(outputId)
     w.swrite(targetPid)
-    w.swrite(sourcePid)
   var r = stream.initPacketReader()
   var outputId: int
   r.sread(outputId)
@@ -1098,12 +1093,13 @@ proc doRequest*(loader: FileLoader; request: Request): Response =
     stream.sclose()
   return response
 
-proc shareCachedItem*(loader: FileLoader; id, targetPid: int) =
+proc shareCachedItem*(loader: FileLoader; id, targetPid: int; sourcePid = -1) =
   let stream = loader.connect()
   if stream != nil:
+    let sourcePid = if sourcePid != -1: sourcePid else: loader.clientPid
     stream.withLoaderPacketWriter loader, w:
       w.swrite(lcShareCachedItem)
-      w.swrite(loader.clientPid)
+      w.swrite(sourcePid)
       w.swrite(targetPid)
       w.swrite(id)
     stream.sclose()
diff --git a/src/loader/loaderhandle.nim b/src/loader/loaderhandle.nim
index 13869c8f..b1b04dee 100644
--- a/src/loader/loaderhandle.nim
+++ b/src/loader/loaderhandle.nim
@@ -108,10 +108,11 @@ proc bufferCleared*(output: OutputHandle) =
   else:
     output.currentBuffer = nil
 
-proc tee*(outputIn: OutputHandle; ostream: PosixStream; outputId, pid: int) =
-  let parent = outputIn.parent
-  parent.outputs.add(OutputHandle(
-    parent: parent,
+proc tee*(outputIn: OutputHandle; ostream: PosixStream; outputId, pid: int):
+    OutputHandle =
+  assert outputIn.suspended
+  let output = OutputHandle(
+    parent: outputIn.parent,
     ostream: ostream,
     currentBuffer: outputIn.currentBuffer,
     currentBufferIdx: outputIn.currentBufferIdx,
@@ -120,7 +121,13 @@ proc tee*(outputIn: OutputHandle; ostream: PosixStream; outputId, pid: int) =
     outputId: outputId,
     ownerPid: pid,
     suspended: outputIn.suspended
-  ))
+  )
+  when defined(debug):
+    output.url = outputIn.url
+  if outputIn.parent != nil:
+    assert outputIn.parent.parser == nil
+    outputIn.parent.outputs.add(output)
+  return output
 
 template output*(handle: LoaderHandle): OutputHandle =
   handle.outputs[0]
@@ -194,6 +201,6 @@ proc oclose*(output: OutputHandle) =
 proc close*(handle: LoaderHandle) =
   handle.iclose()
   for output in handle.outputs:
-    #TODO assert not output.registered
+    assert not output.registered
     if output.ostream != nil:
       output.oclose()
diff --git a/src/loader/response.nim b/src/loader/response.nim
index ab74571e..b8d95e36 100644
--- a/src/loader/response.nim
+++ b/src/loader/response.nim
@@ -129,6 +129,10 @@ proc onReadText(response: Response) =
       opaque.buf.setLen(olen)
       break
 
+proc resume*(response: Response) =
+  response.resumeFun(response.outputId)
+  response.resumeFun = nil
+
 proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} =
   if response.body == nil:
     let p = newPromise[JSResult[string]]()
@@ -144,8 +148,7 @@ proc text*(response: Response): Promise[JSResult[string]] {.jsfunc.} =
   response.opaque = opaque
   response.onRead = onReadText
   response.bodyUsed = true
-  response.resumeFun(response.outputId)
-  response.resumeFun = nil
+  response.resume()
   return response.bodyRead.then(proc(): JSResult[string] =
     let charset = response.getCharset(CHARSET_UTF_8)
     ok(opaque.buf.decodeAll(charset))
@@ -183,8 +186,7 @@ proc blob*(response: Response): Promise[JSResult[Blob]] {.jsfunc.} =
   response.opaque = opaque
   response.onRead = onReadBlob
   response.bodyUsed = true
-  response.resumeFun(response.outputId)
-  response.resumeFun = nil
+  response.resume()
   let contentType = response.getContentType()
   return response.bodyRead.then(proc(): JSResult[Blob] =
     let p = realloc(opaque.p, opaque.len)
@@ -220,8 +222,7 @@ proc saveToBitmap*(response: Response; bmp: Bitmap): EmptyPromise =
   response.opaque = opaque
   response.onRead = onReadBitmap
   response.bodyUsed = true
-  response.resumeFun(response.outputId)
-  response.resumeFun = nil
+  response.resume()
   return response.bodyRead
 
 proc json(ctx: JSContext; this: Response): Promise[JSResult[JSValue]]
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 643e7689..cd0df216 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -21,6 +21,7 @@ import io/socketstream
 import io/stdio
 import io/tempfile
 import io/urlfilter
+import layout/renderdocument
 import loader/connecterror
 import loader/headers
 import loader/loader
@@ -472,7 +473,7 @@ proc redraw(pager: Pager) {.jsfunc.} =
     if pager.container.select != nil:
       pager.container.select.redraw = true
 
-proc loadCachedImage(pager: Pager; container: Container; bmp: NetworkBitmap) =
+proc loadCachedImage(pager: Pager; container: Container; image: PosBitmap) =
   #TODO this is kinda dumb, because we cannot unload cached images.
   # ideally the filesystem cache should serve as the only cache, but right
   # now it's just sort of a temporary place before the image is dumped to
@@ -481,22 +482,42 @@ proc loadCachedImage(pager: Pager; container: Container; bmp: NetworkBitmap) =
   # load start" event in container, and then add one in the pager?
   # the first option seems better; it's simpler, and buffers can add arbitrary
   # cache files if they just tell the pager it's an image anyway.
-  let cacheId = pager.loader.addCacheFile(bmp.outputId,
-    pager.loader.clientPid, container.process)
-  let request = newRequest(newURL("cache:" & $cacheId).get)
+  let bmp = NetworkBitmap(image.bmp)
+  let request = newRequest(newURL("cache:" & $bmp.cacheId).get)
   let cachedImage = CachedImage(bmp: bmp)
-  pager.loader.fetch(request).then(proc(res: JSResult[Response]): EmptyPromise =
+  pager.loader.shareCachedItem(bmp.cacheId, pager.loader.clientPid,
+    container.process)
+  pager.loader.fetch(request).then(proc(res: JSResult[Response]):
+      Promise[JSResult[Response]] =
     if res.isNone:
-      let i = container.cachedImages.find(cachedImage)
-      container.cachedImages.del(i)
-      return nil
-    return res.get.saveToBitmap(bmp)
+      return
+    let response = res.get
+    let headers = newHeaders()
+    if uint64(image.width) != bmp.width or uint64(image.height) != bmp.height:
+      headers.add("Cha-Image-Target-Dimensions", $image.width & 'x' &
+        $image.height)
+    let request = newRequest(
+      newURL("img-codec+" & bmp.contentType.after('/') & ":decode").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()
+    return r
+  ).then(proc(res: JSResult[Response]): EmptyPromise =
+    let response = res.get
+    # take target sizes
+    bmp.width = uint64(image.width)
+    bmp.height = uint64(image.height)
+    return response.saveToBitmap(bmp)
   ).then(proc() =
     container.redraw = true
     cachedImage.loaded = true
-    pager.loader.removeCachedItem(cacheId)
+    pager.loader.removeCachedItem(bmp.cacheId)
   )
-  pager.loader.resume(bmp.outputId) # get rid of dangling output
   container.cachedImages.add(cachedImage)
 
 proc initImages(pager: Pager; container: Container) =
@@ -509,7 +530,7 @@ proc initImages(pager: Pager; container: Container) =
       let cached = container.findCachedImage(bmp.imageId)
       imageId = bmp.imageId
       if cached == nil:
-        pager.loadCachedImage(container, bmp)
+        pager.loadCachedImage(container, image)
         continue
       image.bmp = cached.bmp
       if not cached.loaded:
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index df0cb0ae..f14cda60 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -1022,6 +1022,7 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} =
     let c = ps.sreadChar()
     assert c == char(0)
     ps.sclose()
+    #TODO share cached images with new buffer
     buffer.loader.resume(ids)
     return pid
 
diff --git a/src/utils/sandbox.nim b/src/utils/sandbox.nim
index ce9b194e..d4312a49 100644
--- a/src/utils/sandbox.nim
+++ b/src/utils/sandbox.nim
@@ -89,6 +89,7 @@ elif defined(linux) and not disableSandbox:
       "getrlimit", # glibc uses it after fork it seems
       "getsockname", # Nim needs it for connecting
       "gettimeofday", # used by QuickJS in Date.now()
+      "lseek", # glibc calls lseek on open files at exit
       "mmap", # memory allocation
       "mmap2", # memory allocation
       "mremap", # memory allocation
@@ -131,6 +132,7 @@ elif defined(linux) and not disableSandbox:
     const allowList = [
       cstring"close", "exit_group", # duh
       "read", "write", "recv", "send", "recvfrom", "sendto", # socket i/o
+      "lseek", # glibc calls lseek on open files at exit
       "fcntl", "fcntl64", # so we can set nonblock etc.
       "mmap", "mmap2", "mremap", "munmap", "brk", # memory allocation
       "poll", # curl needs poll