about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--lib/endians2.nim186
-rw-r--r--res/unifont_jp-15.0.05.pngbin0 -> 895326 bytes
-rw-r--r--src/bindings/curl.nim2
-rw-r--r--src/bindings/zlib.nim79
-rw-r--r--src/buffer/buffer.nim110
-rw-r--r--src/config/toml.nim4
-rw-r--r--src/css/cascade.nim12
-rw-r--r--src/css/cssparser.nim8
-rw-r--r--src/css/values.nim25
-rw-r--r--src/data/charset.nim5
-rw-r--r--src/display/client.nim6
-rw-r--r--src/html/dom.nim557
-rw-r--r--src/html/env.nim17
-rw-r--r--src/img/bitmap.nim620
-rw-r--r--src/img/path.nim430
-rw-r--r--src/io/http.nim18
-rw-r--r--src/io/request.nim53
-rw-r--r--src/ips/serialize.nim89
-rw-r--r--src/js/intl.nim46
-rw-r--r--src/js/javascript.nim256
-rw-r--r--src/types/blob.nim61
-rw-r--r--src/types/color.nim103
-rw-r--r--src/types/cookie.nim4
-rw-r--r--src/types/formdata.nim18
-rw-r--r--src/types/line.nim66
-rw-r--r--src/types/matrix.nim46
-rw-r--r--src/types/mime.nim6
-rw-r--r--src/types/url.nim10
-rw-r--r--src/types/vector.nim53
-rw-r--r--src/utils/twtstr.nim94
-rw-r--r--src/xhr/formdata.nim159
31 files changed, 2743 insertions, 400 deletions
diff --git a/lib/endians2.nim b/lib/endians2.nim
new file mode 100644
index 00000000..c9a39144
--- /dev/null
+++ b/lib/endians2.nim
@@ -0,0 +1,186 @@
+# Copyright (c) 2018-2019 Status Research & Development GmbH
+# Licensed and distributed under either of
+#   * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
+#   * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
+# at your option. This file may not be copied, modified, or distributed except according to those terms.
+
+# Endian conversion operations for unsigned integers, suitable for serializing
+# and deserializing data. The operations are only defined for unsigned
+# integers - if you wish to encode signed integers, convert / cast them to
+# unsigned first!
+#
+# Although it would be possible to enforce correctness with endians in the type
+# (`BigEndian[uin64]`) this seems like overkill. That said, some
+# static analysis tools allow you to annotate fields with endianness - perhaps
+# an idea for the future, akin to `TaintedString`?
+#
+# Keeping the above in mind, it's generally safer to use `array[N, byte]` to
+# hold values of specific endianness and read them out with `fromBytes` when the
+# integer interpretation of the bytes is needed.
+
+{.push raises: [].}
+
+type
+  SomeEndianInt* = uint8|uint16|uint32|uint64
+    ## types that we support endian conversions for - uint8 is there for
+    ## for syntactic / generic convenience. Other candidates:
+    ## * int/uint - uncertain size, thus less suitable for binary interop
+    ## * intX - over and underflow protection in nim might easily cause issues -
+    ##          need to consider before adding here
+
+const
+  useBuiltins = not defined(noIntrinsicsEndians)
+
+when (defined(gcc) or defined(llvm_gcc) or defined(clang)) and useBuiltins:
+  func swapBytesBuiltin(x: uint8): uint8 = x
+  func swapBytesBuiltin(x: uint16): uint16 {.
+      importc: "__builtin_bswap16", nodecl.}
+
+  func swapBytesBuiltin(x: uint32): uint32 {.
+      importc: "__builtin_bswap32", nodecl.}
+
+  func swapBytesBuiltin(x: uint64): uint64 {.
+      importc: "__builtin_bswap64", nodecl.}
+
+elif defined(icc) and useBuiltins:
+  func swapBytesBuiltin(x: uint8): uint8 = x
+  func swapBytesBuiltin(a: uint16): uint16 {.importc: "_bswap16", nodecl.}
+  func swapBytesBuiltin(a: uint32): uint32 {.importc: "_bswap", nodec.}
+  func swapBytesBuiltin(a: uint64): uint64 {.importc: "_bswap64", nodecl.}
+
+elif defined(vcc) and useBuiltins:
+  func swapBytesBuiltin(x: uint8): uint8 = x
+  func swapBytesBuiltin(a: uint16): uint16 {.
+      importc: "_byteswap_ushort", cdecl, header: "<intrin.h>".}
+
+  func swapBytesBuiltin(a: uint32): uint32 {.
+      importc: "_byteswap_ulong", cdecl, header: "<intrin.h>".}
+
+  func swapBytesBuiltin(a: uint64): uint64 {.
+      importc: "_byteswap_uint64", cdecl, header: "<intrin.h>".}
+
+func swapBytesNim(x: uint8): uint8 = x
+func swapBytesNim(x: uint16): uint16 = (x shl 8) or (x shr 8)
+
+func swapBytesNim(x: uint32): uint32 =
+  let v = (x shl 16) or (x shr 16)
+
+  ((v shl 8) and 0xff00ff00'u32) or ((v shr 8) and 0x00ff00ff'u32)
+
+func swapBytesNim(x: uint64): uint64 =
+  var v = (x shl 32) or (x shr 32)
+  v =
+    ((v and 0x0000ffff0000ffff'u64) shl 16) or
+    ((v and 0xffff0000ffff0000'u64) shr 16)
+
+  ((v and 0x00ff00ff00ff00ff'u64) shl 8) or
+    ((v and 0xff00ff00ff00ff00'u64) shr 8)
+
+func swapBytes*[T: SomeEndianInt](x: T): T {.inline.} =
+  ## Reverse the bytes within an integer, such that the most significant byte
+  ## changes place with the least significant one, etc
+  ##
+  ## Example:
+  ## doAssert swapBytes(0x01234567'u32) == 0x67452301
+  when nimvm:
+    swapBytesNim(x)
+  else:
+    when declared(swapBytesBuiltin):
+      swapBytesBuiltin(x)
+    else:
+      swapBytesNim(x)
+
+func toBytes*(x: SomeEndianInt, endian: Endianness = system.cpuEndian):
+    array[sizeof(x), byte] {.noinit, inline.} =
+  ## Convert integer to its corresponding byte sequence using the chosen
+  ## endianness. By default, native endianness is used which is not portable!
+  let v =
+    if endian == system.cpuEndian: x
+    else: swapBytes(x)
+
+  when nimvm: # No copyMem in vm
+    for i in 0..<sizeof(result):
+      result[i] = byte((v shr (i * 8)) and 0xff)
+  else:
+    copyMem(addr result, unsafeAddr v, sizeof(result))
+
+func toBytesLE*(x: SomeEndianInt):
+    array[sizeof(x), byte] {.inline.} =
+  ## Convert a native endian integer to a little endian byte sequence
+  toBytes(x, littleEndian)
+
+func toBytesBE*(x: SomeEndianInt):
+    array[sizeof(x), byte] {.inline.} =
+  ## Convert a native endian integer to a native endian byte sequence
+  toBytes(x, bigEndian)
+
+func fromBytes*(
+    T: typedesc[SomeEndianInt],
+    x: openArray[byte],
+    endian: Endianness = system.cpuEndian): T {.inline.} =
+  ## Read bytes and convert to an integer according to the given endianness.
+  ##
+  ## Note: The default value of `system.cpuEndian` is not portable across
+  ## machines.
+  ##
+  ## Panics when `x.len < sizeof(T)` - for shorter buffers, copy the data to
+  ## an `array` first using `arrayops.initCopyFrom`, taking care to zero-fill
+  ## at the right end - usually the beginning for big endian and the end for
+  ## little endian, but this depends on the serialization of the bytes.
+
+  # This check gets optimized away when the compiler can prove that the length
+  # is large enough - passing in an `array` or using a construct like
+  # ` toOpenArray(pos, pos + sizeof(T) - 1)` are two ways that this happens
+  doAssert x.len >= sizeof(T), "Not enough bytes for endian conversion"
+
+  when nimvm: # No copyMem in vm
+    for i in 0..<sizeof(result):
+      result = result or (T(x[i]) shl (i * 8))
+  else:
+    # `copyMem` helps compilers optimize the copy into a single instruction, when
+    # alignment etc permits
+    copyMem(addr result, unsafeAddr x[0], sizeof(result))
+
+  if endian != system.cpuEndian:
+    # The swap is turned into a CPU-specific instruction and/or combined with
+    # the copy above, again when conditions permit it - for example, on X86
+    # fromBytesBE gets compiled into a single `MOVBE` instruction
+    result = swapBytes(result)
+
+func fromBytesBE*(
+    T: typedesc[SomeEndianInt],
+    x: openArray[byte]): T {.inline.} =
+  ## Read big endian bytes and convert to an integer. At runtime, v must contain
+  ## at least sizeof(T) bytes. By default, native endianness is used which is
+  ## not portable!
+  fromBytes(T, x, bigEndian)
+
+func toBE*[T: SomeEndianInt](x: T): T {.inline.} =
+  ## Convert a native endian value to big endian. Consider toBytesBE instead
+  ## which may prevent some confusion.
+  if cpuEndian == bigEndian: x
+  else: x.swapBytes
+
+func fromBE*[T: SomeEndianInt](x: T): T {.inline.} =
+  ## Read a big endian value and return the corresponding native endian
+  # there's no difference between this and toBE, except when reading the code
+  toBE(x)
+
+func fromBytesLE*(
+    T: typedesc[SomeEndianInt],
+    x: openArray[byte]): T {.inline.} =
+  ## Read little endian bytes and convert to an integer. At runtime, v must
+  ## contain at least sizeof(T) bytes. By default, native endianness is used
+  ## which is not portable!
+  fromBytes(T, x, littleEndian)
+
+func toLE*[T: SomeEndianInt](x: T): T {.inline.} =
+  ## Convert a native endian value to little endian. Consider toBytesLE instead
+  ## which may prevent some confusion.
+  if cpuEndian == littleEndian: x
+  else: x.swapBytes
+
+func fromLE*[T: SomeEndianInt](x: T): T {.inline.} =
+  ## Read a little endian value and return the corresponding native endian
+  # there's no difference between this and toLE, except when reading the code
+  toLE(x)
diff --git a/res/unifont_jp-15.0.05.png b/res/unifont_jp-15.0.05.png
new file mode 100644
index 00000000..d0e2a92d
--- /dev/null
+++ b/res/unifont_jp-15.0.05.png
Binary files differdiff --git a/src/bindings/curl.nim b/src/bindings/curl.nim
index ca31e4f2..169314fd 100644
--- a/src/bindings/curl.nim
+++ b/src/bindings/curl.nim
@@ -303,7 +303,7 @@ proc curl_mime_init*(handle: CURL): curl_mime
 proc curl_mime_free*(mime: curl_mime)
 proc curl_mime_addpart*(mime: curl_mime): curl_mimepart
 proc curl_mime_name*(part: curl_mimepart, name: cstring)
-proc curl_mime_data*(part: curl_mimepart, data: cstring, datasize: csize_t)
+proc curl_mime_data*(part: curl_mimepart, data: pointer, datasize: csize_t)
 proc curl_mime_filename*(part: curl_mimepart, name: cstring)
 proc curl_mime_filedata*(part: curl_mimepart, filename: cstring)
 
diff --git a/src/bindings/zlib.nim b/src/bindings/zlib.nim
new file mode 100644
index 00000000..00acb1ca
--- /dev/null
+++ b/src/bindings/zlib.nim
@@ -0,0 +1,79 @@
+const zlib = (func(): string =
+  let res = staticExec("pkg-config --libs --silence-errors zlib")
+  if res != "":
+    return res
+)()
+when zlib == "":
+  error("zlib not found")
+
+{.passL: zlib.}
+
+const
+  Z_NO_FLUSH* = cint(0)
+  Z_PARTIAL_FLUSH* = cint(1)
+  Z_SYNC_FLUSH* = cint(2)
+  Z_FULL_FLUSH* = cint(3)
+  Z_FINISH* = cint(4)
+  Z_BLOCK* = cint(5)
+  Z_TREES* = cint(6)
+
+const
+  Z_OK* = cint(0)
+  Z_STREAM_END* = cint(1)
+  Z_NEED_DICT* = cint(2)
+  Z_ERRNO* = cint(-1)
+  Z_STREAM_ERROR* = cint(-2)
+  Z_DATA_ERROR* = cint(-3)
+  Z_MEM_ERROR* = cint(-4)
+  Z_BUF_ERROR* = cint(-5)
+  Z_VERSION_ERROR* = cint(-6)
+
+const
+  Z_BINARY* = cint(0)
+  Z_TEXT* = cint(1)
+  Z_ASCII* = Z_TEXT
+  Z_UNKNOWN* = cint(2)
+
+type
+  alloc_func* {.importc, header: "zlib.h".} = proc (opaque: pointer,
+    items: cuint, size: cuint): pointer {.cdecl.}
+
+  free_func* {.importc, header: "zlib.h".} = proc (opaque: pointer,
+    address: pointer) {.cdecl.}
+
+  internal_state* {.importc, header: "zlib.h".} = object
+
+  z_stream* {.importc, header: "zlib.h".} = object
+    next_in*: ptr uint8 # next input byte
+    avail_in*: cuint # number of bytes available in next_in
+    total_in*: culong # total number of input bytes read so far
+
+    next_out*: ptr uint8 # next output byte will go here
+    avail_out*: cuint # remaining free space at next_out
+    total_out*: culong # total number of bytes output so far
+
+    msg*: cstring # last error message, NULL if no error
+    state*: ptr internal_state # not visible by applications
+
+    zalloc*: alloc_func # used to allocate the internal state
+    zfree*: free_func # used to free the internal state 
+    opaque*: pointer # private data object passed to zalloc and zfree 
+
+    data_type*: cint # best guess about the data type: binary or text
+                     # for deflate, or the decoding state for inflate 
+    adler*: culong # Adler-32 or CRC-32 value of the uncompressed data 
+    reserved*: culong # reserved for future use 
+
+  z_streamp* = ptr z_stream
+
+{.push header: "zlib.h", importc, cdecl.}
+proc inflateInit*(strm: z_streamp): cint
+proc inflate*(strm: z_streamp, flush: cint): cint
+proc inflateEnd*(strm: z_streamp): cint
+proc compress*(dest: ptr uint8, destLen: ptr culong, source: ptr uint8,
+  sourceLen: culong): cint
+proc compressBound*(sourceLen: culong): culong
+proc uncompress*(dest: ptr uint8, destLen: ptr culong, source: ptr uint8,
+  sourceLen: culong): cint
+proc crc32*(crc: culong, buf: ptr uint8, len: cuint): culong
+{.pop.}
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 7fda2887..d668a01d 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -40,9 +40,11 @@ import render/rendertext
 import types/buffersource
 import types/color
 import types/cookie
+import types/formdata
 import types/referer
 import types/url
 import utils/twtstr
+import xhr/formdata as formdata_impl
 
 type
   LoadInfo* = enum
@@ -751,82 +753,19 @@ proc cancel*(buffer: Buffer): int {.proxy.} =
     buffer.do_reshape()
   return buffer.lines.len
 
-# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set
-proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): seq[tuple[name, value: string]] =
-  if form.constructingentrylist:
-    return
-  form.constructingentrylist = true
-
-  var entrylist: seq[tuple[name, value: string]]
-  for field in form.controls:
-    if field.findAncestor({TAG_DATALIST}) != nil or
-        field.attrb("disabled") or
-        field.isButton() and Element(field) != submitter:
-      continue
-
-    if field.tagType == TAG_INPUT:
-      let field = HTMLInputElement(field)
-      if field.inputType == INPUT_IMAGE:
-        let name = if field.attr("name") != "":
-          field.attr("name") & '.'
-        else:
-          ""
-        entrylist.add((name & 'x', $field.xcoord))
-        entrylist.add((name & 'y', $field.ycoord))
-        continue
-
-    #TODO custom elements
-
-    let name = field.attr("name")
-
-    if name == "":
-      continue
-
-    if field.tagType == TAG_SELECT:
-      let field = HTMLSelectElement(field)
-      for option in field.options:
-        if option.selected or option.disabled:
-          entrylist.add((name, option.value))
-    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}:
-      let value = if field.attr("value") != "":
-        field.attr("value")
-      else:
-        "on"
-      entrylist.add((name, value))
-    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE:
-      #TODO file
-      discard
-    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"):
-      let charset = if encoding != "":
-        encoding
-      else:
-        "UTF-8"
-      entrylist.add((name, charset))
-    else:
-      case field.tagType
-      of TAG_INPUT:
-        entrylist.add((name, HTMLInputElement(field).value))
-      of TAG_BUTTON:
-        entrylist.add((name, HTMLButtonElement(field).value))
-      of TAG_TEXTAREA:
-        entrylist.add((name, HTMLTextAreaElement(field).value))
-      else: assert false, "Tag type " & $field.tagType & " not accounted for in constructEntryList"
-    if field.tagType == TAG_TEXTAREA or
-        field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}:
-      if field.attr("dirname") != "":
-        let dirname = field.attr("dirname")
-        let dir = "ltr" #TODO bidi
-        entrylist.add((dirname, dir))
-
-  form.constructingentrylist = false
-  return entrylist
-
 #https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm
-proc serializeMultipartFormData(kvs: seq[(string, string)]): MimeData =
-  for it in kvs:
-    let name = makeCRLF(it[0])
-    let value = makeCRLF(it[1])
-    result[name] = value
+proc serializeMultipartFormData(entries: seq[FormDataEntry]): FormData =
+  {.cast(noSideEffect).}:
+    # This is correct, because newFormData with no params has no side effects.
+    let formData = newFormData()
+    for entry in entries:
+      let name = makeCRLF(entry.name)
+      if entry.isstr:
+        let value = makeCRLF(entry.svalue)
+        formData.append(name, value)
+      else:
+        formData.append(name, entry.value, entry.filename)
+    return formData
 
 proc serializePlainTextFormData(kvs: seq[(string, string)]): string =
   for it in kvs:
@@ -837,7 +776,9 @@ proc serializePlainTextFormData(kvs: seq[(string, string)]): string =
     result &= "\r\n"
 
 func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] =
-  let entrylist = form.constructEntryList(submitter)
+  if form.constructingEntryList:
+    return
+  let entrylist = form.constructEntryList(submitter).get(@[])
 
   let action = if submitter.action() == "":
     $form.document.url
@@ -868,25 +809,30 @@ func submitForm(form: HTMLFormElement, submitter: Element): Option[Request] =
   #let noopener = true #TODO
 
   template mutateActionUrl() =
-    let query = serializeApplicationXWWWFormUrlEncoded(entrylist)
+    let kvlist = entrylist.toNameValuePairs()
+    let query = serializeApplicationXWWWFormUrlEncoded(kvlist)
     parsedaction.query = query.some
     return newRequest(parsedaction, httpmethod).some
 
   template submitAsEntityBody() =
     var mimetype: string
     var body = none(string)
-    var multipart = none(MimeData)
+    var multipart = none(FormData)
     case enctype
     of FORM_ENCODING_TYPE_URLENCODED:
-      body = serializeApplicationXWWWFormUrlEncoded(entrylist).some
+      let kvlist = entrylist.toNameValuePairs()
+      body = some(serializeApplicationXWWWFormUrlEncoded(kvlist))
       mimeType = $enctype
     of FORM_ENCODING_TYPE_MULTIPART:
-      multipart = serializeMultipartFormData(entrylist).some
+      multipart = some(serializeMultipartFormData(entrylist))
       mimetype = $enctype
     of FORM_ENCODING_TYPE_TEXT_PLAIN:
-      body = serializePlainTextFormData(entrylist).some
+      let kvlist = entrylist.toNameValuePairs()
+      body = some(serializePlainTextFormData(kvlist))
       mimetype = $enctype
-    return newRequest(parsedaction, httpmethod, @{"Content-Type": mimetype}, body).some #TODO multipart
+    let req = newRequest(parsedaction, httpmethod,
+      @{"Content-Type": mimetype}, body)
+    return some(req) #TODO multipart
 
   template getActionUrl() =
     return newRequest(parsedaction).some
diff --git a/src/config/toml.nim b/src/config/toml.nim
index c34e46a7..05a202ab 100644
--- a/src/config/toml.nim
+++ b/src/config/toml.nim
@@ -1,5 +1,6 @@
 import streams
 import tables
+import options
 import times
 import strutils
 import strformat
@@ -367,6 +368,7 @@ proc consumeNumber(state: var TomlParser, c: char): TomlValue =
 
   if state.has(1):
     if state.peek(0) == 'E' or state.peek(0) == 'e':
+      isfloat = true
       var j = 2
       if state.peek(1) == '-' or state.peek(1) == '+':
         inc j
@@ -383,7 +385,7 @@ proc consumeNumber(state: var TomlParser, c: char): TomlValue =
     return TomlValue(vt: VALUE_FLOAT, f: val)
 
   let val = parseInt64(repr)
-  return TomlValue(vt: VALUE_INTEGER, i: val)
+  return TomlValue(vt: VALUE_INTEGER, i: val.get)
 
 proc consumeValue(state: var TomlParser): TomlValue
 
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 0cea87b7..7aac213f 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -127,17 +127,17 @@ func calcPresentationalHints(element: Element): CSSComputedValues =
     if c.isSome:
       set_cv "color", c.get
   template map_colspan =
-    let colspan = element.attrigz("colspan")
+    let colspan = element.attrulgz("colspan")
     if colspan.isSome:
       let i = colspan.get
       if i <= 1000:
-        set_cv "-cha-colspan", i
+        set_cv "-cha-colspan", int(i)
   template map_rowspan =
-    let rowspan = element.attrigez("rowspan")
+    let rowspan = element.attrul("rowspan")
     if rowspan.isSome:
       let i = rowspan.get
       if i <= 65534:
-        set_cv "-cha-rowspan", i
+        set_cv "-cha-rowspan", int(i)
 
   case element.tagType
   of TAG_DIV:
@@ -170,8 +170,8 @@ func calcPresentationalHints(element: Element): CSSComputedValues =
     map_text
   of TAG_TEXTAREA:
     let textarea = HTMLTextAreaElement(element)
-    let cols = textarea.attri("cols").get(20)
-    let rows = textarea.attri("rows").get(1)
+    let cols = textarea.attrul("cols").get(20)
+    let rows = textarea.attrul("rows").get(1)
     set_cv "width", CSSLength(unit: UNIT_CH, num: float64(cols))
     set_cv "height", CSSLength(unit: UNIT_EM, num: float64(rows))
   of TAG_FONT:
diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim
index 9c8328de..57cfc87e 100644
--- a/src/css/cssparser.nim
+++ b/src/css/cssparser.nim
@@ -867,10 +867,10 @@ proc parseAnB*(state: var CSSParseState): Option[CSSAnB] =
       return none(CSSAnB)
   template parse_sub_int(sub: string, skip: int): int =
     let s = sub.substr(skip)
-    for c in s:
-      if c notin AsciiDigit:
-        return none(CSSAnB)
-    parseInt32(s)
+    let x = parseInt32(s)
+    if x.isNone:
+      return none(CSSAnB)
+    x.get
   template fail_non_integer(tok: CSSToken, res: Option[CSSAnB]) =
     if tok.tokenType != CSS_NUMBER_TOKEN:
       state.reconsume()
diff --git a/src/css/values.nim b/src/css/values.nim
index c786e47e..dd3f2df4 100644
--- a/src/css/values.nim
+++ b/src/css/values.nim
@@ -493,7 +493,7 @@ func skipWhitespace(vals: seq[CSSComponentValue], i: var int) =
       break
     inc i
 
-func cssColor(val: CSSComponentValue): RGBAColor =
+func cssColor*(val: CSSComponentValue): RGBAColor =
   if val of CSSToken:
     let tok = CSSToken(val)
     case tok.tokenType
@@ -525,23 +525,15 @@ func cssColor(val: CSSComponentValue): RGBAColor =
           commaMode = true
         elif commaMode:
           raise newException(CSSValueError, "Invalid color")
-        if slash:
+        elif slash:
           if f.value[i] != CSS_DELIM_TOKEN or CSSToken(f.value[i]).rvalue != Rune('/'):
             raise newException(CSSValueError, "Invalid color")
           inc i
           f.value.skipWhitespace(i)
-      check_err
+      if not slash:
+        check_err
     case f.name
-    of "rgb":
-      f.value.skipWhitespace(i)
-      check_err
-      let r = CSSToken(f.value[i]).nvalue
-      next_value true
-      let g = CSSToken(f.value[i]).nvalue
-      next_value
-      let b = CSSToken(f.value[i]).nvalue
-      return rgba(int(r), int(g), int(b), 255)
-    of "rgba":
+    of "rgb", "rgba":
       f.value.skipWhitespace(i)
       check_err
       let r = CSSToken(f.value[i]).nvalue
@@ -550,8 +542,11 @@ func cssColor(val: CSSComponentValue): RGBAColor =
       next_value
       let b = CSSToken(f.value[i]).nvalue
       next_value false, true
-      let a = CSSToken(f.value[i]).nvalue
-      return rgba(int(r), int(g), int(b), int(a))
+      let a = if i < f.value.len:
+        CSSToken(f.value[i]).nvalue
+      else:
+        1
+      return rgba(int(r), int(g), int(b), int(a * 255))
     else: discard
   raise newException(CSSValueError, "Invalid color")
 
diff --git a/src/data/charset.nim b/src/data/charset.nim
index 560e8643..f8f833d5 100644
--- a/src/data/charset.nim
+++ b/src/data/charset.nim
@@ -316,11 +316,6 @@ const CharsetMap = {
   "x-user-defined": CHARSET_X_USER_DEFINED
 }.toTable()
 
-func normalizeLocale(s: string): string =
-  for i in 0 ..< s.len:
-    if cast[uint8](s[i]) > 0x20 and s[i] != '_' and s[i] != '-':
-      result &= s[i].toLowerAscii()
-
 const NormalizedCharsetMap = (func(): Table[string, Charset] =
   for k, v in CharsetMap:
     result[k.normalizeLocale()] = v)()
diff --git a/src/display/client.nim b/src/display/client.nim
index d705eab1..95f036f8 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -31,12 +31,15 @@ import ips/forkserver
 import ips/serialize
 import ips/serversocket
 import ips/socketstream
+import js/intl
 import js/javascript
 import js/module
 import js/timeout
+import types/blob
 import types/cookie
 import types/dispatcher
 import types/url
+import xhr/formdata as formdata_impl
 
 type
   Client* = ref ClientObj
@@ -535,6 +538,9 @@ proc newClient*(config: Config, dispatcher: Dispatcher): Client =
   ctx.addURLModule()
   ctx.addDOMModule()
   ctx.addHTMLModule()
+  ctx.addIntlModule()
+  ctx.addBlobModule()
+  ctx.addFormDataModule()
   ctx.addRequestModule()
   ctx.addLineEditModule()
   ctx.addConfigModule()
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 600639ee..28a77aa7 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -1,22 +1,31 @@
 import deques
 import macros
+import math
 import options
 import sets
 import streams
 import strutils
 import tables
 
+import css/cssparser
 import css/sheet
+import css/values
 import data/charset
 import encoding/decoderstream
 import html/tags
+import img/bitmap
+import img/path
 import io/loader
 import io/request
 import js/javascript
 import js/timeout
+import types/blob
+import types/color
+import types/matrix
 import types/mime
 import types/referer
 import types/url
+import types/vector
 import utils/twtstr
 
 type
@@ -158,7 +167,7 @@ type
     charset*: Charset
     window*: Window
     url*: URL #TODO expose as URL (capitalized)
-    location {.jsget.}: URL #TODO should be location
+    location* {.jsget.}: URL #TODO should be location
     mode*: QuirksMode
     currentScript: HTMLScriptElement
     isxml*: bool
@@ -305,19 +314,343 @@ type
 
   HTMLLabelElement* = ref object of HTMLElement
 
+  HTMLCanvasElement* = ref object of HTMLElement
+    ctx2d: CanvasRenderingContext2D
+    bitmap: Bitmap
+
+  DrawingState = object
+    # CanvasTransform
+    transformMatrix: Matrix
+    # CanvasFillStrokeStyles
+    fillStyle: RGBAColor
+    strokeStyle: RGBAColor
+    # CanvasPathDrawingStyles
+    lineWidth: float64
+    # CanvasTextDrawingStyles
+    textAlign: CSSTextAlign
+    # CanvasPath
+    path: Path
+
+  RenderingContext = ref object of RootObj
+
+  CanvasRenderingContext2D = ref object of RenderingContext
+    canvas {.jsget.}: HTMLCanvasElement
+    bitmap: Bitmap
+    state: DrawingState
+    stateStack: seq[DrawingState]
+
+  TextMetrics = ref object
+    # x-direction
+    width {.jsget.}: float64
+    actualBoundingBoxLeft {.jsget.}: float64
+    actualBoundingBoxRight {.jsget.}: float64
+    # y-direction
+    fontBoundingBoxAscent {.jsget.}: float64
+    fontBoundingBoxDescent {.jsget.}: float64
+    actualBoundingBoxAscent {.jsget.}: float64
+    actualBoundingBoxDescent {.jsget.}: float64
+    emHeightAscent {.jsget.}: float64
+    emHeightDescent {.jsget.}: float64
+    hangingBaseline {.jsget.}: float64
+    alphabeticBaseline {.jsget.}: float64
+    ideographicBaseline {.jsget.}: float64
+
+proc parseColor(element: Element, s: string): RGBAColor
+
+proc resetTransform(state: var DrawingState) =
+  state.transformMatrix = newIdentityMatrix(3)
+
+proc resetState(state: var DrawingState) =
+  state.resetTransform()
+  state.fillStyle = rgba(0, 0, 0, 255)
+  state.strokeStyle = rgba(0, 0, 0, 255)
+  state.path = newPath()
+
+proc create2DContext*(target: HTMLCanvasElement, options: Option[JSObject]):
+    CanvasRenderingContext2D =
+  let ctx = CanvasRenderingContext2D(
+    bitmap: target.bitmap,
+    canvas: target
+  )
+  ctx.state.resetState()
+  return ctx
+
+# CanvasState
+proc save(ctx: CanvasRenderingContext2D) {.jsfunc.} =
+  ctx.stateStack.add(ctx.state)
+
+proc restore(ctx: CanvasRenderingContext2D) {.jsfunc.} =
+  if ctx.stateStack.len > 0:
+    ctx.state = ctx.stateStack.pop()
+
+proc reset(ctx: CanvasRenderingContext2D) {.jsfunc.} =
+  ctx.bitmap.clear()
+  #TODO empty list of subpaths
+  ctx.stateStack.setLen(0)
+  ctx.state.resetState()
+
+# CanvasTransform
+#TODO scale
+proc rotate(ctx: CanvasRenderingContext2D, angle: float64) {.jsfunc.} =
+  if classify(angle) in {fcInf, fcNegInf, fcNan}:
+    return
+  ctx.state.transformMatrix *= newMatrix(
+    me = @[
+      cos(angle), -sin(angle), 0,
+      sin(angle), cos(angle), 0,
+      0, 0, 1
+    ],
+    w = 3,
+    h = 3
+  )
+
+proc translate(ctx: CanvasRenderingContext2D, x, y: float64) {.jsfunc.} =
+  for v in [x, y]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  ctx.state.transformMatrix *= newMatrix(
+    me = @[
+      1f64, 0, x,
+      0, 1, y,
+      0, 0, 1
+    ],
+    w = 3,
+    h = 3
+  )
+
+proc transform(ctx: CanvasRenderingContext2D, a, b, c, d, e, f: float64)
+    {.jsfunc.} =
+  for v in [a, b, c, d, e, f]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  ctx.state.transformMatrix *= newMatrix(
+    me = @[
+      a, c, e,
+      b, d, f,
+      0, 0, 1
+    ],
+    w = 3,
+    h = 3
+  )
+
+#TODO getTransform, setTransform with DOMMatrix (i.e. we're missing DOMMatrix)
+proc setTransform(ctx: CanvasRenderingContext2D, a, b, c, d, e, f: float64)
+    {.jsfunc.} =
+  for v in [a, b, c, d, e, f]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  ctx.state.resetTransform()
+  ctx.transform(a, b, c, d, e, f)
+
+proc resetTransform(ctx: CanvasRenderingContext2D) {.jsfunc.} =
+  ctx.state.resetTransform()
+
+func transform(ctx: CanvasRenderingContext2D, v: Vector2D): Vector2D =
+  let mul = ctx.state.transformMatrix * newMatrix(@[v.x, v.y, 1], 1, 3)
+  return Vector2D(x: mul.me[0], y: mul.me[1])
+
+# CanvasFillStrokeStyles
+proc fillStyle(ctx: CanvasRenderingContext2D): string {.jsfget.} =
+  return ctx.state.fillStyle.serialize()
+
+proc fillStyle(ctx: CanvasRenderingContext2D, s: string) {.jsfset.} =
+  #TODO gradient, pattern
+  ctx.state.fillStyle = ctx.canvas.parseColor(s)
+
+proc strokeStyle(ctx: CanvasRenderingContext2D): string {.jsfget.} =
+  return ctx.state.strokeStyle.serialize()
+
+proc strokeStyle(ctx: CanvasRenderingContext2D, s: string) {.jsfset.} =
+  #TODO gradient, pattern
+  ctx.state.strokeStyle = ctx.canvas.parseColor(s)
+
+# CanvasRect
+proc clearRect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} =
+  for v in [x, y, w, h]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  #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)
+
+proc fillRect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} =
+  for v in [x, y, w, h]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  #TODO do we have to clip here?
+  if w == 0 or h == 0:
+    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)
+
+proc strokeRect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} =
+  for v in [x, y, w, h]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  #TODO do we have to clip here?
+  if w == 0 or h == 0:
+    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)
+
+# CanvasDrawPath
+proc beginPath(ctx: CanvasRenderingContext2D) {.jsfunc.} =
+  ctx.state.path.beginPath()
+
+proc fill(ctx: CanvasRenderingContext2D,
+    fillRule = CanvasFillRule.NON_ZERO) {.jsfunc.} = #TODO path
+  ctx.state.path.tempClosePath()
+  ctx.bitmap.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)
+
+proc clip(ctx: CanvasRenderingContext2D,
+    fillRule = CanvasFillRule.NON_ZERO) {.jsfunc.} = #TODO path
+  #TODO implement
+  discard
+
+#TODO clip, ...
+
+# CanvasUserInterface
+
+# CanvasText
+#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
+  let vec = ctx.transform(Vector2D(x: x, y: y))
+  ctx.bitmap.fillText(text, vec.x, vec.y, ctx.state.fillStyle, ctx.state.textAlign)
+
+#TODO maxwidth
+proc strokeText(ctx: CanvasRenderingContext2D, text: string, x, y: float64) {.jsfunc.} =
+  for v in [x, y]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  let vec = ctx.transform(Vector2D(x: x, y: y))
+  ctx.bitmap.strokeText(text, vec.x, vec.y, ctx.state.strokeStyle, ctx.state.textAlign)
+
+proc measureText(ctx: CanvasRenderingContext2D, text: string): TextMetrics
+    {.jsfunc.} =
+  let tw = text.width()
+  return TextMetrics(
+    width: 8 * float64(tw),
+    actualBoundingBoxLeft: 0,
+    actualBoundingBoxRight: 8 * float64(tw),
+    #TODO and the rest...
+  )
+
+# CanvasDrawImage
+
+# CanvasImageData
+
+# CanvasPathDrawingStyles
+proc lineWidth(ctx: CanvasRenderingContext2D): float64 {.jsfget.} =
+  return ctx.state.lineWidth
+
+proc lineWidth(ctx: CanvasRenderingContext2D, f: float64) {.jsfset.} =
+  if classify(f) in {fcZero, fcNegZero, fcInf, fcNegInf, fcNan}:
+    return
+  ctx.state.lineWidth = f
+
+proc setLineDash(ctx: CanvasRenderingContext2D, segments: seq[float64])
+    {.jsfunc.} =
+  discard #TODO implement
+
+proc getLineDash(ctx: CanvasRenderingContext2D): seq[float64] {.jsfunc.} =
+  discard #TODO implement
+
+# CanvasTextDrawingStyles
+proc textAlign(ctx: CanvasRenderingContext2D): string {.jsfget.} =
+  case ctx.state.textAlign
+  of TEXT_ALIGN_START: return "start"
+  of TEXT_ALIGN_END: return "end"
+  of TEXT_ALIGN_LEFT: return "left"
+  of TEXT_ALIGN_RIGHT: return "right"
+  of TEXT_ALIGN_CENTER: return "center"
+  else: doAssert false
+
+proc textAlign(ctx: CanvasRenderingContext2D, s: string) {.jsfset.} =
+  ctx.state.textAlign = case s
+  of "start": TEXT_ALIGN_START
+  of "end": TEXT_ALIGN_END
+  of "left": TEXT_ALIGN_LEFT
+  of "right": TEXT_ALIGN_RIGHT
+  of "center": TEXT_ALIGN_CENTER
+  else: ctx.state.textAlign
+
+# CanvasPath
+proc closePath(ctx: CanvasRenderingContext2D) {.jsfunc.} =
+  ctx.state.path.closePath()
+
+proc moveTo(ctx: CanvasRenderingContext2D, x, y: float64) {.jsfunc.} =
+  ctx.state.path.moveTo(x, y)
+
+proc lineTo(ctx: CanvasRenderingContext2D, x, y: float64) {.jsfunc.} =
+  ctx.state.path.lineTo(x, y)
+
+proc quadraticCurveTo(ctx: CanvasRenderingContext2D, cpx, cpy, x,
+    y: float64) {.jsfunc.} =
+  ctx.state.path.quadraticCurveTo(cpx, cpy, x, y)
+
+proc arcTo(ctx: CanvasRenderingContext2D, x1, y1, x2, y2, radius: float64)
+    {.jsfunc.} =
+  if not ctx.state.path.arcTo(x1, y1, x2, y2, radius):
+    #TODO should be DOMException
+    JS_ERR JS_TypeError, "IndexSizeError"
+
+proc arc(ctx: CanvasRenderingContext2D, x, y, radius, startAngle,
+    endAngle: float64, counterclockwise = false) {.jsfunc.} =
+  if not ctx.state.path.arc(x, y, radius, startAngle, endAngle,
+      counterclockwise):
+    #TODO should be DOMException
+    JS_ERR JS_TypeError, "IndexSizeError"
+
+proc ellipse(ctx: CanvasRenderingContext2D, x, y, radiusX, radiusY, rotation,
+    startAngle, endAngle: float64, counterclockwise = false) {.jsfunc.} =
+  if not ctx.state.path.ellipse(x, y, radiusX, radiusY, rotation, startAngle,
+      endAngle, counterclockwise):
+    #TODO should be DOMException
+    JS_ERR JS_TypeError, "IndexSizeError"
+
+proc rect(ctx: CanvasRenderingContext2D, x, y, w, h: float64) {.jsfunc.} =
+  ctx.state.path.rect(x, y, w, h)
+
+proc roundRect(ctx: CanvasRenderingContext2D, x, y, w, h, radii: float64) {.jsfunc.} =
+  ctx.state.path.roundRect(x, y, w, h, radii)
+
 # Reflected attributes.
 type
   ReflectType = enum
