From 3c548dd7b9cbf4670f276120ce4b9df121d1f886 Mon Sep 17 00:00:00 2001
From: Ryan Oldenburg <guzba8@gmail.com>
Date: Sun, 23 May 2021 18:20:16 -0500
Subject: [PATCH 1/4] parse post table

---
 src/pixie/fontformats/opentype.nim | 23 ++++++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/pixie/fontformats/opentype.nim b/src/pixie/fontformats/opentype.nim
index bb6353d..477c181 100644
--- a/src/pixie/fontformats/opentype.nim
+++ b/src/pixie/fontformats/opentype.nim
@@ -303,6 +303,13 @@ type
     # featureList: FeatureList
     lookupList: LookupList
 
+  PostTable = ref object
+    version: float32
+    italicAngle: float32
+    underlinePosition: int16
+    underlineThickness: int16
+    isFixedPitch: uint32
+
   OpenType* = ref object
     buf*: string
     version*: uint32
@@ -322,6 +329,7 @@ type
     glyf*: GlyfTable
     kern*: KernTable
     gpos*: GposTable
+    post*: PostTable
     glyphPaths: Table[Rune, Path]
 
 when defined(release):
@@ -1194,6 +1202,16 @@ proc parseGposTable(buf: string, offset: int): GPOSTable =
   result.lookupList =
     parseLookupList(buf, offset + result.lookupListOffset.int, result)
 
+proc parsePostTable(buf: string, offset: int): PostTable =
+  buf.eofCheck(offset + 14)
+
+  result = PostTable()
+  result.version = buf.readFixed32(offset + 0)
+  result.italicAngle = buf.readFixed32(offset + 4)
+  result.underlinePosition = buf.readInt16(offset + 8).swap()
+  result.underlineThickness = buf.readInt16(offset + 10).swap()
+  result.isFixedPitch = buf.readUint32(offset + 12).swap()
+
 proc getGlyphId(opentype: OpenType, rune: Rune): uint16 {.inline.} =
   if rune in opentype.cmap.runeToGlyphId:
     result = opentype.cmap.runeToGlyphId[rune]
@@ -1544,7 +1562,8 @@ proc parseOpenType*(buf: string): OpenType =
     i += 16
 
   const requiredTables = [
-    "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "loca", "glyf"
+    "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "loca", "glyf",
+    "post"
   ]
   for table in requiredTables:
     if table notin result.tableRecords:
@@ -1571,5 +1590,7 @@ proc parseOpenType*(buf: string): OpenType =
   if "GPOS" in result.tableRecords:
     result.gpos = parseGposTable(buf, result.tableRecords["GPOS"].offset.int)
 
+  result.post = parsePostTable(buf, result.tableRecords["post"].offset.int)
+
 when defined(release):
   {.pop.}

From 145d54c7e6d21a64d7f02baa19cfa8191c6be1b5 Mon Sep 17 00:00:00 2001
From: Ryan Oldenburg <guzba8@gmail.com>
Date: Sun, 23 May 2021 18:21:02 -0500
Subject: [PATCH 2/4] rm

---
 src/pixie/images.nim | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/pixie/images.nim b/src/pixie/images.nim