-    REFLECT_STR, REFLECT_BOOL, REFLECT_INT, REFLECT_INT_GREATER_ZERO,
-    REFLECT_INT_GREATER_EQUAL_ZERO
-
-  ReflectEntry = tuple[
-    attrname: string,
-    funcname: string,
-    t: ReflectType,
-    tags: set[TagType],
-    i: int
-  ]
+    REFLECT_STR, REFLECT_BOOL, REFLECT_LONG, REFLECT_ULONG_GZ, REFLECT_ULONG
+
+  ReflectEntry = object
+    attrname: string
+    funcname: string
+    tags: set[TagType]
+    case t: ReflectType
+    of REFLECT_LONG:
+      i: int32
+    of REFLECT_ULONG, REFLECT_ULONG_GZ:
+      u: uint32
+    else: discard
 
 template toset(ts: openarray[TagType]): set[TagType] =
   var tags: system.set[TagType]
@@ -325,23 +658,53 @@ template toset(ts: openarray[TagType]): set[TagType] =
     tags.incl(tag)
   tags
 
-template makes(name: string, ts: set[TagType]): ReflectEntry =
-  (name, name, REFLECT_STR, ts, 0)
+func makes(name: string, ts: set[TagType]): ReflectEntry =
+  ReflectEntry(
+    attrname: name,
+    funcname: name,
+    t: REFLECT_STR,
+    tags: ts
+  )
 
-template makes(attrname: string, funcname: string, ts: set[TagType]): ReflectEntry =
-  (attrname, funcname, REFLECT_STR, ts, 0)
+func makes(attrname: string, funcname: string, ts: set[TagType]): ReflectEntry =
+  ReflectEntry(
+    attrname: attrname,
+    funcname: funcname,
+    t: REFLECT_STR,
+    tags: ts
+  )
 
-template makes(name: string, ts: varargs[TagType]): ReflectEntry =
+func makes(name: string, ts: varargs[TagType]): ReflectEntry =
   makes(name, toset(ts))
 
-template makes(attrname: string, funcname: string, ts: varargs[TagType]): ReflectEntry =
+func makes(attrname, funcname: string, ts: varargs[TagType]): ReflectEntry =
   makes(attrname, funcname, toset(ts))
 
 template makeb(name: string, ts: varargs[TagType]): ReflectEntry =
-  (name, name, REFLECT_BOOL, toset(ts), 0)
+  ReflectEntry(
+    attrname: name,
+    funcname: name,
+    t: REFLECT_BOOL,
+    tags: toset(ts)
+  )
 
-template makeigz(name: string, ts: varargs[TagType], default = 0): ReflectEntry =
-  (name, name, REFLECT_INT_GREATER_ZERO, toset(ts), default)
+template makeul(name: string, ts: varargs[TagType], default = 0u32): ReflectEntry =
+  ReflectEntry(
+    attrname: name,
+    funcname: name,
+    t: REFLECT_ULONG,
+    tags: toset(ts),
+    u: default
+  )
+
+template makeulgz(name: string, ts: varargs[TagType], default = 0u32): ReflectEntry =
+  ReflectEntry(
+    attrname: name,
+    funcname: name,
+    t: REFLECT_ULONG_GZ,
+    tags: toset(ts),
+    u: default
+  )
 
 const ReflectTable0 = [
   # non-global attributes
@@ -350,15 +713,17 @@ const ReflectTable0 = [
   makeb("required", TAG_INPUT, TAG_SELECT, TAG_TEXTAREA),
   makes("rel", "relList", TAG_A, TAG_LINK, TAG_LABEL),
   makes("for", "htmlFor", TAG_LABEL),
-  makeigz("cols", TAG_TEXTAREA, 20),
-  makeigz("rows", TAG_TEXTAREA, 1),
+  makeul("cols", TAG_TEXTAREA, 20u32),
+  makeul("rows", TAG_TEXTAREA, 1u32),
 # <SELECT>:
 #> For historical reasons, the default value of the size IDL attribute does
 #> not return the actual size used, which, in the absence of the size content
 #> attribute, is either 1 or 4 depending on the presence of the multiple
 #> attribute.
-  makeigz("size", TAG_SELECT, 0),
-  makeigz("size", TAG_INPUT, 20),
+  makeulgz("size", TAG_SELECT, 0u32),
+  makeulgz("size", TAG_INPUT, 20u32),
+  makeul("width", TAG_CANVAS, 300u32),
+  makeul("height", TAG_CANVAS, 150u32),
   # "super-global" attributes
   makes("slot", AllTagTypes),
   makes("class", "className", AllTagTypes)
@@ -1033,30 +1398,18 @@ func documentElement(document: Document): Element {.jsfget.} =
 func attr*(element: Element, s: string): string {.inline.} =
   return element.attrs.getOrDefault(s, "")
 
-func attri*(element: Element, s: string): Option[int] =
-  let a = element.attr(s)
-  try:
-    return some(parseInt(a))
-  except ValueError:
-    return none(int)
+func attrl*(element: Element, s: string): Option[int32] =
+  return parseInt32(element.attr(s))
 
-func attrigz*(element: Element, s: string): Option[int] =
-  let a = element.attr(s)
-  try:
-    let i = parseInt(a)
-    if i > 0:
-      return some(i)
-  except ValueError:
-    discard
+func attrulgz*(element: Element, s: string): Option[uint32] =
+  let x = parseUInt32(element.attr(s))
+  if x.isSome and x.get > 0:
+    return x
 
-func attrigez*(element: Element, s: string): Option[int] =
-  let a = element.attr(s)
-  try:
-    let i = parseInt(a)
-    if i >= 0:
-      return some(i)
-  except ValueError:
-    discard
+func attrul*(element: Element, s: string): Option[uint32] =
+  let x = parseUInt32(element.attr(s))
+  if x.isSome and x.get >= 0:
+    return x
 
 func attrb*(element: Element, s: string): bool =
   if s in element.attrs:
@@ -1108,9 +1461,9 @@ func inputString*(input: HTMLInputElement): string =
     if input.checked: "*"
     else: " "
   of INPUT_SEARCH, INPUT_TEXT:
-    input.value.padToWidth(input.attri("size").get(20))
+    input.value.padToWidth(int(input.attrulgz("size").get(20)))
   of INPUT_PASSWORD:
-    '*'.repeat(input.value.len).padToWidth(input.attri("size").get(20))
+    '*'.repeat(input.value.len).padToWidth(int(input.attrulgz("size").get(20)))
   of INPUT_RESET:
     if input.value != "": input.value
     else: "RESET"
@@ -1119,16 +1472,16 @@ func inputString*(input: HTMLInputElement): string =
     else: "SUBMIT"
   of INPUT_FILE:
     if input.file.isnone:
-      "".padToWidth(input.attri("size").get(20))
+      "".padToWidth(int(input.attrulgz("size").get(20)))
     else:
-      input.file.get.path.serialize_unicode().padToWidth(input.attri("size").get(20))
+      input.file.get.path.serialize_unicode().padToWidth(int(input.attrulgz("size").get(20)))
   else: input.value
 
 func textAreaString*(textarea: HTMLTextAreaElement): string =
   let split = textarea.value.split('\n')
-  let rows = textarea.attri("rows").get(1)
+  let rows = int(textarea.attrul("rows").get(1))
   for i in 0 ..< rows:
-    let cols = textarea.attri("cols").get(20)
+    let cols = int(textarea.attrul("cols").get(20))
     if cols > 2:
       if i < split.len:
         result &= '[' & split[i].padToWidth(cols - 2) & "]\n"
@@ -1208,6 +1561,14 @@ func formmethod*(element: Element): FormMethod =
 
   return FORM_METHOD_GET
 
+proc parseColor(element: Element, s: string): RGBAColor =
+  try:
+    return cssColor(parseComponentValue(newStringStream(s)))
+  except CSSValueError:
+    #TODO TODO TODO return element style
+    # For now we just use white.
+    return rgb(255, 255, 255)
+
 #TODO ??
 func target0*(element: Element): string =
   if element.attrb("target"):
@@ -1321,7 +1682,9 @@ func newComment(window: Window, data: string = ""): Comment {.jsgctor.} =
   return window.document.newComment(data)
 
 #TODO custom elements
-func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace.HTML, prefix = none[string](), attrs = Table[string, string]()): HTMLElement =
+func newHTMLElement*(document: Document, tagType: TagType,
+    namespace = Namespace.HTML, prefix = none[string](),
+    attrs = Table[string, string]()): HTMLElement =
   case tagType
   of TAG_INPUT:
     result = new(HTMLInputElement)
@@ -1369,6 +1732,8 @@ func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace
     result = new(HTMLTextAreaElement)
   of TAG_LABEL:
     result = new(HTMLLabelElement)
+  of TAG_CANVAS:
+    result = new(HTMLCanvasElement)
   else:
     result = new(HTMLElement)
   result.nodeType = ELEMENT_NODE
@@ -1382,10 +1747,19 @@ func newHTMLElement*(document: Document, tagType: TagType, namespace = Namespace
   {.cast(noSideEffect).}:
     for k, v in attrs:
       result.attr(k, v)
-  if tagType == TAG_SCRIPT:
+  case tagType
+  of TAG_SCRIPT:
     HTMLScriptElement(result).internalNonce = result.attr("nonce")
+  of TAG_CANVAS:
+    HTMLCanvasElement(result).bitmap = newBitmap(
+      width = result.attrul("width").get(300),
+      height = result.attrul("height").get(150)
+    )
+  else: discard
 
-func newHTMLElement*(document: Document, localName: string, namespace = Namespace.HTML, prefix = none[string](), tagType = tagType(localName), attrs = Table[string, string]()): Element =
+func newHTMLElement*(document: Document, localName: string,
+    namespace = Namespace.HTML, prefix = none[string](),
+    tagType = tagType(localName), attrs = Table[string, string]()): Element =
   result = document.newHTMLElement(tagType, namespace, prefix, attrs)
   if tagType == TAG_UNKNOWN:
     result.localName = localName
@@ -1547,16 +1921,15 @@ proc attr*(element: Element, name, value: string) =
     element.attributes.attrlist.add(element.newAttr(name, value))
   element.attr0(name, value)
 
-proc attri(element: Element, name: string, value: int) =
+proc attrl(element: Element, name: string, value: int32) =
   element.attr(name, $value)
 
-proc attrigz(element: Element, name: string, value: int) =
-  if value > 0:
-    element.attri(name, value)
+proc attrul(element: Element, name: string, value: uint32) =
+  element.attr(name, $value)
 
-proc attrigez(element: Element, name: string, value: int) =
-  if value >= 0:
-    element.attri(name, value)
+proc attrulgz(element: Element, name: string, value: uint32) =
+  if value > 0:
+    element.attrul(name, value)
 
 proc setAttribute(element: Element, qualifiedName, value: string) {.jserr, jsfunc.} =
   if not qualifiedName.matchNameProduction():
@@ -1717,7 +2090,7 @@ proc resetElement*(element: Element) =
   of TAG_SELECT:
     let select = HTMLSelectElement(element)
     if not select.attrb("multiple"):
-      if select.attrigez("size").get(1) == 1:
+      if select.attrul("size").get(1) == 1:
         var i = 0
         var firstOption: HTMLOptionElement
         for option in select.options:
@@ -2277,12 +2650,12 @@ proc jsReflectGet(ctx: JSContext, this: JSValue, magic: cint): JSValue {.cdecl.}
     return x
   of REFLECT_BOOl:
     return toJS(ctx, element.attrb(entry.attrname))
-  of REFLECT_INT:
-    return toJS(ctx, element.attri(entry.attrname).get(entry.i))
-  of REFLECT_INT_GREATER_ZERO:
-    return toJS(ctx, element.attrigz(entry.attrname).get(entry.i))
-  of REFLECT_INT_GREATER_EQUAL_ZERO:
-    return toJS(ctx, element.attrigez(entry.attrname).get(entry.i))
+  of REFLECT_LONG:
+    return toJS(ctx, element.attrl(entry.attrname).get(entry.i))
+  of REFLECT_ULONG:
+    return toJS(ctx, element.attrul(entry.attrname).get(entry.u))
+  of REFLECT_ULONG_GZ:
+    return toJS(ctx, element.attrulgz(entry.attrname).get(entry.u))
 
 proc jsReflectSet(ctx: JSContext, this, val: JSValue, magic: cint): JSValue {.cdecl.} =
   if unlikely(not ctx.isInstanceOf(this, "Element")):
@@ -2305,18 +2678,18 @@ proc jsReflectSet(ctx: JSContext, this, val: JSValue, magic: cint): JSValue {.cd
         element.attr(entry.attrname, "")
       else:
         element.delAttr(entry.attrname)
-  of REFLECT_INT:
-    let x = fromJS[int](ctx, val)
+  of REFLECT_LONG:
+    let x = fromJS[int32](ctx, val)
     if x.isSome:
-      element.attri(entry.attrname, x.get)
-  of REFLECT_INT_GREATER_ZERO:
-    let x = fromJS[int](ctx, val)
+      element.attrl(entry.attrname, x.get)
+  of REFLECT_ULONG:
+    let x = fromJS[uint32](ctx, val)
     if x.isSome:
-      element.attrigz(entry.attrname, x.get)
-  of REFLECT_INT_GREATER_EQUAL_ZERO:
-    let x = fromJS[int](ctx, val)
+      element.attrul(entry.attrname, x.get)
+  of REFLECT_ULONG_GZ:
+    let x = fromJS[uint32](ctx, val)
     if x.isSome:
-      element.attrigez(entry.attrname, x.get)
+      element.attrulgz(entry.attrname, x.get)
   return JS_DupValue(ctx, val)
 
 proc addconsoleModule*(ctx: JSContext) =
@@ -2337,12 +2710,35 @@ func getReflectFunctions(tags: set[TagType]): seq[TabGetSet] =
 
 func getElementReflectFunctions(): seq[TabGetSet] =
   var i: uint16 = ReflectAllStartIndex
-  while i < ReflectTable.len:
+  while i < uint16(ReflectTable.len):
     let entry = ReflectTable[i]
     assert entry.tags == AllTagTypes
     result.add(TabGetSet(name: ReflectTable[i].funcname, get: jsReflectGet, set: jsReflectSet, magic: i))
     inc i
 
+proc getContext*(this: HTMLCanvasElement, contextId: string,
+    options = none(JSObject)): RenderingContext {.jsfunc.} =
+  if contextId == "2d":
+    if this.ctx2d != nil:
+      return this.ctx2d
+    return create2DContext(this, options)
+  return nil
+
+#TODO quality should be `any'
+proc toBlob(this: HTMLCanvasElement, callback: JSObject,
+    s = "image/png", quality: float64 = 1): JSValue {.jsfunc.} =
+  let ctx = callback.ctx
+  var outlen: int
+  let buf = this.bitmap.toPNG(outlen)
+  let blob = newBlob(buf, outlen, "image/png", dealloc)
+  var jsBlob = toJS(ctx, blob)
+  let res = JS_Call(ctx, callback.val, JS_UNDEFINED, 1, addr jsBlob)
+  # Hack. TODO: implement JSValue to callback
+  if res == JS_EXCEPTION:
+    return JS_EXCEPTION
+  JS_FreeValue(ctx, res)
+  return JS_UNDEFINED
+
 proc registerElements(ctx: JSContext, nodeCID: JSClassID) =
   let elementCID = ctx.registerType(Element, parent = nodeCID)
   const extra_getset = getElementReflectFunctions()
@@ -2377,6 +2773,7 @@ proc registerElements(ctx: JSContext, nodeCID: JSClassID) =
   register(HTMLButtonElement, TAG_BUTTON)
   register(HTMLTextAreaElement, TAG_TEXTAREA)
   register(HTMLLabelElement, TAG_LABEL)
+  register(HTMLCanvasElement, TAG_CANVAS)
 
 proc addDOMModule*(ctx: JSContext) =
   let eventTargetCID = ctx.registerType(EventTarget)
@@ -2395,4 +2792,6 @@ proc addDOMModule*(ctx: JSContext) =
   ctx.registerType(DocumentType, parent = nodeCID)
   ctx.registerType(Attr, parent = nodeCID)
   ctx.registerType(NamedNodeMap)
+  ctx.registerType(CanvasRenderingContext2D)
+  ctx.registerType(TextMetrics)
   ctx.registerElements(nodeCID)
diff --git a/src/html/env.nim b/src/html/env.nim
index 544f5083..8b558b8a 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -6,9 +6,12 @@ import html/htmlparser
 import io/loader
 import io/promise
 import io/request
+import js/intl
 import js/javascript
 import js/timeout
+import types/blob
 import types/url
+import xhr/formdata as formdata_impl
 
 # NavigatorID
 proc appCodeName(navigator: Navigator): string {.jsfget.} = "Mozilla"
@@ -73,6 +76,16 @@ proc clearTimeout(window: Window, id: int32) {.jsfunc.} =
 proc clearInterval(window: Window, id: int32) {.jsfunc.} =
   window.timeouts.clearInterval(id)
 
+proc screenX(window: Window): int64 {.jsfget.} = 0
+proc screenY(window: Window): int64 {.jsfget.} = 0
+proc screenLeft(window: Window): int64 {.jsfget.} = 0
+proc screenTop(window: Window): int64 {.jsfget.} = 0
+#TODO outerWidth, outerHeight
+proc devicePixelRatio(window: Window): float64 {.jsfget.} = 1
+
+func location(window: Window): URL {.jsfget.} =
+  window.document.location
+
 proc addScripting*(window: Window, selector: Selector[int]) =
   let rt = newJSRuntime()
   let ctx = rt.newJSContext()
@@ -102,6 +115,10 @@ proc addScripting*(window: Window, selector: Selector[int]) =
   ctx.addDOMModule()
   ctx.addURLModule()
   ctx.addHTMLModule()
+  ctx.addIntlModule()
+  ctx.addBlobModule()
+  ctx.addFormDataModule()
+  ctx.addRequestModule()
 
 proc runJSJobs*(window: Window) =
   window.jsrt.runJSJobs(window.console.err)
diff --git a/src/img/bitmap.nim b/src/img/bitmap.nim
new file mode 100644
index 00000000..03048f4d
--- /dev/null
+++ b/src/img/bitmap.nim
@@ -0,0 +1,620 @@
+import algorithm
+import math
+import unicode
+
+import bindings/zlib
+import css/values
+import img/path
+import types/color
+import types/line
+import types/vector
+
+import lib/endians2
+
+type
+  CanvasFillRule* = enum
+    NON_ZERO = "nonzero"
+    EVEN_ODD = "evenodd"
+
+  Bitmap* = ref object of RootObj
+    px: seq[RGBAColor]
+    width*: uint64
+    height*: uint64
+
+  ImageBitmap* = ref object of Bitmap
+
+proc newBitmap*(width, height: uint64): Bitmap =
+  return ImageBitmap(
+    px: newSeq[RGBAColor](width * height),
+    width: width,
+    height: height
+  )
+
+proc setpx(bmp: Bitmap, x, y: uint64, color: RGBAColor) {.inline.} =
+  bmp.px[bmp.width * y + x] = color
+
+proc getpx*(bmp: Bitmap, x, y: uint64): RGBAColor {.inline.} =
+  return bmp.px[bmp.width * y + x]
+
+proc setpxb(bmp: Bitmap, x, y: uint64, color: RGBAColor) {.inline.} =
+  if color.a == 255:
+    bmp.setpx(x, y, color)
+  else:
+    bmp.setpx(x, y, bmp.getpx(x, y).blend(color))
+
+# https://en.wikipedia.org/wiki/Bresenham's_line_algorithm#All_cases
+proc plotLineLow(bmp: Bitmap, x0, y0, x1, y1: int64, color: RGBAColor) =
+  var dx = x1 - x0
+  var dy = y1 - y0
+  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:
+      break
+    bmp.setpxb(uint64(x), uint64(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: RGBAColor) =
+  var dx = x1 - x0
+  var dy = y1 - y0
+  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:
+      break
+    bmp.setpxb(uint64(x), uint64(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: RGBAColor) =
+  if abs(y1 - y0) < abs(x1 - x0):
+    if x0 > x1:
+      bmp.plotLineLow(x1, y1, x0, y0, color)
+    else:
+      bmp.plotLineLow(x0, y0, x1, y1, color)
+  else:
+    if y0 > y1:
+      bmp.plotLineHigh(x1, y1, x0, y0, color)
+    else:
+      bmp.plotLineHigh(x0, y0, x1, y1, color)
+
+proc plotLine(bmp: Bitmap, a, b: Vector2D, color: RGBAColor) =
+  bmp.plotLine(int64(a.x), int64(a.y), int64(b.x), int64(b.y), color)
+
+proc plotLine(bmp: Bitmap, line: Line, color: RGBAColor) =
+  bmp.plotLine(line.p0, line.p1, color)
+
+proc strokePath*(bmp: Bitmap, path: Path, color: RGBAColor) =
+  for line in path.lines:
+    bmp.plotLine(line, color)
+
+func isInside(windingNumber: int, fillRule: CanvasFillRule): bool =
+  return case fillRule
+  of NON_ZERO: windingNumber != 0
+  of EVEN_ODD: windingNumber mod 2 == 0
+
+# Mainly adapted from SerenityOS.
+proc fillPath*(bmp: Bitmap, path: Path, color: RGBAColor,
+    fillRule: CanvasFillRule) =
+  let lines = path.getLineSegments()
+  var i = 0
+  var ylines: seq[LineSegment]
+  for y in int64(lines.miny) .. int64(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 == NON_ZERO: 1 else: 0
+    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)
+      if isInside(w, 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:
+        case fillRule
+        of EVEN_ODD: inc w
+        of NON_ZERO:
+          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, x0, x1, y0, y1: uint64, color: RGBAColor) =
+  for y in y0 ..< y1:
+    for x in x0 ..< x1:
+      bmp.setpxb(x, y, color)
+
+proc strokeRect*(bmp: Bitmap, x0, x1, y0, y1: uint64, color: RGBAColor) =
+  for x in x0 ..< x1:
+    bmp.setpxb(x, y0, color)
+    bmp.setpxb(x, y1, color)
+  for y in y0 ..< y1:
+    bmp.setpxb(x0, y, color)
+    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)
+
+#TODO clean up templates, also move png encoder to a different file
+type PNGWriter = object
+  buf: pointer
+  i: int
+  outlen: int
+
+func pngInt(i: uint32): auto =
+  doAssert i < uint32(2 ^ 31)
+  return i.toBytesBE()
+
+func oq(writer: PNGWriter): ptr UncheckedArray[uint8] =
+  cast[ptr UncheckedArray[uint8]](writer.buf)
+
+proc writeStr[T](writer: var PNGWriter, s: T) =
+  if writer.outlen < writer.i + s.len:
+    writer.outlen = writer.i + s.len
+    writer.buf = realloc(writer.buf, writer.outlen)
+  copyMem(addr writer.oq[writer.i], unsafeAddr s[0], s.len)
+  writer.i += s.len
+
+proc writeInt(writer: var PNGWriter, i: uint32) =
+  writer.writeStr(i.toBytesBE())
+
+proc writePngInt(writer: var PNGWriter, i: uint32) =
+  doAssert i < uint32(2 ^ 31)
+  writer.writeInt(i)
+
+proc writeChunk[T](writer: var PNGWriter, t: string, data: T) =
+  var crc = uint32(crc32(0, cast[ptr uint8](unsafeAddr t[0]), cuint(t.len)))
+  if data.len > 0:
+    crc = uint32(crc32(crc, cast[ptr uint8](unsafeAddr data[0]),
+      cuint(data.len)))
+  writer.writePngInt(uint32(data.len))
+  writer.writeStr(t)
+  if data.len > 0:
+    writer.writeStr(data)
+  writer.writeInt(uint32(crc))
+
+type PNGColorType {.size: sizeof(uint8).} = enum
+  GRAYSCALE = 0
+  TRUECOLOR = 2
+  INDEXED_COLOR = 3
+  GRAYSCALE_WITH_ALPHA = 4
+  TRUECOLOR_WITH_ALPHA = 6
+
+func u8toc(x: openArray[uint8]): string =
+  #TODO ew
+  var s = newString(x.len)
+  copyMem(addr s[0], unsafeAddr x[0], x.len)
+  return s
+
+const PNGSignature = "\x89PNG\r\n\x1A\n"
+proc writeIHDR(writer: var PNGWriter, width, height: uint32,
+    bitDepth: uint8, colorType: PNGColorType,
+    compressionMethod, filterMethod, interlaceMethod: uint8) =
+  writer.writeStr(PNGSignature)
+  let ihdr = u8toc(pngInt(width)) &
+    u8toc(pngInt(height)) &
+    char(bitDepth) &
+    char(uint8(colorType)) &
+    char(compressionMethod) &
+    char(filterMethod) &
+    char(interlaceMethod)
+  writer.writeChunk("IHDR", ihdr)
+
+proc writeIDAT(writer: var PNGWriter, bmp: Bitmap) =
+  #TODO smaller idat chunks
+  # +1 height for filter
+  var idat = newSeq[uint8]((bmp.width + 1) * bmp.height * 4)
+  var j = 0 # idat pointer
+  for k in 0 ..< bmp.px.len:
+    if k mod int(bmp.width) == 0:
+      # begin row
+      # For now, filter is always 0. TODO implement other filters
+      inc j
+    let p = bmp.px[k]
+    idat[j] = uint8(p.r)
+    idat[j + 1] = uint8(p.g)
+    idat[j + 2] = uint8(p.b)
+    idat[j + 3] = uint8(p.a)
+    j += 4
+  var hlen = compressBound(culong(idat.len))
+  var oidat = newSeq[uint8](int(hlen))
+  let res = compress(addr oidat[0], addr hlen, addr idat[0], culong(idat.len))
+  doAssert res == Z_OK #TODO error handling...
+  oidat.setLen(int(hlen))
+  writer.writeChunk("IDAT", oidat)
+
+proc toPNG*(bmp: Bitmap, outlen: var int): pointer =
+  var writer = PNGWriter(
+    buf: alloc(PNGSignature.len),
+    outlen: PNGSignature.len
+  )
+  writer.writeIHDR(uint32(bmp.width), uint32(bmp.height), 8,
+    TRUECOLOR_WITH_ALPHA, 0, 0, 0)
+  writer.writeIDAT(bmp)
+  writer.writeChunk("IEND", "")
+  outlen = writer.outlen
+  return writer.buf
+
+type PNGReader = object
+  bmp: Bitmap
+  iq: ptr UncheckedArray[uint8]
+  limit: int
+  i: int
+  bitDepth: uint8
+  colorType: PNGColorType
+  background: RGBColor
+  isend: bool
+  idatBuf: seq[uint8]
+  uprow: seq[uint8]
+  idatAt: int
+  hasstrm: bool
+  strm: z_stream
+  strmend: bool
+  atline: int
+
+func width(reader: PNGReader): int {.inline.} = int(reader.bmp.width)
+
+func height(reader: PNGReader): int {.inline.} = int(reader.bmp.height)
+
+func spp(reader: PNGReader): int =
+  case reader.colorType
+  of TRUECOLOR: return 3
+  of GRAYSCALE: return 1
+  of INDEXED_COLOR: return 1
+  of GRAYSCALE_WITH_ALPHA: return 2
+  of TRUECOLOR_WITH_ALPHA: return 4
+
+func scanlen(reader: PNGReader): int {.inline.} =
+  let w = reader.width + 1
+  return (w * reader.spp * int(reader.bitDepth) + 7) div 8
+
+proc handleError(reader: var PNGReader, msg: string) =
+  reader.bmp = nil
+  if reader.hasstrm:
+    discard inflateEnd(addr reader.strm)
+
+template err(reader: var PNGReader, msg: string) =
+  reader.handleError(msg)
+  return
+
+template readStr(reader: var PNGReader, L: int): string =
+  if reader.i + L > reader.limit:
+    reader.err "too short"
+  var s = newString(L)
+  copyMem(addr s[0], addr reader.iq[reader.i], L)
+  reader.i += L
+  s
+
+template readU8(reader: var PNGReader): uint8 =
+  if reader.i > reader.limit:
+    reader.err "too short"
+  let x = reader.iq[reader.i]
+  inc reader.i
+  x
+
+template readU32(reader: var PNGReader): uint32 =
+  if reader.i + 4 > reader.limit:
+    reader.err "too short"
+  let x = fromBytesBE(uint32, toOpenArray(reader.iq, reader.i, reader.i + 3))
+  reader.i += 4
+  x
+
+template readPNGInt(reader: var PNGReader): uint32 =
+  let x = reader.readU32()
+  if x >= uint32(2 ^ 31):
+    reader.err "int too large"
+  x
+
+template readColorType(reader: var PNGReader): PNGColorType =
+  case reader.readU8()
+  of 0u8: GRAYSCALE
+  of 2u8: TRUECOLOR
+  of 3u8: INDEXED_COLOR
+  of 4u8: GRAYSCALE_WITH_ALPHA
+  of 6u8: TRUECOLOR_WITH_ALPHA
+  else: reader.err "unknown color type"
+
+func bitDepthValid(colorType: PNGColorType, bitDepth: uint8): bool =
+  case colorType
+  of GRAYSCALE:
+    return int(bitDepth) in [1, 2, 4, 8, 16]
+  of INDEXED_COLOR:
+    return int(bitDepth) in [1, 2, 4, 8]
+  of TRUECOLOR, GRAYSCALE_WITH_ALPHA, TRUECOLOR_WITH_ALPHA:
+    return int(bitDepth) in [8, 16]
+
+proc readIHDR(reader: var PNGReader) =
+  if reader.readStr(PNGSignature.len) != PNGSignature:
+    reader.err "wrong signature"
+  if reader.readPNGInt() != 13:
+    reader.err "invalid header length"
+  if reader.readStr(4) != "IHDR":
+    reader.err "invalid header chunk"
+  let width = reader.readPNGInt()
+  let height = reader.readPNGInt()
+  reader.bitDepth = reader.readU8() #TODO check?
+  reader.colorType = reader.readColorType()
+  if not bitDepthValid(reader.colorType, reader.bitDepth):
+    reader.err "invalid bit depth"
+  let compressionMethod = reader.readU8()
+  if compressionMethod != 0:
+    reader.err "unknown compression method"
+  let filterMethod = reader.readU8()
+  if filterMethod != 0:
+    reader.err "unknown filter method"
+  let interlaceMethod = reader.readU8()
+  if interlaceMethod != 0:
+    reader.err "unknown interlace method"
+  let crc = crc32(0, addr reader.iq[reader.i - 17], 17)
+  if uint32(crc) != reader.readU32(): reader.err "wrong crc"
+  reader.bmp = newBitmap(width, height)
+
+proc readbKGD(reader: var PNGReader) =
+  case reader.colorType
+  of GRAYSCALE, GRAYSCALE_WITH_ALPHA:
+    discard reader.readU8() #TODO bit depth > 8
+    reader.background = gray(reader.readU8())
+  of TRUECOLOR, TRUECOLOR_WITH_ALPHA:
+    discard reader.readU8() #TODO bit depth > 8
+    let r = reader.readU8()
+    discard reader.readU8()
+    let g = reader.readU8()
+    discard reader.readU8()
+    let b = reader.readU8()
+    reader.background = rgb(r, g, b)
+  of INDEXED_COLOR:
+    discard #TODO
+
+proc unfilter(reader: var PNGReader, irow: openArray[uint8], bpp: int) =
+  # none, sub, up -> replace uprow directly
+  # average, paeth -> copy to temp array, then replace uprow
+  let fil = irow[0]
+  let w = reader.width
+  case fil
+  of 0u8: # none
+    copyMem(addr reader.uprow[0], unsafeAddr irow[1], w)
+  of 1u8: # sub
+    for i in 1 ..< irow.len:
+      let j = i - 1 # skip filter byte
+      reader.uprow[j] = irow[i]
+      if j - bpp >= 0:
+        reader.uprow[j] += irow[j - bpp]
+  of 2u8: # up
+    for i in 1 ..< irow.len:
+      let j = i - 1 # skip filter byte
+      reader.uprow[j] += irow[i]
+  of 3u8: # average
+    reader.err "average not implemented yet"
+  of 4u8: # paeth
+    reader.err "paeth not implemented yet"
+  else:
+    eprint fil
+    reader.err "got invalid filter"
+
+proc writepxs(reader: var PNGReader, crow: var openArray[RGBAColor]) =
+  case reader.colorType
+  of GRAYSCALE:
+    var i = 0
+    var j = 0
+    for x in 0 ..< crow.len:
+      let u = reader.uprow[i]
+      let n = case reader.bitDepth
+      of 1: ((u shr (7 - j)) and 1) * 255
+      of 2: ((u shr (6 - j)) and 3) * 85
+      of 4: ((u shr (6 - j)) and 15) * 17
+      of 8: u
+      of 16: u div 2
+      else: 0
+      j += int(reader.bitDepth)
+      i += j div 8
+      j = j mod 8
+      let nn = int(n)
+      crow[x] = rgba(nn, nn, nn, 255)
+  else: discard
+
+proc readIDAT(reader: var PNGReader) =
+  if reader.idatAt == reader.idatBuf.len:
+    reader.err "idat buffer already filled"
+  if reader.strmend:
+    reader.err "stream already ended"
+  reader.strm.avail_in = cuint(reader.limit - reader.i)
+  reader.strm.next_in = addr reader.iq[reader.i]
+  let olen = reader.idatBuf.len - reader.idatAt
+  reader.strm.avail_out = cuint(olen)
+  reader.strm.next_out = addr reader.idatBuf[reader.idatAt]
+  let res = inflate(addr reader.strm, Z_NO_FLUSH)
+  doAssert res != Z_STREAM_ERROR
+  case res
+  of Z_NEED_DICT, Z_DATA_ERROR, Z_MEM_ERROR, Z_BUF_ERROR:
+    # Z_BUF_ERROR is fatal here, as outlen is at least as large as idat.
+    reader.err "error decompressing idat stream"
+  of Z_STREAM_END:
+    reader.strmend = true
+  of Z_OK:
+    if reader.strm.avail_out == 0:
+      reader.err "not enough space for output; is width or height wrong?"
+  else: doAssert false
+  reader.idatAt = int(reader.strm.total_out)
+  reader.i = reader.limit
+  let maxline = reader.idatAt div int(reader.scanlen)
+  let bmp = reader.bmp
+  let bps = if reader.bitDepth <= 8: 1 else: 2 # else 16 bit
+  let bpp = bps * reader.spp
+  let sl = int(reader.scanlen)
+  for y in reader.atline ..< maxline:
+    let yi = y * sl
+    assert yi + sl - 1 < reader.idatAt
+    reader.unfilter(toOpenArray(reader.idatBuf, yi, yi + sl - 1), bpp)
+    if unlikely(reader.bmp == nil): return
+    let yj = y * reader.width
+    reader.writepxs(toOpenArray(bmp.px, yj, yj + reader.width - 1))
+
+proc readIEND(reader: var PNGReader) =
+  if reader.i < reader.limit:
+    reader.err "IEND too long"
+  reader.isend = true
+
+proc readUnknown(reader: var PNGReader, s: string) =
+  if (int(s[0]) and 0x20) == 0:
+    reader.err "unrecognized critical chunk " & s
+  #else: eprint "warning: unknown chunk " & s #debug
+  reader.i = reader.limit
+
+proc zlibAlloc(opaque: pointer, items: cuint, size: cuint): pointer {.cdecl.} =
+  return alloc(items * size)
+
+proc zlibFree(opaque: pointer, address: pointer) {.cdecl.} =
+  dealloc(address)
+
+proc initZStream(reader: var PNGReader) =
+  let bps = max(int(reader.bitDepth) div 8, 1)
+  reader.idatBuf = newSeq[uint8](reader.scanlen * reader.height * bps)
+  reader.uprow = newSeq[uint8](reader.width * bps)
+  reader.strm = z_stream(
+    zalloc: zlibAlloc,
+    zfree: zlibFree
+  )
+  let ret = inflateInit(addr reader.strm)
+  if ret != Z_OK:
+    reader.err "failed to init inflate: " & $ret
+  reader.hasstrm = true
+
+proc fromPNG*(iq: openArray[uint8]): Bitmap =
+  if iq.len == 0: return
+  var reader = PNGReader(
+    iq: cast[ptr UncheckedArray[uint8]](unsafeAddr iq[0]),
+    limit: iq.len
+  )
+  reader.readIHDR()
+  if reader.bmp == nil: return
+  if reader.width == 0 or reader.height == 0:
+    reader.err "invalid zero sized png"
+  if reader.colorType != GRAYSCALE:
+    reader.err "only grayscale is implemented"
+  reader.initZStream()
+  while reader.i < iq.len and not reader.isend:
+    let len = int(reader.readPNGInt())
+    if reader.i + len > iq.len:
+      reader.err "chunk too long"
+    let j = reader.i
+    let t = reader.readStr(4)
+    reader.limit = reader.i + len
+    case t
+    of "IHDR": reader.err "IHDR expected to be first chunk"
+    of "IDAT": reader.readIDAT()
+    of "IEND": reader.readIEND()
+    of "bKGD": reader.readbKGD()
+    else: reader.readUnknown(t)
+    if reader.bmp == nil: return
+    let crc = crc32(0, unsafeAddr iq[j], cuint(len + 4))
+    reader.limit = iq.len
+    let y = reader.readU32()
+    if uint32(crc) != y:
+      reader.err "wrong crc"
+  if not reader.isend:
+    reader.err "IEND not found"
+  return reader.bmp
+
+const unifont = readFile"res/unifont_jp-15.0.05.png"
+var unifontBitmap: Bitmap
+var glyphCache: seq[tuple[u: uint32, bmp: Bitmap]]
+var glyphCacheI = 0
+proc getCharBmp(u: uint32): Bitmap =
+  # We only have the BMP.
+  let u = if u <= 0xFFFF: u else: 0xFFFD
+  if unifontBitmap == nil:
+    unifontBitmap = fromPNG(toOpenArrayByte(unifont, 0, unifont.high))
+  for (cu, bmp) in glyphCache:
+    if cu == u:
+      return bmp
+  # Unifont glyphs start at x: 32, y: 64, and are of 8x16/16x16 size
+  let gx = uint64(32 + 16 * (u mod 0xFF))
+  let gy = uint64(64 + 16 * (u div 0xFF))
+  var fullwidth = false
+  const white = rgba(255, 255, 255, 255)
+  block loop:
+    # hack to recognize full width characters
+    for y in 0 ..< 16u64:
+      for x in 8 ..< 16u64:
+        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((u, bmp))
+  else:
+    glyphCache[glyphCacheI] = (u, 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 = uint64(p.x) + x
+      let ay = uint64(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: RGBAColor,
+    textAlign: CSSTextAlign) =
+  var w = 0f64
+  var glyphs: seq[Bitmap]
+  for r in text.runes:
+    let glyph = getCharBmp(uint32(r))
+    glyphs.add(glyph)
+    w += float64(glyph.width)
+  var x = x
+  #TODO rtl
+  case textAlign
+  of TEXT_ALIGN_LEFT, TEXT_ALIGN_START: discard
+  of TEXT_ALIGN_RIGHT, TEXT_ALIGN_END: x -= w
+  of TEXT_ALIGN_CENTER: x -= w / 2
+  else: doAssert false
+  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: RGBAColor,
+    textAlign: CSSTextAlign) =
+  #TODO
+  bmp.fillText(text, x, y, color, textAlign)
diff --git a/src/img/path.nim b/src/img/path.nim
new file mode 100644
index 00000000..4c112f28
--- /dev/null
+++ b/src/img/path.nim
@@ -0,0 +1,430 @@
+import algorithm
+import deques
+import math
+
+import types/line
+import types/vector
+
+type
+  Path* = ref object
+    subpaths: seq[Subpath]
+    needsNewSubpath: bool
+    tempClosed: bool
+
+  PathLines* = object
+    lines*: seq[LineSegment]
+    miny*: float64
+    maxy*: float64
+
+  PathSegmentType = enum
+    SEGMENT_STRAIGHT, SEGMENT_QUADRATIC, SEGMENT_BEZIER, SEGMENT_ARC,
+    SEGMENT_ELLIPSE
+
+  PathSegment = object
+    case t: PathSegmentType
+    of SEGMENT_QUADRATIC:
+      cp: Vector2D
+    of SEGMENT_BEZIER:
+      cp0: Vector2D
+      cp1: Vector2D
+    of SEGMENT_ARC:
+      oa: Vector2D
+      r: float64
+      ia: bool
+    of SEGMENT_ELLIPSE:
+      oe: Vector2D
+      rx: float64
+      ry: float64
+    else: discard
+
+  Subpath* = object
+    points: seq[Vector2D]
+    segments: seq[PathSegment]
+    closed: bool
+
+proc newPath*(): Path =
+  return Path(
+    needsNewSubpath: true
+  )
+
+proc addSubpathAt(path: Path, p: Vector2D) =
+  path.subpaths.add(Subpath(points: @[p]))
+
+proc addSegment(path: Path, segment: PathSegment, p: Vector2D) =
+  path.subpaths[^1].segments.add(segment)
+  path.subpaths[^1].points.add(p)
+
+proc addStraightSegment(path: Path, p: Vector2D) =
+  let segment = PathSegment(t: SEGMENT_STRAIGHT)
+  path.addSegment(segment, p)
+
+proc addQuadraticSegment(path: Path, cp, p: Vector2D) =
+  let segment = PathSegment(
+    t: SEGMENT_QUADRATIC,
+    cp: cp
+  )
+  path.addSegment(segment, p)
+
+proc addBezierSegment(path: Path, cp0, cp1, p: Vector2D) =
+  let segment = PathSegment(
+    t: SEGMENT_BEZIER,
+    cp0: cp0,
+    cp1: cp1
+  )
+  path.addSegment(segment, p)
+
+# Goes from start tangent point to end tangent point
+proc addArcSegment(path: Path, o, etan: Vector2D, r: float64, ia: bool) =
+  let segment = PathSegment(
+    t: SEGMENT_ARC,
+    oa: o,
+    r: r,
+    ia: ia
+  )
+  path.addSegment(segment, etan)
+
+proc addEllipseSegment(path: Path, o, etan: Vector2D, rx, ry: float64) =
+  #TODO simplify to bezier?
+  let segment = PathSegment(
+    t: SEGMENT_ELLIPSE,
+    oe: o,
+    rx: rx,
+    ry: ry
+  )
+  path.addSegment(segment, etan)
+
+# https://hcklbrrfnn.files.wordpress.com/2012/08/bez.pdf
+func flatEnough(a, b, c: Vector2D): bool =
+  let ux = 3 * c.x - 2 * a.x - b.x
+  let uy = 3 * c.y - 2 * a.y - b.y
+  let vx = 3 * c.x - 2 * b.x - b.x
+  let vy = 3 * c.y - 2 * b.y - b.y
+  return max(ux * ux, vx * vx) + max(uy * uy, vy * vy) <= 0.02
+
+func flatEnough(a, b, c0, c1: Vector2D): bool =
+  let ux = 3 * c0.x - 2 * a.x - b.x
+  let uy = 3 * c0.y - 2 * a.y - b.y
+  let vx = 3 * c1.x - a.x - 2 * b.x
+  let vy = 3 * c1.y - a.y - 2 * b.y
+  return max(ux * ux, vx * vx) + max(uy * uy, vy * vy) <= 0.02
+
+iterator items*(pl: PathLines): LineSegment {.inline.} =
+  for line in pl.lines:
+    yield line
+
+func `[]`*(pl: PathLines, i: int): LineSegment = pl.lines[i]
+func `[]`*(pl: PathLines, i: BackwardsIndex): LineSegment = pl.lines[i]
+func `[]`*(pl: PathLines, s: Slice[int]): seq[LineSegment] = pl.lines[s]
+func len*(pl: PathLines): int = pl.lines.len
+
+iterator quadraticLines(a, b, c: Vector2D): Line {.inline.} =
+  var points: Deque[tuple[a, b, c: Vector2D]]
+  let tup = (a, b, c)
+  points.addFirst(tup)
+  while points.len > 2:
+    let (a, b, c) = points.popFirst()
+    if flatEnough(a, b, c):
+      yield Line(p0: a, p1: b)
+    else:
+      let mid1 = (c + a) / 2
+      let mid2 = (c + b) / 2
+      let s = (mid1 + mid2) / 2
+      points.addFirst((a, s, mid1))
+      points.addFirst((s, b, mid2))
+
+iterator bezierLines(p0, p1, c0, c1: Vector2D): Line {.inline.} =
+  var points: Deque[tuple[p0, p1, c0, c1: Vector2D]]
+  let tup = (p0, p1, c0, c1)
+  points.addLast(tup)
+  while points.len > 0:
+    let (p0, p1, c0, c1) = points.popFirst()
+    if flatEnough(p0, p1, c0, c1):
+      yield Line(p0: p0, p1: p1)
+    else:
+      let mida1 = (p0 + c0) / 2
+      let mida2 = (c0 + c1) / 2
+      let mida3 = (c1 + p1) / 2
+      let midb1 = (mida1 + mida2) / 2
+      let midb2 = (mida2 + mida3) / 2
+      let midc = (midb1 + midb2) / 2
+      points.addLast((p0, midc, mida1, midb1))
+      points.addLast((midc, p1, midb2, mida3))
+
+# https://stackoverflow.com/a/44829356
+func arcControlPoints(p1, p4, o: Vector2D): tuple[c0, c1: Vector2D] =
+  let a = p1 - o
+  let b = p4 - o
+  let q1 = a.x * a.x + a.y * a.y
+  let q2 = q1 + a.x * b.x + a.y * b.y
+  let k2 = (4 / 3) * (sqrt(2 * q1 * q2) - q2) / a.cross(b)
+  let c0 = o + a + Vector2D(x: -k2 * a.y, y:  k2 * a.x)
+  let c1 = o + b + Vector2D(x:  k2 * b.y, y: -k2 * b.x)
+  return (c0, c1)
+
+iterator arcLines(p0, p1, o: Vector2D, r: float64, i: bool): Line {.inline.} =
+  var p0 = p0
+  let pp0 = p0 - o
+  let pp1 = p1 - o
+  var theta = pp0.innerAngle(pp1)
+  if not i:
+    theta = PI * 2 - theta
+  while theta > 0:
+    let step = if theta > PI / 2: PI / 2 else: theta
+    var p1 = p0 - o
+    p1 = p1.rotate(step)
+    p1 += o
+    let (c0, c1) = arcControlPoints(p0, p1, o)
+    for line in bezierLines(p0, p1, c0, c1):
+      yield line
+    p0 = p1
+    theta -= step
+
+# From SerenityOS
+iterator ellipseLines(p0, p1, o: Vector2D, rx, ry, theta_1, rotx,
+    theta_delta: float64): Line {.inline.} =
+  if rx > 0 and ry > 0:
+    var s = p0
+    var e = p1
+    var theta_1 = theta_1
+    var theta_delta = theta_delta
+    if theta_delta < 0:
+      swap(s, e)
+      theta_1 += theta_delta
+      theta_delta = abs(theta_delta)
+    # The segments are at most 1 long
+    let step = arctan2(1f64, max(rx, ry))
+    var current_point = s - o
+    var next_point = Vector2D()
+    var theta = theta_1
+    while theta <= theta_1 + theta_delta:
+      next_point.x = rx * cos(theta)
+      next_point.y = ry * sin(theta)
+      next_point = next_point.rotate(rotx)
+      yield Line(p0: current_point + o, p1: next_point + o)
+      current_point = next_point
+      theta += step
+    yield Line(p0: current_point + o, p1: e)
+
+iterator lines(subpath: Subpath, i: int): Line {.inline.} =
+  let p0 = subpath.points[i]
+  let p1 = subpath.points[i + 1]
+  case subpath.segments[i].t
+  of SEGMENT_STRAIGHT:
+    yield Line(p0: p0, p1: p1)
+  of SEGMENT_QUADRATIC:
+    let c = subpath.segments[i].cp
+    for line in quadraticLines(p0, p1, c):
+      yield line
+  of SEGMENT_BEZIER:
+    let c0 = subpath.segments[i].cp0
+    let c1 = subpath.segments[i].cp1
+    for line in bezierLines(p0, p1, c0, c1):
+      yield line
+  of SEGMENT_ARC:
+    let o = subpath.segments[i].oa
+    let r = subpath.segments[i].r
+    let i = subpath.segments[i].ia
+    for line in arcLines(p0, p1, o, r, i):
+      yield line
+  of SEGMENT_ELLIPSE:
+    discard #TODO
+
+iterator lines*(path: Path): Line {.inline.} =
+  for subpath in path.subpaths:
+    assert subpath.points.len == subpath.segments.len + 1
+    for i in 0 ..< subpath.segments.len:
+      for line in subpath.lines(i):
+        if line.p0 == line.p1:
+          continue
+        yield line
+
+proc getLineSegments*(path: Path): PathLines =
+  if path.subpaths.len == 0:
+    return
+  var miny = Inf
+  var maxy = -Inf
+  var segments: seq[LineSegment]
+  for line in path.lines:
+    let ls = LineSegment(line)
+    miny = min(miny, ls.miny)
+    maxy = max(maxy, ls.maxy)
+    segments.add(ls)
+  segments.sort(cmpLineSegmentY)
+  return PathLines(
+    miny: miny,
+    maxy: maxy,
+    lines: segments
+  )
+
+proc moveTo(path: Path, v: Vector2D) =
+  path.addSubpathAt(v)
+  path.needsNewSubpath = false #TODO TODO TODO ???? why here
+
+proc beginPath*(path: Path) =
+  path.subpaths.setLen(0)
+
+proc moveTo*(path: Path, x, y: float64) =
+  for v in [x, y]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  path.moveTo(Vector2D(x: x, y: y))
+
+proc ensureSubpath(path: Path, x, y: float64) =
+  if path.needsNewSubpath:
+    path.moveTo(x, y)
+    path.needsNewSubpath = false
+
+proc closePath*(path: Path) =
+  let lsp = path.subpaths[^1]
+  if path.subpaths.len > 0 and (lsp.points.len > 0 or lsp.closed):
+    path.subpaths[^1].closed = true
+    path.addSubpathAt(path.subpaths[^1].points[0])
+
+#TODO this is a hack, and breaks as soon as any draw command is issued
+# between tempClosePath and tempOpenPath
+proc tempClosePath*(path: Path) =
+  if path.subpaths.len > 0 and not path.subpaths[^1].closed:
+    path.subpaths[^1].closed = true
+    let lsp = path.subpaths[^1]
+    path.addSubpathAt(lsp.points[^1])
+    path.addStraightSegment(lsp.points[0])
+    path.tempClosed = true
+
+proc tempOpenPath*(path: Path) =
+  if path.tempClosed:
+    path.subpaths.setLen(path.subpaths.len - 1)
+    path.subpaths[^1].closed = false
+    path.tempClosed = false
+
+proc lineTo*(path: Path, x, y: float64) =
+  for v in [x, y]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  if path.subpaths.len == 0:
+    path.ensureSubpath(x, y)
+  else:
+    path.addStraightSegment(Vector2D(x: x, y: y))
+
+proc quadraticCurveTo*(path: Path, cpx, cpy, x, y: float64) =
+  for v in [cpx, cpy, x, y]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  path.ensureSubpath(cpx, cpy)
+  let cp = Vector2D(x: cpx, y: cpy)
+  let p = Vector2D(x: x, y: y)
+  path.addQuadraticSegment(cp, p)
+
+proc bezierCurveTo*(path: Path, cp0x, cp0y, cp1x, cp1y, x, y: float64) =
+  for v in [cp0x, cp0y, cp1x, cp1y, x, y]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  path.ensureSubpath(cp0x, cp0y)
+  let cp0 = Vector2D(x: cp0x, y: cp0y)
+  let cp1 = Vector2D(x: cp1x, y: cp1y)
+  let p = Vector2D(x: x, y: y)
+  path.addBezierSegment(cp0, cp1, p)
+
+proc arcTo*(path: Path, x1, y1, x2, y2, radius: float64): bool =
+  for v in [x1, y1, x2, y2, radius]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  if radius < 0:
+    return false
+  path.ensureSubpath(x1, y1)
+  #TODO this should be transformed by the inverse of the transformation matrix
+  let v0 = path.subpaths[^1].points[^1]
+  let v1 = Vector2D(x: x1, y: y1)
+  let v2 = Vector2D(x: x2, y: y2)
+  if v0.x == x1 and v0.y == y1 or x1 == x2 and y1 == y2 or radius == 0:
+    path.addStraightSegment(v1)
+  elif collinear(v0, v1, v2):
+    path.addStraightSegment(v1)
+  else:
+    let pv0 = v0 - v1
+    let pv2 = v2 - v1
+    let tv0 = v1 + pv0 * radius * 2 / pv0.norm()
+    let tv2 = v1 + pv2 * radius * 2 / pv2.norm()
+    let q = -(pv0.x * tv0.x + pv0.y * tv0.y)
+    let p = -(pv2.x * tv2.x + pv2.y * tv2.y)
+    let cr = pv0.cross(pv2)
+    let origin = Vector2D(
+      x: (pv0.y * p - pv2.y * q) / cr,
+      y: (pv2.x * q - pv0.x * p) / cr
+    )
+    path.addStraightSegment(tv0)
+    path.addArcSegment(origin, tv2, radius, true) #TODO always inner?
+  return true
+
+func resolveEllipsePoint(o: Vector2D, angle, radiusX, radiusY,
+    rotation: float64): Vector2D =
+  # Stolen from SerenityOS
+  let tanrel = tan(angle)
+  let tan2 = tanrel * tanrel
+  let ab = radiusX * radiusY
+  let a2 = radiusX * radiusX
+  let b2 = radiusY * radiusY
+  let sq = sqrt(b2 + a2 * tan2)
+  let sn = if cos(angle) >= 0: 1f64 else: -1f64
+  let relx = ab / sq * sn
+  let rely = ab * tanrel / sq * sn
+  return Vector2D(x: relx, y: rely).rotate(rotation) + o
+
+proc arc*(path: Path, x, y, radius, startAngle, endAngle: float64,
+    counterclockwise: bool): bool =
+  for v in [x, y, radius, startAngle, endAngle]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  if radius < 0:
+    return false
+  let o = Vector2D(x: x, y: y)
+  var s = resolveEllipsePoint(o, startAngle, radius, radius, 0)
+  var e = resolveEllipsePoint(o, endAngle, radius, radius, 0)
+  if counterclockwise:
+    let tmp = s
+    e = s
+    s = tmp
+  if path.subpaths.len > 0:
+    path.addStraightSegment(s)
+  else:
+    path.moveTo(s)
+  path.addArcSegment(o, e, radius, abs(startAngle - endAngle) < PI)
+  return true
+
+proc ellipse*(path: Path, x, y, radiusX, radiusY, rotation, startAngle,
+    endAngle: float64, counterclockwise: bool): bool =
+  for v in [x, y, radiusX, radiusY, rotation, startAngle, endAngle]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  if radiusX < 0 or radiusY < 0:
+    return false
+  let o = Vector2D(x: x, y: y)
+  var s = resolveEllipsePoint(o, startAngle, radiusX, radiusY, rotation)
+  var e = resolveEllipsePoint(o, endAngle, radiusX, radiusY, rotation)
+  if counterclockwise:
+    let tmp = s
+    e = s
+    s = tmp
+  if path.subpaths.len > 0:
+    path.addStraightSegment(s)
+  else:
+    path.moveTo(s)
+  path.addEllipseSegment(o, e, radiusX, radiusY)
+  return true
+
+proc rect*(path: Path, x, y, w, h: float64) =
+  for v in [x, y, w, h]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  path.addSubpathAt(Vector2D(x: x, y: y))
+  path.addStraightSegment(Vector2D(x: x + w, y: y))
+  path.addStraightSegment(Vector2D(x: x + w, y: y + h))
+  path.addStraightSegment(Vector2D(x: x, y: y + h))
+  path.addStraightSegment(Vector2D(x: x, y: y))
+  path.addSubpathAt(Vector2D(x: x, y: y))
+
+proc roundRect*(path: Path, x, y, w, h, radii: float64) =
+  for v in [x, y, w, h]:
+    if classify(v) in {fcInf, fcNegInf, fcNan}:
+      return
+  #TODO implement
+  path.rect(x, y, w, h) # :P
diff --git a/src/io/http.nim b/src/io/http.nim
index 984cae7c..f9023f5f 100644
--- a/src/io/http.nim
+++ b/src/io/http.nim
@@ -5,6 +5,8 @@ import strutils
 import bindings/curl
 import io/request
 import ips/serialize
+import types/blob
+import types/formdata
 import types/url
 import utils/twtstr
 
@@ -89,7 +91,7 @@ proc applyPostBody(curl: CURL, request: Request, handleData: HandleData) =
       handleData.ostream.swrite(-1)
       handleData.ostream.flush()
       return
-    for entry in request.multipart.get.content:
+    for entry in request.multipart.get:
       let part = curl_mime_addpart(handleData.mime)
       if part == nil:
         # fail (TODO: raise?)
@@ -97,16 +99,16 @@ proc applyPostBody(curl: CURL, request: Request, handleData: HandleData) =
         handleData.ostream.flush()
         return
       curl_mime_name(part, cstring(entry.name))
-      if entry.isFile:
-        if entry.isStream:
-          curl_mime_filedata(part, cstring(entry.filename))
+      if entry.isstr:
+        curl_mime_data(part, cstring(entry.svalue), csize_t(entry.svalue.len))
+      else:
+        let blob = entry.value
+        if blob.isfile: #TODO ?
+          curl_mime_filedata(part, cstring(WebFile(blob).path))
         else:
-          let fd = readFile(entry.filename)
-          curl_mime_data(part, cstring(fd), csize_t(fd.len))
+          curl_mime_data(part, blob.buffer, csize_t(blob.size))
         # may be overridden by curl_mime_filedata, so set it here
         curl_mime_filename(part, cstring(entry.filename))
-      else:
-        curl_mime_data(part, cstring(entry.content), csize_t(entry.content.len))
     curl.setopt(CURLOPT_MIMEPOST, handleData.mime)
   elif request.body.issome:
     curl.setopt(CURLOPT_POSTFIELDS, cstring(request.body.get))
diff --git a/src/io/request.nim b/src/io/request.nim
index 531a2fc3..76fd9fc4 100644
--- a/src/io/request.nim
+++ b/src/io/request.nim
@@ -4,6 +4,7 @@ import strutils
 import tables
 
 import bindings/quickjs
+import types/formdata
 import types/url
 import js/javascript
 import utils/twtstr
@@ -66,7 +67,7 @@ type
     url*: Url
     headers* {.jsget.}: Headers
     body*: Option[string]
-    multipart*: Option[MimeData]
+    multipart*: Option[FormData]
     referer*: URL
     mode* {.jsget.}: RequestMode
     destination* {.jsget.}: RequestDestination
@@ -92,19 +93,6 @@ type
   Headers* = ref object
     table* {.jsget.}: Table[string, seq[string]]
 
-# Originally from the stdlib
-  MimePart* = object
-    name*, content*: string
-    case isFile*: bool
-    of true:
-      filename*, contentType*: string
-      fileSize*: int64
-      isStream*: bool
-    else: discard
-
-  MimeData* = object
-    content*: seq[MimePart]
-
 proc Request_url(ctx: JSContext, this: JSValue, magic: cint): JSValue {.cdecl.} =
   let op = getOpaque0(this)
   if unlikely(not ctx.isInstanceOf(this, "Request") or op == nil):
@@ -213,31 +201,30 @@ func newHeaders*(table: Table[string, string]): Headers =
       result.table[k] = @[v]
 
 func newRequest*(url: URL, httpmethod = HTTP_GET, headers = newHeaders(),
-                 body = none(string), # multipart = none(MimeData),
-                 mode = RequestMode.NO_CORS,
-                 credentialsMode = CredentialsMode.SAME_ORIGIN,
-                 destination = RequestDestination.NO_DESTINATION,
-                 proxy: URL = nil): Request =
+    body = none(string), multipart = none(FormData), mode = RequestMode.NO_CORS,
+    credentialsMode = CredentialsMode.SAME_ORIGIN,
+    destination = RequestDestination.NO_DESTINATION, proxy: URL = nil): Request =
   return Request(
     url: url,
     httpmethod: httpmethod,
     headers: headers,
     body: body,
-    #multipart: multipart,
+    multipart: multipart,
     mode: mode,
     credentialsMode: credentialsMode,
     destination: destination,
     proxy: proxy
   )
 
-func newRequest*(url: URL, httpmethod = HTTP_GET, headers: seq[(string, string)] = @[],
-                 body = none(string), # multipart = none(MimeData), TODO TODO TODO multipart
-                 mode = RequestMode.NO_CORS, proxy: URL = nil): Request =
+func newRequest*(url: URL, httpmethod = HTTP_GET,
+    headers: seq[(string, string)] = @[], body = none(string),
+    multipart = none(FormData), mode = RequestMode.NO_CORS, proxy: URL = nil):
+    Request =
   let hl = newHeaders()
   for pair in headers:
     let (k, v) = pair
     hl.table[k] = @[v]
-  return newRequest(url, httpmethod, hl, body, mode, proxy = proxy)
+  return newRequest(url, httpmethod, hl, body, multipart, mode, proxy = proxy)
 
 func createPotentialCORSRequest*(url: URL, destination: RequestDestination, cors: CORSAttribute, fallbackFlag = false): Request =
   var mode = if cors == NO_CORS:
@@ -257,16 +244,23 @@ func createPotentialCORSRequest*(url: URL, destination: RequestDestination, cors
 func newRequest*(resource: string, init: JSObject): Request {.jserr, jsctor.} =
   let x = parseURL(resource)
   if x.isNone:
-    JS_ERR JS_TypeError, resource & " is not a valid URL"
+    JS_ERR JS_TypeError, resource & " is not a valid URL."
   if x.get.username != "" or x.get.password != "":
-    JS_ERR JS_TypeError, resource & " is not a valid URL"
+    JS_ERR JS_TypeError, resource & " is not a valid URL."
   let url = x.get
   let ctx = init.ctx
   let fallbackMode = some(RequestMode.CORS) #TODO none if resource is request
   #TODO fallback mode, origin, window, request mode, ...
   let httpMethod = fromJS[HttpMethod](ctx,
     JS_GetPropertyStr(ctx, init.val, "method")).get(HTTP_GET)
-  let body = fromJS[string](ctx, JS_GetPropertyStr(ctx, init.val, "body"))
+  let bodyProp = JS_GetPropertyStr(ctx, init.val, "body")
+  let multipart = fromJS[FormData](ctx, bodyProp)
+  var body: Option[string]
+  if multipart.isNone:
+    body = fromJS[string](ctx, bodyProp)
+  #TODO inputbody
+  if (multipart.isSome or body.isSome) and httpMethod in {HTTP_GET, HTTP_HEAD}:
+    JS_ERR JS_TypeError, "HEAD or GET Request cannot have a body."
   let jheaders = JS_GetPropertyStr(ctx, init.val, "headers")
   let hl = newHeaders()
   hl.fill(ctx, jheaders)
@@ -276,10 +270,7 @@ func newRequest*(resource: string, init: JSObject): Request {.jserr, jsctor.} =
     .get(fallbackMode.get(RequestMode.NO_CORS))
   #TODO find a standard compatible way to implement this
   let proxyUrl = fromJS[URL](ctx, JS_GetPropertyStr(ctx, init.val, "proxyUrl"))
-  return newRequest(url, httpMethod, hl, body, mode, credentials, proxy = proxyUrl.get(nil))
-
-proc `[]=`*(multipart: var MimeData, k, v: string) =
-  multipart.content.add(MimePart(name: k, content: v))
+  return newRequest(url, httpMethod, hl, body, multipart, mode, credentials, proxy = proxyUrl.get(nil))
 
 proc add*(headers: var Headers, k, v: string) =
   let k = k.toHeaderCase()
diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim
index 0636e2e9..c2a6f5a6 100644
--- a/src/ips/serialize.nim
+++ b/src/ips/serialize.nim
@@ -7,7 +7,9 @@ import tables
 
 import io/request
 import js/regex
+import types/blob
 import types/buffersource
+import types/formdata
 import types/url
 
 proc swrite*(stream: Stream, n: SomeNumber)
@@ -54,9 +56,13 @@ proc swrite*(stream: Stream, obj: ref object)
 proc sread*(stream: Stream, obj: var ref object)
 func slen*(obj: ref object): int
 
-proc swrite*(stream: Stream, part: MimePart)
-proc sread*(stream: Stream, part: var MimePart)
-func slen*(part: MimePart): int
+proc swrite*(stream: Stream, part: FormDataEntry)
+proc sread*(stream: Stream, part: var FormDataEntry)
+func slen*(part: FormDataEntry): int
+
+proc swrite*(stream: Stream, blob: Blob)
+proc sread*(stream: Stream, blob: var Blob)
+func slen*(blob: Blob): int
 
 proc swrite*[T](stream: Stream, o: Option[T])
 proc sread*[T](stream: Stream, o: var Option[T])
@@ -242,40 +248,57 @@ func slen*(obj: ref object): int =
   if obj != nil:
     result += slen(obj[])
 
-proc swrite*(stream: Stream, part: MimePart) =
-  stream.swrite(part.isFile)
+proc swrite*(stream: Stream, part: FormDataEntry) =
+  stream.swrite(part.isstr)
   stream.swrite(part.name)
-  stream.swrite(part.content)
-  if part.isFile:
-    stream.swrite(part.filename)
-    stream.swrite(part.contentType)
-    stream.swrite(part.fileSize)
-    stream.swrite(part.isStream)
-
-proc sread*(stream: Stream, part: var MimePart) =
-  var isFile: bool
-  stream.sread(isFile)
-  if isFile:
-    part = MimePart(isFile: true)
+  stream.swrite(part.filename)
+  if part.isstr:
+    stream.swrite(part.svalue)
+  else:
+    stream.swrite(part.value)
+
+proc sread*(stream: Stream, part: var FormDataEntry) =
+  var isstr: bool
+  stream.sread(isstr)
+  if isstr:
+    part = FormDataEntry(isstr: true)
   else:
-    part = MimePart(isFile: false)
+    part = FormDataEntry(isstr: false)
   stream.sread(part.name)
-  stream.sread(part.content)
-  if part.isFile:
-    stream.sread(part.filename)
-    stream.sread(part.contentType)
-    stream.sread(part.fileSize)
-    stream.sread(part.isStream)
-
-func slen*(part: MimePart): int =
-  result += slen(part.isFile)
+  stream.sread(part.filename)
+  if part.isstr:
+    stream.sread(part.svalue)
+  else:
+    stream.sread(part.value)
+
+func slen*(part: FormDataEntry): int =
+  result += slen(part.isstr)
   result += slen(part.name)
-  result += slen(part.content)
-  if part.isFile:
-    result += slen(part.filename)
-    result += slen(part.contentType)
-    result += slen(part.fileSize)
-    result += slen(part.isStream)
+  result += slen(part.filename)
+  if part.isstr:
+    result += slen(part.svalue)
+  else:
+    result += slen(part.value)
+
+proc swrite*(stream: Stream, blob: Blob) =
+  stream.swrite(blob.ctype)
+  stream.swrite(blob.size)
+  #TODO ??
+  stream.writeData(blob.buffer, int(blob.size))
+
+proc sread*(stream: Stream, blob: var Blob) =
+  new(blob)
+  stream.sread(blob.ctype)
+  stream.sread(blob.size)
+  blob.buffer = alloc(blob.size)
+  blob.deallocFun = dealloc
+  #TODO ??
+  assert stream.readData(blob.buffer, int(blob.size)) == int(blob.size)
+
+func slen*(blob: Blob): int =
+  result += slen(blob.ctype)
+  result += slen(blob.size)
+  result += int(blob.size) #TODO ??
 
 proc swrite*[T](stream: Stream, o: Option[T]) =
   stream.swrite(o.issome)
diff --git a/src/js/intl.nim b/src/js/intl.nim
new file mode 100644
index 00000000..28cc42d5
--- /dev/null
+++ b/src/js/intl.nim
@@ -0,0 +1,46 @@
+# Very minimal Intl module... TODO make it more complete
+
+import bindings/quickjs
+import js/javascript
+
+type
+  NumberFormat = ref object
+
+#TODO ...yeah
+proc newNumberFormat(name: string = "en-US",
+    options = none(JSObject)): NumberFormat {.jsctor.} =
+  return NumberFormat()
+
+#TODO: this should accept string/BigInt too
+proc format(nf: NumberFormat, num: float64): string {.jsfunc.} =
+  let s = $num
+  var i = 0
+  var L = s.len
+  for k in countdown(s.high, 0):
+    if s[k] == '.':
+      L = k
+      break
+  if L mod 3 != 0:
+    while i < L mod 3:
+      result &= s[i]
+      inc i
+    if i < L:
+      result &= ','
+  let j = i
+  while i < L:
+    if j != i and i mod 3 == j:
+      result &= ','
+    result &= s[i]
+    inc i
+  if i + 1 < s.len and s[i] == '.':
+    if not (s[i + 1] == '0' and s.len == i + 2):
+      while i < s.len:
+        result &= s[i]
+        inc i
+
+proc addIntlModule*(ctx: JSContext) =
+  let global = JS_GetGlobalObject(ctx)
+  let intl = JS_NewObject(ctx)
+  ctx.registerType(NumberFormat, namespace = intl)
+  ctx.defineProperty(global, "Intl", intl)
+  JS_FreeValue(ctx, global)
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index 18ff3850..adf58ee4 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -5,7 +5,7 @@
 # around each bound function call, so it shouldn't be too difficult to get it
 # working. (This would involve generating JS functions in registerType.)
 # Now for the pragmas:
-# {.jsctr.} for constructors. These need no `this' value, and are bound as
+# {.jsctor.} for constructors. These need no `this' value, and are bound as
 #   regular constructors in JS. They must return a ref object, which will have
 #   a JS counterpart too. (Other functions can return ref objects too, which
 #   will either use the existing JS counterpart, if exists, or create a new
@@ -293,7 +293,8 @@ proc definePropertyCWE*[T](ctx: JSContext, this: JSValue, name: string,
 func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, tname: string,
                  ctor: JSCFunction, funcs: JSFunctionList, nimt: pointer,
                  parent: JSClassID, asglobal: bool, nointerface: bool,
-                 finalizer: proc(val: JSValue)): JSClassID {.discardable.} =
+                 finalizer: proc(val: JSValue),
+                 namespace: JSValue): JSClassID {.discardable.} =
   let rt = JS_GetRuntime(ctx)
   discard JS_NewClassID(addr result)
   var ctxOpaque = ctx.getOpaque()
@@ -334,9 +335,12 @@ func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, tname: string,
   JS_SetConstructor(ctx, jctor, proto)
   ctxOpaque.ctors[result] = JS_DupValue(ctx, jctor)
   if not nointerface:
-    let global = JS_GetGlobalObject(ctx)
-    ctx.defineProperty(global, $cdef.class_name, jctor)
-    JS_FreeValue(ctx, global)
+    if namespace == JS_NULL:
+      let global = JS_GetGlobalObject(ctx)
+      ctx.defineProperty(global, $cdef.class_name, jctor)
+      JS_FreeValue(ctx, global)
+    else:
+      ctx.defineProperty(namespace, $cdef.class_name, jctor)
 
 type FuncParam = tuple[name: string, t: NimNode, val: Option[NimNode], generic: Option[NimNode]]
 
@@ -576,10 +580,12 @@ proc fromJSTable[A, B](ctx: JSContext, val: JSValue): Option[Table[A, B]] =
 proc toJS*(ctx: JSContext, s: cstring): JSValue
 proc toJS*(ctx: JSContext, s: string): JSValue
 proc toJS(ctx: JSContext, r: Rune): JSValue
-proc toJS(ctx: JSContext, n: int64): JSValue
-proc toJS(ctx: JSContext, n: int32): JSValue
+proc toJS*(ctx: JSContext, n: int64): JSValue
+proc toJS*(ctx: JSContext, n: int32): JSValue
 proc toJS*(ctx: JSContext, n: int): JSValue
-proc toJS(ctx: JSContext, n: uint32): JSValue
+proc toJS*(ctx: JSContext, n: uint16): JSValue
+proc toJS*(ctx: JSContext, n: uint32): JSValue
+proc toJS*(ctx: JSContext, n: uint64): JSValue
 proc toJS(ctx: JSContext, n: SomeFloat): JSValue
 proc toJS*(ctx: JSContext, b: bool): JSValue
 proc toJS[U, V](ctx: JSContext, t: Table[U, V]): JSValue
@@ -666,7 +672,7 @@ proc fromJS*[T](ctx: JSContext, val: JSValue): Option[T] =
       return fromJSInt[T](ctx, val)
   elif T is SomeFloat:
     if JS_IsNumber(val):
-      let f64: float64
+      var f64: float64
       if JS_ToFloat64(ctx, addr f64, val) < 0:
         return none(T)
       return some(cast[T](f64))
@@ -724,19 +730,26 @@ proc toJS*(ctx: JSContext, s: string): JSValue =
 proc toJS(ctx: JSContext, r: Rune): JSValue =
   return toJS(ctx, $r)
 
-proc toJS(ctx: JSContext, n: int32): JSValue =
+proc toJS*(ctx: JSContext, n: int32): JSValue =
   return JS_NewInt32(ctx, n)
 
-proc toJS(ctx: JSContext, n: int64): JSValue =
+proc toJS*(ctx: JSContext, n: int64): JSValue =
   return JS_NewInt64(ctx, n)
 
 # Always int32, so we don't risk 32-bit only breakage.
 proc toJS*(ctx: JSContext, n: int): JSValue =
   return toJS(ctx, int32(n))
 
-proc toJS(ctx: JSContext, n: uint32): JSValue =
+proc toJS*(ctx: JSContext, n: uint16): JSValue =
+  return JS_NewUint32(ctx, uint32(n))
+
+proc toJS*(ctx: JSContext, n: uint32): JSValue =
   return JS_NewUint32(ctx, n)
 
+proc toJS*(ctx: JSContext, n: uint64): JSValue =
+  #TODO this is incorrect
+  return JS_NewFloat64(ctx, float64(n))
+
 proc toJS(ctx: JSContext, n: SomeFloat): JSValue =
   return JS_NewFloat64(ctx, float64(n))
 
@@ -837,9 +850,10 @@ type
     funcName: string
     generics: Table[string, seq[NimNode]]
     funcParams: seq[FuncParam]
+    passCtx: bool
     thisType: string
     returnType: Option[NimNode]
-    newName: string
+    newName: NimNode
     newBranchList: seq[NimNode]
     errval: NimNode # JS_EXCEPTION or -1
     dielabel: NimNode # die: didn't match parameters, but could still match other ones
@@ -922,8 +936,7 @@ proc getParams(fun: NimNode): seq[FuncParam] =
       t = quote do:
         typeof(`x`)
     else:
-      eprint treeRepr it
-      error("??")
+      error("?? " & treeRepr(it))
     let val = if it[^1].kind != nnkEmpty:
       let x = it[^1]
       some(newPar(x))
@@ -1182,6 +1195,9 @@ proc addOptionalParams(gen: var JSFuncGenerator) =
           )
         ))
     else:
+      if gen.funcParams[gen.i][2].isNone:
+        error("No fallback value. Maybe a non-optional parameter follows an " &
+          "optional parameter?")
       let fallback = gen.funcParams[gen.i][2].get
       if tt.typeKind == ntyGenericParam:
         gen.addUnionParam(tt, s, fallback)
@@ -1208,11 +1224,11 @@ proc registerFunction(typ: string, t: BoundFunctionType, name: string, id: NimNo
   existing_funcs.incl(id.strVal)
 
 proc registerConstructor(gen: JSFuncGenerator) =
-  registerFunction(gen.thisType, gen.t, gen.funcName, ident(gen.newName))
+  registerFunction(gen.thisType, gen.t, gen.funcName, gen.newName)
   js_funcs[gen.funcName] = gen
 
 proc registerFunction(gen: JSFuncGenerator) =
-  registerFunction(gen.thisType, gen.t, gen.funcName, ident(gen.newName))
+  registerFunction(gen.thisType, gen.t, gen.funcName, gen.newName)
 
 var js_errors {.compileTime.}: Table[string, seq[string]]
 
@@ -1222,13 +1238,18 @@ export JS_ThrowTypeError, JS_ThrowRangeError, JS_ThrowSyntaxError,
 proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode =
   let tt = gen.thisType
   let fn = gen.funcName
-  let ma = if gen.thisname.isSome: gen.minArgs - 1 else: gen.minArgs
+  var ma = gen.minArgs
+  if gen.thisname.isSome:
+    ma -= 1
+  if gen.passCtx:
+    ma -= 1
+  let pctx = gen.passCtx
   assert ma >= 0
   result = newStmtList()
   if isva:
     result.add(quote do: 
       if argc < `ma`:
-        return JS_ThrowTypeError(ctx, "At least %d arguments required, but only %d passed", `ma`, argc)
+        return JS_ThrowTypeError(ctx, "At least %d arguments required, but only %d passed %d", `ma`, argc, `pctx`)
     )
   if gen.thisname.isSome:
     let tn = ident(gen.thisname.get)
@@ -1258,7 +1279,7 @@ proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode =
 proc newJSProc(gen: var JSFuncGenerator, params: openArray[NimNode], isva = true): NimNode =
   let jsBody = gen.newJSProcBody(isva)
   let jsPragmas = newNimNode(nnkPragma).add(ident("cdecl"))
-  result = newProc(ident(gen.newName), params, jsBody, pragmas = jsPragmas)
+  result = newProc(gen.newName, params, jsBody, pragmas = jsPragmas)
   gen.res = result
 
 # WARNING: for now, this only works correctly when the .jserr pragma was
@@ -1271,35 +1292,60 @@ macro JS_ERR*(a: typed, b: string) =
     block when_js:
       raise newException(`a`, `b`)
 
-proc setupGenerator(fun: NimNode, t: BoundFunctionType, thisname = some("this")): JSFuncGenerator =
-  result.t = t
-  result.funcName = $fun[0]
-  if result.funcName == "$":
+func getFuncName(fun: NimNode, jsname: string): string =
+  if jsname != "":
+    return jsname
+  let x = $fun[0]
+  if x == "$":
     # stringifier
-    result.funcName = "toString"
-  result.generics = getGenerics(fun)
-  result.funcParams = getParams(fun)
-  result.returnType = getReturn(fun)
-  result.minArgs = result.funcParams.getMinArgs()
-  result.original = fun
-  result.thisname = thisname
+    return "toString"
+  return x
+
+func getErrVal(t: BoundFunctionType): NimNode =
   if t in {PROPERTY_GET, PROPERTY_HAS}:
-    result.errval = quote do: cint(-1)
-  else:
-    result.errval = quote do: JS_EXCEPTION
-  result.dielabel = ident("ondie")
-  result.jsFunCallList = newStmtList()
-  result.jsFunCallLists.add(result.jsFunCallList)
-  result.jsFunCall = newCall(fun[0])
+    return quote do: cint(-1)
+  return quote do: JS_EXCEPTION
+
+proc addJSContext(gen: var JSFuncGenerator) =
+  if gen.funcParams.len > gen.i and
+      gen.funcParams[gen.i].t.eqIdent(ident("JSContext")):
+    gen.passCtx = true
+    gen.jsFunCall.add(ident("ctx"))
+    inc gen.i
+
+proc addThisName(gen: var JSFuncGenerator, thisname: Option[string]) =
   if thisname.isSome:
-    result.thisType = $result.funcParams[0][1]
-    result.newName = $t & "_" & result.thisType & "_" & result.funcName
+    gen.thisType = $gen.funcParams[gen.i][1]
+    gen.newName = ident($gen.t & "_" & gen.thisType & "_" & gen.funcName)
   else:
-    if result.returnType.get.kind == nnkRefTy:
-      result.thisType = result.returnType.get[0].strVal
+    if gen.returnType.get.kind == nnkRefTy:
+      gen.thisType = gen.returnType.get[0].strVal
     else:
-      result.thisType = result.returnType.get.strVal
-    result.newName = $t & "_" & result.funcName
+      gen.thisType = gen.returnType.get.strVal
+    gen.newName = ident($gen.t & "_" & gen.funcName)
+
+proc setupGenerator(fun: NimNode, t: BoundFunctionType,
+    thisname = some("this"), jsname: string = ""): JSFuncGenerator =
+  let jsFunCallList = newStmtList()
+  let funcParams = getParams(fun)
+  var gen = JSFuncGenerator(
+    t: t,
+    funcName: getFuncName(fun, jsname),
+    generics: getGenerics(fun),
+    funcParams: funcParams,
+    returnType: getReturn(fun),
+    minArgs: funcParams.getMinArgs(),
+    original: fun,
+    thisname: thisname,
+    errval: getErrVal(t),
+    dielabel: ident("ondie"),
+    jsFunCallList: jsFunCallList,
+    jsFunCallLists: @[jsFunCallList],
+    jsFunCall: newCall(fun[0])
+  )
+  gen.addJSContext()
+  gen.addThisName(thisname)
+  return gen
 
 # this might be pretty slow...
 #TODO ideally we wouldn't need separate functions at all. Not sure how that
@@ -1354,7 +1400,7 @@ macro jserr*(fun: untyped) =
 
 macro jsctor*(fun: typed) =
   var gen = setupGenerator(fun, CONSTRUCTOR, thisname = none(string))
-  if gen.newName in existing_funcs:
+  if gen.newName.strVal in existing_funcs:
     #TODO TODO TODO implement function overloading
     error("Function overloading hasn't been implemented yet...")
   gen.addRequiredParams()
@@ -1372,7 +1418,7 @@ macro jsctor*(fun: typed) =
 
 macro jsgctor*(fun: typed) =
   var gen = setupGenerator(fun, CONSTRUCTOR, thisname = none(string))
-  if gen.newName in existing_funcs:
+  if gen.newName.strVal in existing_funcs:
     #TODO TODO TODO implement function overloading
     error("Function overloading hasn't been implemented yet...")
   gen.addFixParam("this")
@@ -1391,7 +1437,7 @@ macro jsgctor*(fun: typed) =
 
 macro jshasprop*(fun: typed) =
   var gen = setupGenerator(fun, PROPERTY_HAS, thisname = some("obj"))
-  if gen.newName in existing_funcs:
+  if gen.newName.strVal in existing_funcs:
     #TODO TODO TODO ditto
     error("Function overloading hasn't been implemented yet...")
   gen.addFixParam("obj")
@@ -1409,7 +1455,7 @@ macro jshasprop*(fun: typed) =
 
 macro jsgetprop*(fun: typed) =
   var gen = setupGenerator(fun, PROPERTY_GET, thisname = some("obj"))
-  if gen.newName in existing_funcs:
+  if gen.newName.strVal in existing_funcs:
     #TODO TODO TODO ditto
     error("Function overloading hasn't been implemented yet...")
   gen.addFixParam("obj")
@@ -1431,13 +1477,13 @@ macro jsgetprop*(fun: typed) =
   gen.registerFunction()
   result = newStmtList(fun, jsProc)
 
-macro jsfget*(fun: typed) =
-  var gen = setupGenerator(fun, GETTER)
+macro jsfgetn(jsname: static string, fun: typed) =
+  var gen = setupGenerator(fun, GETTER, jsname = jsname)
   if gen.minArgs != 1 or gen.funcParams.len != gen.minArgs:
     error("jsfget functions must only have one parameter.")
   if gen.returnType.isnone:
     error("jsfget functions must have a return type.")
-  if gen.newName in existing_funcs:
+  if gen.newName.strVal in existing_funcs:
     #TODO TODO TODO ditto
     error("Function overloading hasn't been implemented yet...")
   gen.addFixParam("this")
@@ -1451,8 +1497,17 @@ macro jsfget*(fun: typed) =
   gen.registerFunction()
   result = newStmtList(fun, jsProc)
 
-macro jsfset*(fun: typed) =
-  var gen = setupGenerator(fun, SETTER)
+# "Why?" So the compiler doesn't cry.
+macro jsfget*(fun: typed) =
+  quote do:
+    jsfgetn("", `fun`)
+
+macro jsfget*(jsname: static string, fun: typed) =
+  quote do:
+    jsfgetn(`jsname`, `fun`)
+
+macro jsfsetn(jsname: static string, fun: typed) =
+  var gen = setupGenerator(fun, SETTER, jsname = jsname)
   if gen.minArgs != 2 or gen.funcParams.len != gen.minArgs:
     error("jsfset functions must accept two parameters")
   if gen.returnType.issome:
@@ -1472,8 +1527,16 @@ macro jsfset*(fun: typed) =
   gen.registerFunction()
   result = newStmtList(fun, jsProc)
 
-macro jsfunc*(fun: typed) =
-  var gen = setupGenerator(fun, FUNCTION)
+macro jsfset*(fun: typed) =
+  quote do:
+    jsfsetn("", `fun`)
+
+macro jsfset*(jsname: static string, fun: typed) =
+  quote do:
+    jsfsetn(`jsname`, `fun`)
+
+macro jsfuncn*(jsname: static string, fun: typed) =
+  var gen = setupGenerator(fun, FUNCTION, jsname = jsname)
   if gen.minArgs == 0:
     error("Zero-parameter functions are not supported. (Maybe pass Window or Client?)")
   gen.addFixParam("this")
@@ -1496,15 +1559,25 @@ macro jsfunc*(fun: typed) =
   gen.registerFunction()
   result = newStmtList(fun, jsProc)
 
+macro jsfunc*(fun: typed) =
+  quote do:
+    jsfuncn("", `fun`)
+
+macro jsfunc*(jsname: static string, fun: typed) =
+  quote do:
+    jsfuncn(`jsname`, `fun`)
+
 macro jsfin*(fun: typed) =
   var gen = setupGenerator(fun, FINALIZER, thisname = some("fin"))
-  registerFunction(gen.thisType, FINALIZER, gen.funcName, ident(gen.newName))
+  registerFunction(gen.thisType, FINALIZER, gen.funcName, gen.newName)
   fun
 
 # Having the same names for these and the macros leads to weird bugs, so the
 # macros get an additional f.
 template jsget*() {.pragma.}
+template jsget*(name: string) {.pragma.}
 template jsset*() {.pragma.}
+template jsset*(name: string) {.pragma.}
 
 proc nim_finalize_for_js[T](obj: T) =
   for rt in runtimes:
@@ -1543,9 +1616,26 @@ proc nim_finalize_for_js[T](obj: T) =
 proc js_illegal_ctor*(ctx: JSContext, this: JSValue, argc: cint, argv: ptr JSValue): JSValue {.cdecl.} =
   return JS_ThrowTypeError(ctx, "Illegal constructor")
 
-type JSObjectPragmas = object
-  jsget: seq[NimNode]
-  jsset: seq[NimNode]
+type
+  JSObjectPragma = object
+    name: string
+    varsym: NimNode
+
+  JSObjectPragmas = object
+    jsget: seq[JSObjectPragma]
+    jsset: seq[JSObjectPragma]
+    jsinclude: seq[JSObjectPragma]
+
+func getPragmaName(varPragma: NimNode): string =
+  if varPragma.kind == nnkExprColonExpr:
+    return $varPragma[0]
+  return $varPragma
+
+func getStringFromPragma(varPragma: NimNode): Option[string] =
+  if varPragma.kind == nnkExprColonExpr:
+    if not varPragma.len == 1 and varPragma[1].kind == nnkStrLit:
+      error("Expected string as pragma argument")
+    return some($varPragma[1])
 
 proc findPragmas(t: NimNode): JSObjectPragmas =
   let typ = t.getTypeInst()[1] # The type, as declared.
@@ -1557,7 +1647,6 @@ proc findPragmas(t: NimNode): JSObjectPragmas =
   for i in 0..<identDefsStack.len: identDefsStack[i] = impl[2][i]
   while identDefsStack.len > 0:
     var identDefs = identDefsStack.pop()
-
     case identDefs.kind
     of nnkRecList:
       for child in identDefs.children:
@@ -1576,10 +1665,17 @@ proc findPragmas(t: NimNode): JSObjectPragmas =
           if varName.kind == nnkPostfix:
             # This is a public field. We are skipping the postfix *
             varName = varName[1]
-          for pragma in varNode[1]:
-            case $pragma
-            of "jsget": result.jsget.add(varName)
-            of "jsset": result.jsset.add(varName)
+          var varPragmas = varNode[1]
+          for varPragma in varPragmas:
+            let pragmaName = getPragmaName(varPragma)
+            let op = JSObjectPragma(
+              name: getStringFromPragma(varPragma).get($varName),
+              varsym: varName
+            )
+            case pragmaName
+            of "jsget": result.jsget.add(op)
+            of "jsset": result.jsset.add(op)
+            of "jsinclude": result.jsinclude.add(op)
 
 type
   TabGetSet* = object
@@ -1595,12 +1691,13 @@ type
 macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal =
                    false, nointerface = false, name: static string = "",
                    extra_getset: static openarray[TabGetSet] = [],
-                   extra_funcs: static openarray[TabFunc] = []): JSClassID =
+                   extra_funcs: static openarray[TabFunc] = [],
+                   namespace: JSValue = JS_NULL): JSClassID =
   result = newStmtList()
   let tname = t.strVal # the nim type's name.
   let name = if name == "": tname else: name # possibly a different name, e.g. Buffer for Container
   var sctr = ident("js_illegal_ctor")