index 9a9d271..91de331 100644
--- a/src/pixie/images.nim
+++ b/src/pixie/images.nim
@@ -396,10 +396,8 @@ proc blur*(
 
       for xx in max(x - radius, image.width) .. x + radius:
         values += outOfBounds * kernel[xx - x + radius]
+
       blurX.setRgbaUnsafe(y, x, rgbx(values))
-      # this would also work (see: `limitations of method call syntax`)
-      # `mixin rgbx`
-      # blurX.setRgbaUnsafe(y, x, values.rgbx)
 
   # Blur in the Y direction.
   for y in 0 ..< image.height:

From b73b88bb634ff3fc3c5e2f6883194fe5d581b2cd Mon Sep 17 00:00:00 2001
From: Ryan Oldenburg <guzba8@gmail.com>
Date: Sun, 23 May 2021 18:56:21 -0500
Subject: [PATCH 3/4] layers

---
 src/pixie/context.nim              | 294 ++++++++++++++++-------------
 tests/images/context/clip_1d.png   | Bin 0 -> 3664 bytes
 tests/images/context/clip_1e.png   | Bin 0 -> 618 bytes
 tests/images/context/clip_text.png | Bin 0 -> 4569 bytes
 tests/test_context.nim             |  73 +++++++
 5 files changed, 237 insertions(+), 130 deletions(-)
 create mode 100644 tests/images/context/clip_1d.png
 create mode 100644 tests/images/context/clip_1e.png
 create mode 100644 tests/images/context/clip_text.png

diff --git a/src/pixie/context.nim b/src/pixie/context.nim
index a081e57..1ed8788 100644
--- a/src/pixie/context.nim
+++ b/src/pixie/context.nim
@@ -18,6 +18,7 @@ type
     path: Path
     mat: Mat3
     mask: Mask
+    layer: Image
     stateStack: seq[ContextState]
 
   ContextState = object
@@ -29,6 +30,10 @@ type
     textAlign: HAlignMode
     mat: Mat3
     mask: Mask
+    layer: Image
+
+  TextMetrics* = object
+    width*: float32
 
 proc newContext*(image: Image): Context =
   ## Create a new Context that will draw to the parameter image.
@@ -43,6 +48,116 @@ proc newContext*(width, height: int): Context {.inline.} =
   ## Create a new Context that will draw to a new image of width and height.
   newContext(newImage(width, height))
 
+proc state(ctx: Context): ContextState =
+  result.fillStyle = ctx.fillStyle
+  result.strokeStyle = ctx.strokeStyle
+  result.lineWidth = ctx.lineWidth
+  result.lineCap = ctx.lineCap
+  result.lineJoin = ctx.lineJoin
+  result.font = ctx.font
+  result.textAlign = ctx.textAlign
+  result.mat = ctx.mat
+  result.mask = if ctx.mask != nil: ctx.mask.copy() else: nil
+
+proc save*(ctx: Context) {.inline.} =
+  ## Saves the entire state of the canvas by pushing the current state onto
+  ## a stack.
+  ctx.stateStack.add(ctx.state())
+
+proc saveLayer*(ctx: Context) =
+  var state = ctx.state()
+  state.layer = ctx.layer
+  ctx.stateStack.add(state)
+  ctx.layer = newImage(ctx.image.width, ctx.image.height)
+
+proc restore*(ctx: Context) =
+  ## Restores the most recently saved canvas state by popping the top entry
+  ## in the drawing state stack. If there is no saved state, this method does
+  ## nothing.
+  if ctx.stateStack.len == 0:
+    return
+
+  let
+    poppedLayer = ctx.layer
+    poppedMask = ctx.mask
+
+  let state = ctx.stateStack.pop()
+  ctx.fillStyle = state.fillStyle
+  ctx.strokeStyle = state.strokeStyle
+  ctx.lineWidth = state.lineWidth
+  ctx.lineCap = state.lineCap
+  ctx.lineJoin = state.lineJoin
+  ctx.font = state.font
+  ctx.textAlign = state.textAlign
+  ctx.mat = state.mat
+  ctx.mask = state.mask
+  ctx.layer = state.layer
+
+  if poppedLayer != nil: # If there is a layer being popped
+    if poppedMask != nil: # If there is a mask, apply it
+      poppedLayer.draw(poppedMask)
+    if ctx.layer != nil: # If we popped to another layer, draw to it
+      ctx.layer.draw(poppedLayer)
+    else: # Otherwise draw to the root image
+      ctx.image.draw(poppedLayer)
+
+proc fill(
+  ctx: Context, image: Image, path: Path, windingRule: WindingRule
+) {.inline.} =
+  image.fillPath(
+    path,
+    ctx.fillStyle,
+    ctx.mat,
+    windingRule
+  )
+
+proc stroke(ctx: Context, image: Image, path: Path) {.inline.} =
+  image.strokePath(
+    path,
+    ctx.strokeStyle,
+    ctx.mat,
+    ctx.lineWidth,
+    ctx.lineCap,
+    ctx.lineJoin
+  )
+
+proc fillText(ctx: Context, image: Image, text: string, at: Vec2) {.inline.} =
+  if ctx.font.typeface == nil:
+    raise newException(PixieError, "No font has been set on this Context")
+
+  # Canvas positions text relative to the alphabetic baseline by default
+  var at = at
+  at.y -= round(ctx.font.typeface.ascent * ctx.font.scale)
+
+  ctx.font.paint = ctx.fillStyle
+
+  image.fillText(
+    ctx.font,
+    text,
+    ctx.mat * translate(at),
+    hAlign = ctx.textAlign
+  )
+
+proc strokeText(ctx: Context, image: Image, text: string, at: Vec2) {.inline.} =
+  if ctx.font.typeface == nil:
+    raise newException(PixieError, "No font has been set on this Context")
+
+  # Canvas positions text relative to the alphabetic baseline by default
+  var at = at
+  at.y -= round(ctx.font.typeface.ascent * ctx.font.scale)
+
+  ctx.font.paint = ctx.strokeStyle
+
+  image.strokeText(
+    ctx.font,
+    text,
+    ctx.mat * translate(at),
+    ctx.lineWidth,
+    hAlign = ctx.textAlign,
+    lineCap = ctx.lineCap,
+    lineJoin = ctx.lineJoin
+  )
+
 proc beginPath*(ctx: Context) {.inline.} =
   ## Starts a new path by emptying the list of sub-paths.
   ctx.path = Path()
@@ -121,23 +236,14 @@ proc ellipse*(ctx: Context, x, y, rx, ry: float32) {.inline.} =
 
 proc fill*(ctx: Context, path: Path, windingRule = wrNonZero) {.inline.} =
   ## Fills the path with the current fillStyle.
-  if ctx.mask != nil:
-    let tmp = newImage(ctx.image.width, ctx.image.height)
-    tmp.fillPath(
-      path,
-      ctx.fillStyle,
-      ctx.mat,
-      windingRule
-    )
-    tmp.draw(ctx.mask)
-    ctx.image.draw(tmp)
+  if ctx.mask != nil and ctx.layer == nil:
+    ctx.saveLayer()
+    ctx.fill(ctx.layer, path, windingRule)
+    ctx.restore()
+  elif ctx.layer != nil:
+    ctx.fill(ctx.layer, path, windingRule)
   else:
-    ctx.image.fillPath(
-      path,
-      ctx.fillStyle,
-      ctx.mat,
-      windingRule
-    )
+    ctx.fill(ctx.image, path, windingRule)
 
 proc fill*(ctx: Context, windingRule = wrNonZero) {.inline.} =
   ## Fills the current path with the current fillStyle.
@@ -163,27 +269,14 @@ proc clip*(ctx: Context, windingRule = wrNonZero) {.inline.} =
 
 proc stroke*(ctx: Context, path: Path) {.inline.} =
   ## Strokes (outlines) the current or given path with the current strokeStyle.
-  if ctx.mask != nil:
-    let tmp = newImage(ctx.image.width, ctx.image.height)
-    tmp.strokePath(
-      path,
-      ctx.strokeStyle,
-      ctx.mat,
-      ctx.lineWidth,
-      ctx.lineCap,
-      ctx.lineJoin
-    )
-    tmp.draw(ctx.mask)
-    ctx.image.draw(tmp)
+  if ctx.mask != nil and ctx.layer == nil:
+    ctx.saveLayer()
+    ctx.stroke(ctx.layer, path)
+    ctx.restore()
+  elif ctx.layer != nil:
+    ctx.stroke(ctx.layer, path)
   else:
-    ctx.image.strokePath(
-      path,
-      ctx.strokeStyle,
-      ctx.mat,
-      ctx.lineWidth,
-      ctx.lineCap,
-      ctx.lineJoin
-    )
+    ctx.stroke(ctx.image, path)
 
 proc stroke*(ctx: Context) {.inline.} =
   ## Strokes (outlines) the current or given path with the current strokeStyle.
@@ -193,11 +286,18 @@ proc clearRect*(ctx: Context, rect: Rect) =
   ## Erases the pixels in a rectangular area.
   var path: Path
   path.rect(rect)
-  ctx.image.fillPath(
-    path,
-    Paint(kind: pkSolid, color:rgbx(0, 0, 0, 0), blendMode: bmOverwrite),
-    ctx.mat
-  )
+  if ctx.layer != nil:
+    ctx.layer.fillPath(
+      path,
+      Paint(kind: pkSolid, color:rgbx(0, 0, 0, 0), blendMode: bmOverwrite),
+      ctx.mat
+    )
+  else:
+    ctx.image.fillPath(
+      path,
+      Paint(kind: pkSolid, color:rgbx(0, 0, 0, 0), blendMode: bmOverwrite),
+      ctx.mat
+    )
 
 proc clearRect*(ctx: Context, x, y, width, height: float32) {.inline.} =
   ## Erases the pixels in a rectangular area.
@@ -228,33 +328,14 @@ proc strokeRect*(ctx: Context, x, y, width, height: float32) {.inline.} =
 proc fillText*(ctx: Context, text: string, at: Vec2) =
   ## Draws a text string at the specified coordinates, filling the string's
   ## characters with the current fillStyle
-
-  if ctx.font.typeface == nil:
-    raise newException(PixieError, "No font has been set on this Context")
-
-  # Canvas positions text relative to the alphabetic baseline by default
-  var at = at
-  at.y -= round(ctx.font.typeface.ascent * ctx.font.scale)
-
-  ctx.font.paint = ctx.fillStyle
-
-  if ctx.mask != nil:
-    let tmp = newImage(ctx.image.width, ctx.image.height)
-    tmp.fillText(
-      ctx.font,
-      text,
-      ctx.mat * translate(at),
-      hAlign = ctx.textAlign
-    )
-    tmp.draw(ctx.mask)
-    ctx.image.draw(tmp)
+  if ctx.mask != nil and ctx.layer == nil:
+    ctx.saveLayer()
+    ctx.fillText(ctx.layer, text, at)
+    ctx.restore()
+  elif ctx.layer != nil:
+    ctx.fillText(ctx.layer, text, at)
   else:
-    ctx.image.fillText(
-      ctx.font,
-      text,
-      ctx.mat * translate(at),
-      hAlign = ctx.textAlign
-    )
+    ctx.fillText(ctx.image, text, at)
 
 proc fillText*(ctx: Context, text: string, x, y: float32) {.inline.} =
   ## Draws the outlines of the characters of a text string at the specified
@@ -264,45 +345,29 @@ proc fillText*(ctx: Context, text: string, x, y: float32) {.inline.} =
 proc strokeText*(ctx: Context, text: string, at: Vec2) =
   ## Draws the outlines of the characters of a text string at the specified
   ## coordinates.
-
-  if ctx.font.typeface == nil:
-    raise newException(PixieError, "No font has been set on this Context")
-
-  # Canvas positions text relative to the alphabetic baseline by default
-  var at = at
-  at.y -= round(ctx.font.typeface.ascent * ctx.font.scale)
-
-  ctx.font.paint = ctx.strokeStyle
-
-  if ctx.mask != nil:
-    let tmp = newImage(ctx.image.width, ctx.image.height)
-    tmp.strokeText(
-      ctx.font,
-      text,
-      ctx.mat * translate(at),
-      ctx.lineWidth,
-      hAlign = ctx.textAlign,
-      lineCap = ctx.lineCap,
-      lineJoin = ctx.lineJoin
-    )
-    tmp.draw(ctx.mask)
-    ctx.image.draw(tmp)
+  if ctx.mask != nil and ctx.layer == nil:
+    ctx.saveLayer()
+    ctx.strokeText(ctx.layer, text, at)
+    ctx.restore()
+  elif ctx.layer != nil:
+    ctx.strokeText(ctx.layer, text, at)
   else:
-    ctx.image.strokeText(
-      ctx.font,
-      text,
-      ctx.mat * translate(at),
-      ctx.lineWidth,
-      hAlign = ctx.textAlign,
-      lineCap = ctx.lineCap,
-      lineJoin = ctx.lineJoin
-    )
+    ctx.strokeText(ctx.image, text, at)
 
 proc strokeText*(ctx: Context, text: string, x, y: float32) {.inline.} =
   ## Draws the outlines of the characters of a text string at the specified
   ## coordinates.
   ctx.strokeText(text, vec2(x, y))
 
+proc measureText*(ctx: Context, text: string): TextMetrics =
+  ## Returns a TextMetrics object that contains information about the measured
+  ## text (such as its width, for example).
+  if ctx.font.typeface == nil:
+    raise newException(PixieError, "No font has been set on this Context")
+
+  let bounds = typeset(ctx.font, text).computeBounds()
+  result.width = bounds.x
+
 proc getTransform*(ctx: Context): Mat3 {.inline.} =
   ## Retrieves the current transform matrix being applied to the context.
   ctx.mat
@@ -351,37 +416,6 @@ proc resetTransform*(ctx: Context) {.inline.} =
   ## Resets the current transform to the identity matrix.
   ctx.mat = mat3()
 
-proc save*(ctx: Context) =
-  ## Saves the entire state of the canvas by pushing the current state onto
-  ## a stack.
-  var state: ContextState
-  state.fillStyle = ctx.fillStyle
-  state.strokeStyle = ctx.strokeStyle
-  state.lineWidth = ctx.lineWidth
-  state.lineCap = ctx.lineCap
-  state.lineJoin = ctx.lineJoin
-  state.font = ctx.font
-  state.textAlign = ctx.textAlign
-  state.mat = ctx.mat
-  state.mask = if ctx.mask != nil: ctx.mask.copy() else: nil
-  ctx.stateStack.add(state)
-
-proc restore*(ctx: Context) =
-  ## Restores the most recently saved canvas state by popping the top entry
-  ## in the drawing state stack. If there is no saved state, this method does
-  ## nothing.
-  if ctx.stateStack.len > 0:
-    let state = ctx.stateStack.pop()
-    ctx.fillStyle = state.fillStyle
-    ctx.strokeStyle = state.strokeStyle
-    ctx.lineWidth = state.lineWidth
-    ctx.lineCap = state.lineCap
-    ctx.lineJoin = state.lineJoin
-    ctx.font = state.font
-    ctx.textAlign = state.textAlign
-    ctx.mat = state.mat
-    ctx.mask = state.mask
-
 # Additional procs that are not part of the JS API
 
 proc roundedRect*(ctx: Context, x, y, w, h, nw, ne, se, sw: float32) {.inline.} =
diff --git a/tests/images/context/clip_1d.png b/tests/images/context/clip_1d.png
new file mode 100644
index 0000000000000000000000000000000000000000..bdee0a9680ee5fe888ed0fdace99749ce6a154c3
GIT binary patch
literal 3664
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

literal 0
HcmV?d00001

diff --git a/tests/images/context/clip_1e.png b/tests/images/context/clip_1e.png
new file mode 100644
index 0000000000000000000000000000000000000000..c7b514bfb5d35a76698a926f166e57f75428088d
GIT binary patch
literal 618
zcmeAS@N?(olHy`uVBq!ia0y~yVAKJ!r*W_W$&~1=mlznB3_V>OLn;{GUI|>r;>hE0
zF+lqKx#x!Y+?T(EJ`ea3-rZ51W;cIk?0LRB4bm0AdFT9RWW49Up@HWhlMxH|2>~4i
zXR2_j4lwjv81L@?Bl+MsHO+;(Qi?6@+?jQ*46IG8DI8)G6eAqk5*Q_^!->f*I(H^7
zFYAF9b<9I`ogl|GhMKuE@4gp5aGI0KE(h9FE~Q~*y!+_E#@|E+%OA#j4u>zOxEukc
OAqG!ZKbLh*2~7a=hPXQb

literal 0
HcmV?d00001

diff --git a/tests/images/context/clip_text.png b/tests/images/context/clip_text.png
new file mode 100644
index 0000000000000000000000000000000000000000..b12da69b7c2721de831d1695b08dfcb1ec3626c6
GIT binary patch
literal 4569
zcmcgw`#Tfv`)4!OoK`{+Qb@5>&ZnH;IfR^zVlv6GMMz?1=-^G|9ferlC2wZTaVv*y
z3Ps3i&TOe98!Lx7&A#jV`TPZ+&vku%d9LTVpX++=`+DyCd0wyAeeZ(HIZ22DL`X<T
z(!t)=T}Vh+TQJWDiwZ`hf8(H#(B9_`wpN}uh1t`48q~a!yM8XSgT|0bmQ-QDeF)?k
z_zVlRUqtfoiErup`_kSMI|>f}acZjV@ZBR>M}U9c*F*`8ds@b;cONkkg=3-d5=Mvb
zvLtSN-NC<kg~yNHTG`6>LE~Z-Dx*gTl}p?$ZuImUFWo(RAK7aXZPgSY+I#P10-S9B
zE~V~!boGB{iV%T5dh}E;mvH~?pQ-;_|5cO|*><I8#ebNR0(SMm(rYP2*)pYV5kb6=
z!&4(k$&WJ5Vb**)bVQfj5ii;orNE^1l%icTl)o$;C(i7iS<KQlgH)Xk)0W&-%<%ru
z165WYdexbSlURB`s8OQ?^}U1jue%W17l=ft0{&W6{WF7aM4rQB7uN7IQIoz+dTl#M
z)VchoL|D4NQwV4mg>sUEe6u9ioznRwZ-dd-*oC5&bv#@BFzRP+2^{@AhVd`G;3ZfI
z@Au8qmjfomR1^Z7kN{2Mi50Ch(<)ErfA4L2(UkluUs3lJ2tlg4cfuL3o>U$u+G?Z%
z@MkD;WS-;5e%~W9mKd*sK*g!EW#PdRRh|1eV%EtjoO2ip@J<SY3!rFh_^6s$g@v}l
zK3@0DCSh^HYtz*2_N2{F&SF}P6>l%tVR<ePnhm3(MRxw^9c@;f(%+F&Y{t&Ez!Qq^
z$Fa@l<D&RZRyS{KB=-;n5tqTLT=Ocu1im~uU4U6Jl83>@urr7RA(JrMrl?6I%$Ra?
z!}Rb@TSNazxjztPR@YCE;p3+#5fiKCUzS58QAOBE@mwI)LV9gd^@i~-h-m>{GG)Z~
z1>8Qg69x@9*fP?2g4)YC>-W!g6j(>XE#V-e$@|iz1tslNIr<Vl6dTXG2<~4E^obE%
zi<}-&p$K4@R<ASmH#Pw$I$ln*kYtYXMseS%;bS9%D>T9opB%FUgCAC##Pav1KUq;1
zuRbfETfn&31ly(*0hu$AZ7P%`IU{3y4_``3X^zy}PVOy*mPBZU1rr8V6BsHOoYe-d
zsblaj0KN{ir>gT<stU%xI+k>=_{6kS^hjSwC){cA0W>8fcG}q!p)V)QhoNxSK(_G9
zR<+jLuhDS`d5%RoXFjI4c8^XPNzR?Q;#87>>R_-pU(j0d4Kqmae17*E&UPs5{lyrS
z;kL*9ap94mDa-YP2O`X8kuqnV3Ov_}dIy^mx3{J~vzyPiiLt~R7ob;c!0<(E88zxt
z+sVl^%1iF?cebE>{vmRIUXG-_ix?}Q6I&X8=H~Y20N|8#q;PZ{A@8~tz~71&3C^yH
zv`TDFYjzIO>ajLzJT+?<e4HVH%6cqUEw$#M+Twje=Vw<)`OlSP>GsBz`3XI5K#I%y
zOsupG*$nq+S5OPt>coB9&ve8V`#-41n>2_G=U_RW0fn`jsDm3~!V&tzb-z}f)|A0=
zF_+HUBJ}B?lUMm^$7+DvR%~CebYt5WI!(DG6{R~|gF&7{(dUPd<<0jRk<nM>v=8%R
z=*v&)C(CydRLyWtZaDgfG$-UYJ3RDVoL}k-d5wfKknhoFD|D=HsullW^ox?NS8Q}a
zck!MY=40{>nRH*pG4i-ufZp?=X|&u_TL*+H@y{B#<RV#Sr<?KWt2%=YKnHQX>9!*I
z#hv4)8#ckiC!@3$pd}*LtU`GP>vsLl*sTP6*9E!9=zULJCVUKLr=ig}i&2`&K$&_`
z60lu;^ZYTsDk`a|`>%PL(qL=H*w~(DjYi_RI3cn4z`!?d2(R1}xbzSAkPC=nV6-DX
z;PQjNYdtI%Nea}-H*{Bbv%K4&_PAo%b4tG1Ts;-M$HR-D#$X62dCbd@@0i3WhZT$l
znfbxj6SpWjpifTFTj>>zOHU=E@3EfCL@bOQz4gnR@#Cf8n>&W}Uqim`YDL(Mp46_%
z`*|#W;n=p?ur@Np9#ZAFQ#$?$E0qk;8V66IABVG~8^s($%*R3!DZ=D6aV>2Cb24M|
zm7AFN)-EY}Nt>jT-u0`1_9Fpcp#5^J`<7VxU!tQ*nSqC<sx$O`(zYC?@3?J9r*5AQ
znyvxH`oktxAG~vhvW8o-a(3}Dsyj`w9mqt+jX_kOy3ZBfZTAcSexIV@9P3F<l`CQs
z4J3u&t$&H4pdyRhgYG82Yc?5qK0;`K5_<ETvTtaUh4-;d_2Ju0awb%PA%jwhCb05F
z(aPze{vp9%qgp0{2r25M{c7@7H{Zl&8GIUva@WL3nSr9_SOKhE%eR@A78-Z)-%j0s
z=hw3y%RUp|vn;t3oWXm_(<e8WfV|aHBpKr;owrjkA<1eHvhliN;pMX5?l7Wu24@Je
zL-?q55I-3I@y|2FD(rYMBI!6;qTMTFQ?;xMzBE){-ZXRii|CfU|4TXvX@Q^O6-)b`
zKSb6&d~E?Pmc+jKkA>(5tBi7@OUBlRVwfJqoNWF}DOJ*;qWG#zF>5I!r!QWdlvupz
zXd%c8E2dG04>I5Z7PHtqpBAAt$>(h=9GxZ+uJWWC?RD{s*`dS4AdubM>fJA^KfcpC
zB!Tia<PMF>pV!ig;FZBXU!t}1vfXZ#UT(Jjnh5RF`3Ow0LEV}394bI;yKd9y!rZp1
zDL*vbIG}ClFq<-=?SCxf=o~uVCC(m*KSef3)Mz%*_Bu{H;Y1<R#XN>}h&jt=b~Vv*
zI}iEGV6EVCW9bRo9(!N}Z?GcQSiIfbz|EOwXCL_~>^f`cQg8$zZZ7b|nlk~i8^QLO
zYq{jCoikoIpb(HN8eY4GOgOmHknFn9-+0PCuRmc|_(P&1#zG$lOc7(AFM1d*_&&V$
zXg3FbRPsucfBSlWdomlJqenE*io2q?_^}!l8{fBdbXD*S&<x<q;Qme<p7g`pg$;4Y
zsml*{-Hso^TY#pzoA&3PEA=Ly`poVOSxjoUbp?_RH+bY=I>0_Xp2#1$h`6$>*v!7d
z+4ND)o{qSQakkeNU`C~gce2AErR8W)aIMA};tXgB=eYdN<;8A|r-2$;0{-v|EAzX`
zH(hZJ$#-$IDBrQ1Y)!tM!7TBdh|&}1^2i%oBMYA0nWta+TwyPXC~mLQ*RudAVpkO-
zZ=D`KS+C#h<R9CC1~Qbv7T?>dS6@&otK1R2*mqmRLi9c7&!y`$mfWFc$<U8A?!E<&
znuw7?5u3FpqYv1RKly1Y!TNhjeN;aAZa6UFwT7RGFj<i^d~vc;-NNwR;uk|hTL$m-
zxxR}yCnw<5q1(b7ukVIOW|sFv>-*P!P*&7yHipRxs04nWZ<tthv7erp#w(WSx2(a6
z>xSLbN}b~DBiDjmZOE?)NX@W_-c0Czc-D35QFtXMev7+|B<UVp?5gt^OMJL)K?uuu
zX=p#$A+imZEi2E^h43CYS#Lx)W%6Sp%mOZB9i_kX4=7&a=X^D-e^b$+dRx6X*YRIz
z{p;)-q-;@m9c6Vk(&a(Dn`k~?=H*-w#PC!wS5oSyUiz79Btu!##_Z`0o@vnm6@xyQ
z(t(LX?z!<C{xeqc*h+IDH}n^FsXR7}7si;GHd9W@96!h)1or$vXuh%uyP&mtEpBcU
z80s2!wax;u=HbV^*!`QGcu8qD7nd0sT{r#*Ez^$AOECYW3Djbbj!XT@cL;}Uz}Y^t
znM{5?Z@u7M$z24Ok-ZZ7*#|&MX5M8PJpD==4^D|Wmk`kM9&Yp%cy=hYeh6i%f3hUJ
zL<*}cJtrSUng65~@72&Q<8}`7{rvr<yYeOiD(0FeH5U#h<#iYCd}hQ)^a)jqe%J@C
zZhHOm>oKfpT|;k#>XhFJEzbW_$x~wOc^R7k3)!hA0)S-3(M+H855eluMXrS6-rlyr
zOpLG0cV?rdrf3<xFySglba9n-B4|S*J#qYY0s7F>Jtz4kMhIh6Q3L&kuIk6A-h?^6
z&>=|lK>J`E4JWL6{P72&O=;^)Z)2h|U>!s{A0)4c;hz@Swo`VWYh_5MZ#lQ3KM4L>
zuCN%k=%Ug0HClh#C(XxeOo~YK&>*DCl8pB>kZRXua77KvUf1Sf^u@cbStYu}!sVH=
z9Di(5QY=$}>Q5tldWM#mk}n)eP9LRFB&Qnaj?wCjv)}|$Hxdf+4)FqNwd`pVUV2No
z1lP(6W7MQ_tnn>RI_cE7af93_Y9NSN*4HQiKW0dsH@%aPTu!K}xIhY4fcj;(rkarL
z{MP<Ts;A#cc6q^=9SGlR>;u+X@d8sC@6GJ^Q`wJUq~|3n-j*>b%k--S6qTm|R|y5Z
zG#rTORrHAVyA~w1=h%-IRH%gT*n%5d9v3L`?Lau(FxwoHMT6zR;^G|}&!)%LZ~pBj
zmjBZb*o^h>`*tSU#=c1eZ5)PwS=2eea5<haQbav@A*sqg`zJGk!P3=s;I>+Xfw>-h
zb2VN2x7BZJp;aTs=^X8iL`GA@Z?NjaaBkqwZwcW48~gEPoO|QhWZzCd>0J}5i))oi
z*>Se%1hA#CkPf6A8I}i9U>iy_wD{C+na*oo%rb!K+MB&!z?R?<RKCI(zO_+6RKyXK
zJsbK|R{&p_GepA)MUS>)zO?}wm*;g~xS;{L1!ehpulI!4NB&LW9NA#g#upG)zZ6hH
z3kx&E9T-)P;PFvUPx>g2_b}+U%D|^i)_6u)A`XflK*NY06&5`vp9x3NE8m_>0Dj28
z=bQWo@|)D;+*LCC9_*If18d7DAKU(wT-SPZ+z+$V%3z5{^Ws&2+!T|UI}SOie#D9a
z$hFj2iK%AC?M;*P!2Ir89Qr_;O&HU)G~T-+QfLvueIm3B<hn5S{!^^?Ja_-^C_Jdn
zI$nxLi05xYb}~|vD@L1gnd+lJQ%iQqOq|9Ef&G90Ql8OMDoyPb-Xz&Xtv}cAdJrf#
zl~cx60yAH`!=-l}P&gQD=Z|Hrx4bdg4_kH*$1<`+)5W(4Ft(roHp_ksC<7$o;386b
z9z)qnlp7Ae<l<A9JBrat)v;vTgVE6^zXu%w_KbP;ohoAeqBL4pp$T{1ZQaW0c2SH-
zSoICF-MNU;^;2|RN}qV*zk+>9n7iaiyZxSn@wp;@tX+~(6N*qN+%Evp`o1CPRrJ~c
z?1(Gzr_(%^uYi(7OcGIbPvYuS#K*#h#$`?7=ks!Ua_ajJXv?^0j?{X4*cnJ^J(Jwk
zBYLQ(3`qDT>2zUR!wqiGI&ZEWSkZH2ZVo1yz1W&*FDQ$W7&lq$s(#}r3#rQJGx<Vr
z)!fOX3-qjOXTs~rwp1pqeVfQ#H41y5rELVtziVrQQ9m+p0ddb>PaqTbN&Z)+qiIxY
zTQ&XTyqdfABLDqy|5<4CSq<CEddEAla*rs^|8b{-FKq!6SH2pc$`!P1gdFT#Y^$vi
GY5xbZzmiV?

literal 0
HcmV?d00001

diff --git a/tests/test_context.nim b/tests/test_context.nim
index 605564c..9485de5 100644
--- a/tests/test_context.nim
+++ b/tests/test_context.nim
@@ -363,6 +363,51 @@ block:
 
   ctx.image.writeFile("tests/images/context/clip_1c.png")
 
+block:
+  let ctx = newContext(newImage(300, 150))
+
+  ctx.fillStyle = "blue"
+  ctx.fillRect(0, 0, ctx.image.width.float32, ctx.image.height.float32)
+
+  ctx.beginPath()
+  ctx.circle(100, 75, 50)
+  ctx.clip()
+
+  ctx.saveLayer()
+
+  ctx.fillStyle = "red"
+  ctx.fillRect(0, 0, ctx.image.width.float32, ctx.image.height.float32)
+  ctx.fillStyle = "orange"
+  ctx.fillRect(0, 0, 100, 100)
+
+  ctx.restore()
+
+  ctx.image.writeFile("tests/images/context/clip_1d.png")
+
+block:
+  let ctx = newContext(newImage(300, 150))
+
+  ctx.save()
+
+  ctx.beginPath()
+  ctx.circle(100, 75, 50)
+  ctx.clip()
+
+  ctx.saveLayer()
+
+  ctx.fillStyle = "red"
+  ctx.fillRect(0, 0, ctx.image.width.float32, ctx.image.height.float32)
+  ctx.fillStyle = "orange"
+  ctx.fillRect(0, 0, 100, 100)
+
+  ctx.restore() # Pop the layer
+  ctx.restore() # Pop the clip
+
+  ctx.fillStyle = "blue"
+  ctx.fillRect(0, 0, ctx.image.width.float32, ctx.image.height.float32)
+
+  ctx.image.writeFile("tests/images/context/clip_1e.png")
+
 block:
   let ctx = newContext(newImage(300, 150))
 
@@ -393,3 +438,31 @@ block:
   ctx.fillRect(0, 0, ctx.image.width.float32, ctx.image.height.float32)
 
   image.writeFile("tests/images/context/clip_3.png")
+
+block:
+  let image = newImage(300, 150)
+
+  let ctx = newContext(image)
+  ctx.font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+  ctx.font.size = 50
+  ctx.fillStyle = "blue"
+
+  ctx.saveLayer()
+
+  var circlePath: Path
+  circlePath.circle(150, 75, 75)
+
+  ctx.clip(circlePath)
+
+  ctx.fillText("Hello world", 50, 90)
+
+  ctx.restore()
+
+  image.writeFile("tests/images/context/clip_text.png")
+
+block:
+  let ctx = newContext(100, 100)
+  ctx.font = readFont("tests/fonts/Roboto-Regular_1.ttf")
+
+  let metrics = ctx.measureText("Hello world")
+  doAssert metrics.width == 61

From 67c55b0422e37505ee500b6e6182f19659ac6462 Mon Sep 17 00:00:00 2001
From: Ryan Oldenburg <guzba8@gmail.com>
Date: Sun, 23 May 2021 20:52:06 -0500
Subject: [PATCH 4/4] multiple layers test

---
 tests/images/context/clip_1f.png | Bin 0 -> 3368 bytes
 tests/test_context.nim           |  25 +++++++++++++++++++++++++
 2 files changed, 25 insertions(+)
 create mode 100644 tests/images/context/clip_1f.png

diff --git a/tests/images/context/clip_1f.png b/tests/images/context/clip_1f.png
new file mode 100644
index 0000000000000000000000000000000000000000..844523eb76490106f5552b10d8edcbe8152963da
GIT binary patch
literal 3368
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
E7vEw)1poj5

literal 0
HcmV?d00001

diff --git a/tests/test_context.nim b/tests/test_context.nim
index 9485de5..8c1f499 100644
--- a/tests/test_context.nim
+++ b/tests/test_context.nim
@@ -408,6 +408,31 @@ block:
 
   ctx.image.writeFile("tests/images/context/clip_1e.png")
 
+
+block:
+  let ctx = newContext(newImage(300, 150))
+
+  ctx.save()
+
+  ctx.beginPath()
+  ctx.circle(100, 75, 50)
+  ctx.clip()
+
+  ctx.saveLayer()
+
+  ctx.fillStyle = "red"
+  ctx.fillRect(0, 0, ctx.image.width.float32, ctx.image.height.float32)
+
+  ctx.restore()
+  ctx.saveLayer()
+
+  ctx.fillStyle = "orange"
+  ctx.fillRect(0, 0, 100, 100)
+
+  ctx.restore() # Pop the layer
+
+  ctx.image.writeFile("tests/images/context/clip_1f.png")
+
 block:
   let ctx = newContext(newImage(300, 150))