-  var sfin = ident("js_" & t.strVal & "ClassFin")
+  var sfin = ident("js_" & tname & "ClassFin")
   # constructor
   var ctorFun: NimNode
   var ctorImpl: NimNode
@@ -1614,9 +1711,10 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal =
   var setters, getters: Table[string, NimNode]
   let tabList = newNimNode(nnkBracket)
   let pragmas = findPragmas(t)
-  for node in pragmas.jsget:
-    let id = ident($GETTER & "_" & t.strVal & "_" & $node)
-    let fn = $node
+  for op in pragmas.jsget:
+    let node = op.varsym
+    let fn = op.name
+    let id = ident($GETTER & "_" & tname & "_" & fn)
     result.add(quote do:
       proc `id`(ctx: JSContext, this: JSValue): JSValue {.cdecl.} =
         if not (JS_IsUndefined(this) or ctx.isGlobal(`tname`)) and not ctx.isInstanceOf(this, `tname`):
@@ -1625,10 +1723,11 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal =
         let arg_0 = fromJS_or_return(`t`, ctx, this)
         return toJS(ctx, arg_0.`node`)
     )
-    registerFunction(t.strVal, GETTER, fn, id)
-  for node in pragmas.jsset:
-    let id = ident($SETTER & "_" & t.strVal & "_" & $node)
-    let fn = $node
+    registerFunction(tname, GETTER, fn, id)
+  for op in pragmas.jsset:
+    let node = op.varsym
+    let fn = op.name
+    let id = ident($SETTER & "_" & tname & "_" & fn)
     result.add(quote do:
       proc `id`(ctx: JSContext, this: JSValue, val: JSValue): JSValue {.cdecl.} =
         if not (JS_IsUndefined(this) or ctx.isGlobal(`tname`)) and not ctx.isInstanceOf(this, `tname`):
@@ -1639,10 +1738,10 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal =
         arg_0.`node` = fromJS_or_return(typeof(arg_0.`node`), ctx, arg_1)
         return JS_DupValue(ctx, arg_1)
     )
-    registerFunction(t.strVal, SETTER, fn, id)
+    registerFunction(tname, SETTER, fn, id)
 
-  if t.strVal in BoundFunctions:
-    for fun in BoundFunctions[t.strVal].mitems:
+  if tname in BoundFunctions:
+    for fun in BoundFunctions[tname].mitems:
       var f0 = fun.name
       let f1 = fun.id
       if fun.name.endsWith("_exceptions"):
@@ -1655,7 +1754,7 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal =
       of CONSTRUCTOR:
         ctorImpl = js_funcs[$f0].res
         if ctorFun != nil:
-          error("Class " & t.strVal & " has 2+ constructors.")
+          error("Class " & tname & " has 2+ constructors.")
         ctorFun = f1
       of GETTER:
         getters[f0] = f1
@@ -1765,7 +1864,8 @@ static JSClassDef """, `cdname`, """ = {
     # any associated JS object from all relevant runtimes.
     var x: `t`
     new(x, nim_finalize_for_js)
-    `ctx`.newJSClass(`classDef`, `tname`, `sctr`, `tabList`, getTypePtr(x), `parent`, `asglobal`, `nointerface`, `finName`)
+    `ctx`.newJSClass(`classDef`, `tname`, `sctr`, `tabList`, getTypePtr(x),
+      `parent`, `asglobal`, `nointerface`, `finName`, `namespace`)
   )
   result.add(newBlockStmt(endstmts))
 
diff --git a/src/types/blob.nim b/src/types/blob.nim
new file mode 100644
index 00000000..677fb037
--- /dev/null
+++ b/src/types/blob.nim
@@ -0,0 +1,61 @@
+import js/javascript
+import types/mime
+import utils/twtstr
+
+type
+  DeallocFun = proc(buffer: pointer) {.noconv.}
+
+  Blob* = ref object of RootObj
+    isfile*: bool
+    size* {.jsget.}: uint64
+    ctype* {.jsget: "type".}: string
+    buffer*: pointer
+    deallocFun*: DeallocFun
+
+  WebFile* = ref object of Blob
+    webkitRelativePath {.jsget.}: string
+    path*: string
+    file: File #TODO maybe use fd?
+
+proc newBlob*(buffer: pointer, size: int, ctype: string,
+    deallocFun: DeallocFun): Blob =
+  return Blob(
+    buffer: buffer,
+    size: uint64(size),
+    ctype: ctype,
+    deallocFun: deallocFun
+  )
+
+proc finalize(blob: Blob) {.jsfin.} =
+  if blob.deallocFun != nil and blob.buffer != nil:
+    blob.deallocFun(blob.buffer)
+
+proc newWebFile*(path: string, webkitRelativePath = ""): WebFile =
+  var file: File
+  doAssert open(file, path, fmRead) #TODO bleh
+  return WebFile(
+    isfile: true,
+    path: path,
+    file: file,
+    webkitRelativePath: webkitRelativePath
+  )
+
+#TODO File, Blob constructors
+
+func size*(this: WebFile): uint64 {.jsfget.} =
+  #TODO use stat instead
+  return uint64(this.file.getFileSize())
+
+func ctype*(this: WebFile): string {.jsfget: "type".} =
+  return guessContentType(this.path)
+
+func name*(this: WebFile): string {.jsfget.} =
+  if this.path.len > 0 and this.path[^1] != '/':
+    return this.path.afterLast('/')
+  return this.path.afterLast('/', 2)
+
+#TODO lastModified
+
+proc addBlobModule*(ctx: JSContext) =
+  ctx.registerType(Blob)
+  ctx.registerType(WebFile, name = "File")
diff --git a/src/types/color.nim b/src/types/color.nim
index 02f56a56..818d4f5a 100644
--- a/src/types/color.nim
+++ b/src/types/color.nim
@@ -1,3 +1,4 @@
+import math
 import options
 import sequtils
 import strutils
@@ -21,6 +22,8 @@ converter toRGBColor*(i: RGBAColor): RGBColor =
 converter toRGBAColor*(i: RGBColor): RGBAColor =
   return RGBAColor(uint32(i) or 0xFF000000u32)
 
+func `==`*(a, b: RGBAColor): bool {.borrow.}
+
 func rgbcolor*(color: CellColor): RGBColor =
   cast[RGBColor](color.n)
 
@@ -220,10 +223,99 @@ func b*(c: RGBAColor): int =
 func a*(c: RGBAColor): int =
   return int(uint32(c) shr 24 and 0xff)
 
+# https://html.spec.whatwg.org/#serialisation-of-a-color
+func serialize*(color: RGBAColor): string =
+  if color.a == 255:
+    let r = toHex(cast[uint8](color.r))
+    let g = toHex(cast[uint8](color.g))
+    let b = toHex(cast[uint8](color.b))
+    return "#" & r & g & b
+  let a = float64(color.a) / 255
+  return "rgba(" & $color.r & ", " & $color.g & ", " & $color.b & ", " & $a &
+    ")"
+
+func `$`*(rgbacolor: RGBAColor): string =
+  return rgbacolor.serialize()
+
+# https://arxiv.org/pdf/2202.02864.pdf
+func fastmul(c, ca: uint32): uint32 =
+  let u = c or 0xFF000000u32
+  var rb = u and 0x00FF00FFu32
+  rb *= ca
+  rb += 0x00800080
+  rb += (rb shr 8) and 0x00FF00FFu32
+  rb = rb and 0xFF00FF00u32
+  var ga = (u shr 8) and 0x00FF00FFu32
+  ga *= ca
+  ga += 0x00800080
+  ga += (ga shr 8) and 0x00FF00FFu32
+  ga = ga and 0xFF00FF00u32
+  return ga or (rb shr 8)
+
+# fastmul, but preserves alpha
+func fastmul1(c, ca: uint32): uint32 =
+  let u = c
+  var rb = u and 0x00FF00FFu32
+  rb *= ca
+  rb += 0x00800080
+  rb += (rb shr 8) and 0x00FF00FFu32
+  rb = rb and 0xFF00FF00u32
+  var ga = (u shr 8) and 0x00FF00FFu32
+  ga *= ca
+  ga += 0x00800080
+  ga += (ga shr 8) and 0x00FF00FFu32
+  ga = ga and 0xFF00FF00u32
+  return ga or (rb shr 8)
+
+func fastmul(c: RGBAColor, ca: uint32): uint32 =
+  return fastmul(uint32(c), ca)
+
+func fastmul1(c: RGBAColor, ca: uint32): uint32 =
+  return fastmul1(uint32(c), ca)
+
+func rgba*(r, g, b, a: int): RGBAColor
+
+func premul(c: RGBAColor): RGBAColor =
+  return RGBAColor(fastmul(c, uint32(c.a)))
+
+const straightAlphaTable = (func(): auto =
+  var table: array[256, array[256, uint8]]
+  for a in 0 ..< 256:
+    let multiplier = if a > 0: (255 / a.float32) else: 0
+    for c in 0 ..< 256:
+      table[a][c] = min(round((c.float32 * multiplier)), 255).uint8
+  return table)()
+
+proc straight*(c: RGBAColor): RGBAColor =
+  let r = straightAlphaTable[c.a][c.r]
+  let g = straightAlphaTable[c.a][c.g]
+  let b = straightAlphaTable[c.a][c.b]
+  return rgba(int(r), int(g), int(b), int(c.a))
+
+func blend*(c0, c1: RGBAColor): RGBAColor =
+  let pc0 = c0.premul()
+  let pc1 = c1.premul()
+  let k = 255 - pc1.a
+  let mc = RGBAColor(fastmul1(pc0, uint32(k)))
+  let rr = pc1.r + mc.r
+  let rg = pc1.g + mc.g
+  let rb = pc1.b + mc.b
+  let ra = pc1.a + mc.a
+  let pres = rgba(rr, rg, rb, ra)
+  let res = straight(pres)
+  return res
+
+#func blend*(c0, c1: RGBAColor): RGBAColor =
+#  const norm = 1f64 / 255f64
+#  let c0a = float64(c0.a) * norm
+#  let c1a = float64(c1.a) * norm
+#  let a0 = c0a + c1a * (1 - c0a)
+
 func rgb*(r, g, b: int): RGBColor =
   return RGBColor((r shl 16) or (g shl 8) or b)
 
-func `==`*(a, b: RGBAColor): bool {.borrow.}
+func rgb*(r, g, b: uint8): RGBColor {.inline.} =
+  return rgb(int(r), int(g), int(b))
 
 func r*(c: RGBColor): int =
   return int(uint32(c) shr 16 and 0xff)
@@ -256,12 +348,15 @@ func YUV*(Y, U, V: int): RGBColor =
 func rgba*(r, g, b, a: int): RGBAColor =
   return RGBAColor((uint32(a) shl 24) or (uint32(r) shl 16) or (uint32(g) shl 8) or uint32(b))
 
+func gray*(n: int): RGBColor =
+  return rgb(n, n, n) #TODO use yuv instead?
+
+func gray*(n: uint8): RGBColor =
+  return gray(int(n))
+
 template `$`*(rgbcolor: RGBColor): string =
   "rgb(" & $rgbcolor.r & ", " & $rgbcolor.g & ", " & $rgbcolor.b & ")"
 
-template `$`*(rgbacolor: RGBAColor): string =
-  "rgba(" & $rgbacolor.r & ", " & $rgbacolor.g & ", " & $rgbacolor.b & ", " & $rgbacolor.a & ")"
-
 template `$`*(color: CellColor): string =
   if color.rgb:
     $color.rgbcolor
diff --git a/src/types/cookie.nim b/src/types/cookie.nim
index 41de0f4c..9dc50550 100644
--- a/src/types/cookie.nim
+++ b/src/types/cookie.nim
@@ -157,7 +157,9 @@ proc newCookie*(str: string): Cookie {.jsctor.} =
       if date.issome:
         cookie.expires = date.get.toTime().toUnix()
     of "max-age":
-      cookie.expires = now().toTime().toUnix() + parseInt64(val)
+      let x = parseInt64(val)
+      if x.isSome:
+        cookie.expires = now().toTime().toUnix() + x.get
     of "secure": cookie.secure = true
     of "httponly": cookie.httponly = true
     of "samesite": cookie.samesite = true
diff --git a/src/types/formdata.nim b/src/types/formdata.nim
new file mode 100644
index 00000000..983508bf
--- /dev/null
+++ b/src/types/formdata.nim
@@ -0,0 +1,18 @@
+import types/blob
+
+type
+  FormDataEntry* = object
+    name*: string
+    filename*: string
+    case isstr*: bool
+    of true:
+      svalue*: string
+    of false:
+      value*: Blob
+
+  FormData* = ref object
+    entries*: seq[FormDataEntry]
+
+iterator items*(this: FormData): FormDataEntry {.inline.} =
+  for entry in this.entries:
+    yield entry
diff --git a/src/types/line.nim b/src/types/line.nim
new file mode 100644
index 00000000..31a639f5
--- /dev/null
+++ b/src/types/line.nim
@@ -0,0 +1,66 @@
+import types/vector
+
+type
+  Line* = object
+    p0*: Vector2D
+    p1*: Vector2D
+
+  LineSegment* = object
+    line: Line
+    miny*: float64
+    maxy*: float64
+    minyx*: float64
+    islope*: float64
+
+func minx*(line: Line): float64 =
+  return min(line.p0.x, line.p1.x)
+
+func maxx*(line: Line): float64 =
+  return max(line.p0.x, line.p1.x)
+
+func minyx*(line: Line): float64 =
+  if line.p0.y < line.p1.y:
+    return line.p0.x
+  return line.p1.x
+
+func maxyx*(line: Line): float64 =
+  if line.p0.y > line.p1.y:
+    return line.p0.x
+  return line.p1.x
+
+func miny*(line: Line): float64 =
+  return min(line.p0.y, line.p1.y)
+
+func maxy*(line: Line): float64 =
+  return max(line.p0.y, line.p1.y)
+
+func slope*(line: Line): float64 =
+  let xdiff = (line.p0.x - line.p1.x)
+  if xdiff == 0:
+    return 0
+  return (line.p0.y - line.p1.y) / xdiff
+
+# inverse slope
+func islope*(line: Line): float64 =
+  let ydiff = (line.p0.y - line.p1.y)
+  if ydiff == 0:
+    return 0
+  return (line.p0.x - line.p1.x) / ydiff
+
+proc cmpLineSegmentY*(l1, l2: LineSegment): int =
+  return cmp(l1.miny, l2.miny)
+
+proc cmpLineSegmentX*(l1, l2: LineSegment): int =
+  return cmp(l1.minyx, l2.minyx)
+
+func p0*(ls: LineSegment): Vector2D {.inline.} = ls.line.p0
+func p1*(ls: LineSegment): Vector2D {.inline.} = ls.line.p1
+
+converter toLineSegment*(line: Line): LineSegment =
+  LineSegment(
+    line: line,
+    miny: line.miny,
+    maxy: line.maxy,
+    minyx: line.minyx,
+    islope: line.islope
+  )
diff --git a/src/types/matrix.nim b/src/types/matrix.nim
new file mode 100644
index 00000000..17ff3c0d
--- /dev/null
+++ b/src/types/matrix.nim
@@ -0,0 +1,46 @@
+type Matrix* = object
+  me*: seq[float64]
+  w: int
+  h: int
+
+proc newMatrix*(me: seq[float64], w: int, h: int): Matrix =
+  return Matrix(
+    me: me,
+    w: w,
+    h: h
+  )
+
+proc newIdentityMatrix*(n: int): Matrix =
+  var me = newSeq[float64](n * n)
+  for i in 0 ..< n:
+    me[n * i + i] = 1
+  return Matrix(
+    me: me,
+    w: n,
+    h: n
+  )
+
+proc newMatrixUninitialized*(w, h: int): Matrix =
+  return Matrix(
+    me: newSeqUninitialized[float64](w * h),
+    w: w,
+    h: h
+  )
+
+#TODO this is extremely inefficient
+proc `*`*(a: Matrix, b: Matrix): Matrix =
+  assert a.w == b.h
+  let h = a.h
+  let w = b.w
+  let n = a.w
+  var c = newMatrixUninitialized(w, h)
+  for x in 0 ..< w:
+    for y in 0 ..< h:
+      var val: float64 = 0
+      for i in 0 ..< n:
+        val += a.me[y * a.w + i] * b.me[i * b.w + x]
+      c.me[y * c.w + x] = val
+  return c
+
+proc `*=`*(a: var Matrix, b: Matrix) =
+  a = a * b
diff --git a/src/types/mime.nim b/src/types/mime.nim
index 2db567ad..cd4c2d51 100644
--- a/src/types/mime.nim
+++ b/src/types/mime.nim
@@ -12,12 +12,12 @@ const DefaultGuess = [
   ("", "text/plain")
 ].toTable()
 
-proc guessContentType*(path: string): string =
+proc guessContentType*(path: string, def = DefaultGuess[""]): string =
   var i = path.len - 1
   var n = 0
   while i > 0:
     if path[i] == '/':
-      return DefaultGuess[""]
+      return def
     if path[i] == '.':
       n = i
       break
@@ -26,7 +26,7 @@ proc guessContentType*(path: string): string =
     let ext = path.substr(n + 1)
     if ext in DefaultGuess:
       return DefaultGuess[ext]
-  return DefaultGuess[""]
+  return def
 
 const JavaScriptTypes = [
   "application/ecmascript",
diff --git a/src/types/url.nim b/src/types/url.nim
index 10b91fe5..cba83adb 100644
--- a/src/types/url.nim
+++ b/src/types/url.nim
@@ -6,6 +6,7 @@ import unicode
 import math
 
 import js/javascript
+import types/blob
 import utils/twtstr
 
 type
@@ -17,10 +18,8 @@ type
     RELATIVE_SLASH_STATE, QUERY_STATE, HOST_STATE, HOSTNAME_STATE,
     FILE_HOST_STATE, PORT_STATE, PATH_START_STATE, FILE_SLASH_STATE
 
-  Blob* = object
-
   BlobUrlEntry* = object
-    obj: Blob #TODO
+    obj: Blob #TODO blob urls
 
   UrlPath* = object
     case opaque*: bool
@@ -570,10 +569,11 @@ proc basicParseUrl*(input: string, base = none(Url), url: Url = Url(), stateOver
         (url.is_special and c == '\\') or override:
         if buffer != "":
           let i = parseInt32(buffer)
-          if i notin 0..65535:
+          if i.isNone or i.get notin 0..65535:
+            eprint "validation error???", i
             #TODO validation error
             return none(Url)
-          let port = cast[uint16](i).some
+          let port = cast[uint16](i.get).some
           url.port = if url.is_special and url.default_port == port: none(uint16) else: port
           buffer = ""
         if override:
diff --git a/src/types/vector.nim b/src/types/vector.nim
new file mode 100644
index 00000000..e200cc98
--- /dev/null
+++ b/src/types/vector.nim
@@ -0,0 +1,53 @@
+import math
+
+type Vector2D* = object
+  x*: float64
+  y*: float64
+
+func `-`*(v1, v2: Vector2D): Vector2D =
+  return Vector2D(x: v1.x - v2.x, y: v1.y - v2.y)
+
+func `+`*(v1, v2: Vector2D): Vector2D =
+  return Vector2D(x: v1.x + v2.x, y: v1.y + v2.y)
+
+proc `+=`*(v1: var Vector2D, v2: Vector2D) =
+  v1.x += v2.x
+  v1.y += v2.y
+
+proc `-=`*(v1: var Vector2D, v2: Vector2D) =
+  v1.x -= v2.x
+  v1.y -= v2.y
+
+# scalar multiplication
+func `*`*(v: Vector2D, s: float64): Vector2D =
+  return Vector2D(x: v.x * s, y: v.y * s)
+
+func `/`*(v: Vector2D, s: float64): Vector2D =
+  return Vector2D(x: v.x / s, y: v.y / s)
+
+# dot product
+func `*`*(v1, v2: Vector2D): float64 =
+  return v1.x * v2.x + v1.y * v2.y
+
+func norm*(v: Vector2D): float64 =
+  return sqrt(v.x * v.x + v.y * v.y)
+
+# kind of a cross product?
+func cross*(v1, v2: Vector2D): float64 =
+  return v1.x * v2.y - v1.y * v2.x
+
+# https://en.wikipedia.org/wiki/Inner_product_space
+func innerAngle*(v1, v2: Vector2D): float64 =
+  return arccos((v1 * v2) / (v1.norm() * v2.norm()))
+
+func rotate*(v: Vector2D, alpha: float64): Vector2D =
+  let sa = sin(alpha)
+  let ca = cos(alpha)
+  return Vector2D(
+    x: v.x * ca - v.y * sa,
+    y: v.x * sa + v.y * ca
+  )
+
+func collinear*(v1, v2, v3: Vector2D): bool =
+  return almostEqual((v1.y - v2.y) * (v1.x - v3.x),
+    (v1.y - v3.y) * (v1.x - v2.x))
diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim
index d67e1ab7..93fce06f 100644
--- a/src/utils/twtstr.nim
+++ b/src/utils/twtstr.nim
@@ -122,6 +122,11 @@ func snakeToKebabCase*(str: string): string =
     if c == '_':
       c = '-'
 
+func normalizeLocale*(s: string): string =
+  for i in 0 ..< s.len:
+    if cast[uint8](s[i]) > 0x20 and s[i] != '_' and s[i] != '-':
+      result &= s[i].toLowerAscii()
+
 func isAscii*(r: Rune): bool =
   return cast[uint32](r) < 128
 
@@ -168,6 +173,9 @@ func toHex*(c: char): string =
   result[0] = HexChars[(uint8(c) shr 4)]
   result[1] = HexChars[(uint8(c) and 0xF)]
 
+func toHex*(i: uint8): string =
+  return toHex(cast[char](i))
+
 func equalsIgnoreCase*(s1: seq[Rune], s2: string): bool =
   var i = 0
   while i < min(s1.len, s2.len):
@@ -236,6 +244,17 @@ func after*(s: string, c: set[char]): string =
 
 func after*(s: string, c: char): string = s.after({c})
 
+func afterLast*(s: string, c: set[char], n = 1): string =
+  var j = 0
+  for i in countdown(s.high, 0):
+    if s[i] in c:
+      inc j
+      if j == n:
+        return s.substr(i + 1)
+  return s
+
+func afterLast*(s: string, c: char, n = 1): string = s.afterLast({c}, n)
+
 proc c_sprintf(buf, fm: cstring): cint {.header: "<stdio.h>", importc: "sprintf", varargs}
 
 # From w3m
@@ -345,72 +364,59 @@ func japaneseNumber*(i: int): string =
     result &= ss[n]
     dec n
 
-func parseInt32*(s: string): int =
-  var sign = 1
-  var t = 1
-  var integer: int = 0
-  var e: int = 0
-
+# Implements https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#signed-integers
+#TODO TODO TODO handle overflow defects
+func parseInt32*(s: string): Option[int32] =
+  var sign: int32 = 1
   var i = 0
   if i < s.len and s[i] == '-':
     sign = -1
     inc i
   elif i < s.len and s[i] == '+':
     inc i
-
+  if i == s.len or s[i] notin AsciiDigit:
+    return none(int32)
+  var integer = int32(decValue(s[i]))
+  inc i
   while i < s.len and isDigit(s[i]):
     integer *= 10
-    integer += decValue(s[i])
+    integer += int32(decValue(s[i]))
     inc i
+  return some(sign * integer)
 
-  if i < s.len and (s[i] == 'e' or s[i] == 'E'):
-    inc i
-    if i < s.len and s[i] == '-':
-      t = -1
-      inc i
-    elif i < s.len and s[i] == '+':
-      inc i
-
-    while i < s.len and isDigit(s[i]):
-      e *= 10
-      e += decValue(s[i])
-      inc i
-
-  return sign * integer * 10 ^ (t * e)
-
-func parseInt64*(s: string): int64 =
-  var sign = 1
-  var t = 1
-  var integer: int64 = 0
-  var e: int64 = 0
-
+func parseInt64*(s: string): Option[int64] =
+  var sign: int64 = 1
   var i = 0
   if i < s.len and s[i] == '-':
     sign = -1
     inc i
   elif i < s.len and s[i] == '+':
     inc i
-
+  if i == s.len or s[i] notin AsciiDigit:
+    return none(int64)
+  var integer = int64(decValue(s[i]))
+  inc i
   while i < s.len and isDigit(s[i]):
     integer *= 10
-    integer += decValue(s[i])
+    integer += int64(decValue(s[i]))
     inc i
+  return some(sign * integer)
 
-  if i < s.len and (s[i] == 'e' or s[i] == 'E'):
+func parseUInt32*(s: string): Option[uint32] =
+  var i = 0
+  if i < s.len and s[i] == '+':
     inc i
-    if i < s.len and s[i] == '-':
-      t = -1
-      inc i
-    elif i < s.len and s[i] == '+':
-      inc i
-
-    while i < s.len and isDigit(s[i]):
-      e *= 10
-      e += decValue(s[i])
-      inc i
-
-  return sign * integer * 10 ^ (t * e)
+  if i == s.len or s[i] notin AsciiDigit:
+    return none(uint32)
+  var integer = uint32(decValue(s[i]))
+  inc i
+  while i < s.len and isDigit(s[i]):
+    integer *= 10
+    integer += uint32(decValue(s[i]))
+    inc i
+  return some(integer)
 
+#TODO not sure where this algorithm is from...
 func parseFloat64*(s: string): float64 =
   var sign = 1
   var t = 1
diff --git a/src/xhr/formdata.nim b/src/xhr/formdata.nim
new file mode 100644
index 00000000..9b45e7ba
--- /dev/null
+++ b/src/xhr/formdata.nim
@@ -0,0 +1,159 @@
+import html/dom
+import html/tags
+import js/javascript
+import types/blob
+import types/formdata
+import utils/twtstr
+
+proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
+    encoding: string = ""): Option[seq[FormDataEntry]]
+
+proc newFormData*(form: HTMLFormElement = nil,
+    submitter: HTMLElement = nil): FormData {.jserr, jsctor.} =
+  let this = FormData()
+  if form != nil:
+    if submitter != nil:
+      if not submitter.isSubmitButton():
+        JS_ERR JS_TypeError, "Submitter must be a submit button"
+      if FormAssociatedElement(submitter).form != form:
+        #TODO should be DOMException
+        JS_ERR JS_TypeError, "InvalidStateError"
+    this.entries = constructEntryList(form, submitter).get(@[])
+  return this
+
+#TODO as jsfunc
+proc append*(this: FormData, name: string, svalue: string, filename = "") =
+  this.entries.add(FormDataEntry(
+    name: name,
+    isstr: true,
+    svalue: svalue,
+    filename: filename
+  ))
+
+proc append*(this: FormData, name: string, value: Blob,
+    filename = "blob") =
+  this.entries.add(FormDataEntry(
+    name: name,
+    isstr: false,
+    value: value,
+    filename: filename
+  ))
+
+#TODO hack
+proc append(this: FormData, name: string, value: JSObject,
+    filename = none(string)) {.jsfunc.} =
+  let blob = fromJS[Blob](value.ctx, value.val)
+  if blob.isSome:
+    this.append(name, blob.get, filename.get("blob"))
+  else:
+    let s = fromJS[string](value.ctx, value.val)
+    # toString should never fail (?)
+    this.append(name, s.get, filename.get(""))
+
+proc delete(this: FormData, name: string) {.jsfunc.} =
+  for i in countdown(this.entries.high, 0):
+    if this.entries[i].name == name:
+      this.entries.delete(i)
+
+proc get(ctx: JSContext, this: FormData, name: string): JSValue {.jsfunc.} =
+  for entry in this.entries:
+    if entry.name == name:
+      if entry.isstr:
+        return toJS(ctx, entry.svalue)
+      else:
+        return toJS(ctx, entry.value)
+  return JS_NULL
+
+proc getAll(this: FormData, name: string): seq[Blob] {.jsfunc.} =
+  for entry in this.entries:
+    if entry.name == name:
+      result.add(entry.value) # may be null
+
+proc add(list: var seq[FormDataEntry], entry: tuple[name, value: string]) =
+  list.add(FormDataEntry(
+    name: entry.name,
+    isstr: true,
+    svalue: entry.value
+  ))
+
+func toNameValuePairs*(list: seq[FormDataEntry]):
+    seq[tuple[name, value: string]] =
+  for entry in list:
+    if entry.isstr:
+      result.add((entry.name, entry.svalue))
+    else:
+      result.add((entry.name, entry.name))
+
+# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set
+proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
+    encoding: string = ""): Option[seq[FormDataEntry]] =
+  if form.constructingentrylist:
+    return
+  form.constructingentrylist = true
+
+  var entrylist: seq[FormDataEntry]
+  for field in form.controls:
+    if field.findAncestor({TAG_DATALIST}) != nil or
+        field.attrb("disabled") or
+        field.isButton() and Element(field) != submitter:
+      continue
+
+    if field.tagType == TAG_INPUT:
+      let field = HTMLInputElement(field)
+      if field.inputType == INPUT_IMAGE:
+        let name = if field.attr("name") != "":
+          field.attr("name") & '.'
+        else:
+          ""
+        entrylist.add((name & 'x', $field.xcoord))
+        entrylist.add((name & 'y', $field.ycoord))
+        continue
+
+    #TODO custom elements
+
+    let name = field.attr("name")
+
+    if name == "":
+      continue
+
+    if field.tagType == TAG_SELECT:
+      let field = HTMLSelectElement(field)
+      for option in field.options:
+        if option.selected or option.disabled:
+          entrylist.add((name, option.value))
+    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_CHECKBOX, INPUT_RADIO}:
+      let value = if field.attr("value") != "":
+        field.attr("value")
+      else:
+        "on"
+      entrylist.add((name, value))
+    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_FILE:
+      #TODO file
+      discard
+    elif field.tagType == TAG_INPUT and HTMLInputElement(field).inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"):
+      let charset = if encoding != "":
+        encoding
+      else:
+        "UTF-8"
+      entrylist.add((name, charset))
+    else:
+      case field.tagType
+      of TAG_INPUT:
+        entrylist.add((name, HTMLInputElement(field).value))
+      of TAG_BUTTON:
+        entrylist.add((name, HTMLButtonElement(field).value))
+      of TAG_TEXTAREA:
+        entrylist.add((name, HTMLTextAreaElement(field).value))
+      else: assert false, "Tag type " & $field.tagType & " not accounted for in constructEntryList"
+    if field.tagType == TAG_TEXTAREA or
+        field.tagType == TAG_INPUT and HTMLInputElement(field).inputType in {INPUT_TEXT, INPUT_SEARCH}:
+      if field.attr("dirname") != "":
+        let dirname = field.attr("dirname")
+        let dir = "ltr" #TODO bidi
+        entrylist.add((dirname, dir))
+
+  form.constructingentrylist = false
+  return some(entrylist)
+
+proc addFormDataModule*(ctx: JSContext) =
+  ctx.registerType(FormData)