shiny/iconvg: implement arcs.

Change-Id: Id2948c1244afcf0f76410d981ebf2ad341168fa3
Reviewed-on: https://go-review.googlesource.com/31115
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/iconvg/buffer.go b/shiny/iconvg/buffer.go
index fe5f44c..5bdad2a 100644
--- a/shiny/iconvg/buffer.go
+++ b/shiny/iconvg/buffer.go
@@ -191,6 +191,13 @@
 	return 4
 }
 
+func (b *buffer) encodeAngle(f float32) int {
+	// Normalize f to the range [0, 1).
+	g := float64(f)
+	g -= math.Floor(g)
+	return b.encodeZeroToOne(float32(g))
+}
+
 func (b *buffer) encodeZeroToOne(f float32) int {
 	if u := uint32(f * 15120); float32(u) == f*15120 && u < 15120 {
 		if u%126 == 0 {
diff --git a/shiny/iconvg/decode.go b/shiny/iconvg/decode.go
index e245e63..b5bb381 100644
--- a/shiny/iconvg/decode.go
+++ b/shiny/iconvg/decode.go
@@ -485,9 +485,30 @@
 			if p != nil && i != 0 {
 				p(nil, "%s, implicit\n", op)
 			}
-			src, err = decodeCoordinates(coords[:nCoords], p, src)
-			if err != nil {
-				return nil, nil, err
+			var largeArc, sweep bool
+			if op[0] != 'A' && op[0] != 'a' {
+				src, err = decodeCoordinates(coords[:nCoords], p, src)
+				if err != nil {
+					return nil, nil, err
+				}
+			} else {
+				// We have an absolute or relative arcTo.
+				src, err = decodeCoordinates(coords[:2], p, src)
+				if err != nil {
+					return nil, nil, err
+				}
+				coords[2], src, err = decodeAngle(p, src)
+				if err != nil {
+					return nil, nil, err
+				}
+				largeArc, sweep, src, err = decodeArcToFlags(p, src)
+				if err != nil {
+					return nil, nil, err
+				}
+				src, err = decodeCoordinates(coords[4:6], p, src)
+				if err != nil {
+					return nil, nil, err
+				}
 			}
 
 			if dst == nil {
@@ -496,54 +517,27 @@
 			switch op[0] {
 			case 'L':
 				dst.AbsLineTo(coords[0], coords[1])
-				continue
 			case 'l':
 				dst.RelLineTo(coords[0], coords[1])
-				continue
 			case 'T':
 				dst.AbsSmoothQuadTo(coords[0], coords[1])
-				continue
 			case 't':
 				dst.RelSmoothQuadTo(coords[0], coords[1])
-				continue
 			case 'Q':
 				dst.AbsQuadTo(coords[0], coords[1], coords[2], coords[3])
-				continue
 			case 'q':
 				dst.RelQuadTo(coords[0], coords[1], coords[2], coords[3])
-				continue
 			case 'S':
 				dst.AbsSmoothCubeTo(coords[0], coords[1], coords[2], coords[3])
-				continue
 			case 's':
 				dst.RelSmoothCubeTo(coords[0], coords[1], coords[2], coords[3])
-				continue
 			case 'C':
 				dst.AbsCubeTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5])
-				continue
 			case 'c':
 				dst.RelCubeTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5])
-				continue
-			}
-
-			// We have an absolute or relative arcTo.
-			src, err = decodeCoordinates(coords[:3], p, src)
-			if err != nil {
-				return nil, nil, err
-			}
-			var largeArc, sweep bool
-			largeArc, sweep, src, err = decodeArcToFlags(p, src)
-			if err != nil {
-				return nil, nil, err
-			}
-			src, err = decodeCoordinates(coords[4:6], p, src)
-			if err != nil {
-				return nil, nil, err
-			}
-
-			if op[0] == 'A' {
+			case 'A':
 				dst.AbsArcTo(coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5])
-			} else {
+			case 'a':
 				dst.RelArcTo(coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5])
 			}
 		}
@@ -665,6 +659,17 @@
 	return src, nil
 }
 
+func decodeAngle(p printer, src buffer) (float32, buffer, error) {
+	x, n := src.decodeZeroToOne()
+	if n == 0 {
+		return 0, nil, errInvalidNumber
+	}
+	if p != nil {
+		p(src[:n], "    %v × 360 degrees (%v degrees)\n", x, x*360)
+	}
+	return x, src[n:], nil
+}
+
 func decodeArcToFlags(p printer, src buffer) (bool, bool, buffer, error) {
 	x, n := src.decodeNatural()
 	if n == 0 {
diff --git a/shiny/iconvg/decode_test.go b/shiny/iconvg/decode_test.go
index 71ee314..26b780b 100644
--- a/shiny/iconvg/decode_test.go
+++ b/shiny/iconvg/decode_test.go
@@ -132,6 +132,7 @@
 }{
 	{"testdata/action-info.lores", ""},
 	{"testdata/action-info.hires", ""},
+	{"testdata/arcs", ""},
 	{"testdata/blank", ""},
 	{"testdata/lod-polygon", ";64"},
 	{"testdata/video-005.primitive", ""},
diff --git a/shiny/iconvg/encode.go b/shiny/iconvg/encode.go
index 9b1bb88..9e00bfd 100644
--- a/shiny/iconvg/encode.go
+++ b/shiny/iconvg/encode.go
@@ -145,39 +145,37 @@
 	return e.lod0, e.lod1
 }
 
-func (e *Encoder) SetCSel(cSel uint8) {
-	if e.err != nil {
+func (e *Encoder) checkModeStyling() {
+	if e.mode == modeStyling {
 		return
 	}
-	if e.mode != modeStyling {
-		if e.mode == modeInitial {
-			e.appendDefaultMetadata()
-		} else {
-			e.err = errStylingOpsUsedInDrawingMode
-			return
-		}
+	if e.mode == modeInitial {
+		e.appendDefaultMetadata()
+		return
+	}
+	e.err = errStylingOpsUsedInDrawingMode
+}
+
+func (e *Encoder) SetCSel(cSel uint8) {
+	e.checkModeStyling()
+	if e.err != nil {
+		return
 	}
 	e.cSel = cSel & 0x3f
 	e.buf = append(e.buf, e.cSel)
 }
 
 func (e *Encoder) SetNSel(nSel uint8) {
+	e.checkModeStyling()
 	if e.err != nil {
 		return
 	}
-	if e.mode != modeStyling {
-		if e.mode == modeInitial {
-			e.appendDefaultMetadata()
-		} else {
-			e.err = errStylingOpsUsedInDrawingMode
-			return
-		}
-	}
 	e.nSel = nSel & 0x3f
 	e.buf = append(e.buf, e.nSel|0x40)
 }
 
 func (e *Encoder) SetCReg(adj uint8, incr bool, c Color) {
+	e.checkModeStyling()
 	if e.err != nil {
 		return
 	}
@@ -216,6 +214,7 @@
 }
 
 func (e *Encoder) SetNReg(adj uint8, incr bool, f float32) {
+	e.checkModeStyling()
 	if e.err != nil {
 		return
 	}
@@ -249,17 +248,10 @@
 }
 
 func (e *Encoder) SetLOD(lod0, lod1 float32) {
+	e.checkModeStyling()
 	if e.err != nil {
 		return
 	}
-	if e.mode != modeStyling {
-		if e.mode == modeInitial {
-			e.appendDefaultMetadata()
-		} else {
-			e.err = errStylingOpsUsedInDrawingMode
-			return
-		}
-	}
 	e.lod0 = lod0
 	e.lod1 = lod1
 	e.buf = append(e.buf, 0xc7)
@@ -268,17 +260,10 @@
 }
 
 func (e *Encoder) StartPath(adj uint8, x, y float32) {
+	e.checkModeStyling()
 	if e.err != nil {
 		return
 	}
-	if e.mode != modeStyling {
-		if e.mode == modeInitial {
-			e.appendDefaultMetadata()
-		} else {
-			e.err = errStylingOpsUsedInDrawingMode
-			return
-		}
-	}
 	if adj < 0 || 6 < adj {
 		e.err = errInvalidSelectorAdjustment
 		return
@@ -389,7 +374,7 @@
 				for j := m; j > 0; j-- {
 					e.buf.encodeCoordinate(e.quantize(e.drawArgs[i+0]))
 					e.buf.encodeCoordinate(e.quantize(e.drawArgs[i+1]))
-					e.buf.encodeZeroToOne(e.drawArgs[i+2])
+					e.buf.encodeAngle(e.drawArgs[i+2])
 					e.buf.encodeNatural(uint32(e.drawArgs[i+3]))
 					e.buf.encodeCoordinate(e.quantize(e.drawArgs[i+4]))
 					e.buf.encodeCoordinate(e.quantize(e.drawArgs[i+5]))
diff --git a/shiny/iconvg/encode_test.go b/shiny/iconvg/encode_test.go
index 7d28cdd..6b1bdf9 100644
--- a/shiny/iconvg/encode_test.go
+++ b/shiny/iconvg/encode_test.go
@@ -86,6 +86,49 @@
 	}
 }
 
+func TestEncodeArcs(t *testing.T) {
+	var e Encoder
+
+	e.SetCReg(1, false, RGBAColor(color.RGBA{0xff, 0x00, 0x00, 0xff}))
+	e.SetCReg(2, false, RGBAColor(color.RGBA{0xff, 0xff, 0x00, 0xff}))
+	e.SetCReg(3, false, RGBAColor(color.RGBA{0x00, 0x00, 0x00, 0xff}))
+	e.SetCReg(4, false, RGBAColor(color.RGBA{0x00, 0x00, 0x80, 0xff}))
+
+	e.StartPath(1, -10, 0)
+	e.RelHLineTo(-15)
+	e.RelArcTo(15, 15, 0, true, false, 15, -15)
+	e.ClosePathEndPath()
+
+	e.StartPath(2, -14, -4)
+	e.RelVLineTo(-15)
+	e.RelArcTo(15, 15, 0, false, false, -15, 15)
+	e.ClosePathEndPath()
+
+	const thirtyDegrees = 30.0 / 360
+	e.StartPath(3, -15, 30)
+	e.RelLineTo(5.0, -2.5)
+	e.RelArcTo(2.5, 2.5, -thirtyDegrees, false, true, 5.0, -2.5)
+	e.RelLineTo(5.0, -2.5)
+	e.RelArcTo(2.5, 5.0, -thirtyDegrees, false, true, 5.0, -2.5)
+	e.RelLineTo(5.0, -2.5)
+	e.RelArcTo(2.5, 7.5, -thirtyDegrees, false, true, 5.0, -2.5)
+	e.RelLineTo(5.0, -2.5)
+	e.RelArcTo(2.5, 10.0, -thirtyDegrees, false, true, 5.0, -2.5)
+	e.RelLineTo(5.0, -2.5)
+	e.AbsVLineTo(30)
+	e.ClosePathEndPath()
+
+	for largeArc := 0; largeArc <= 1; largeArc++ {
+		for sweep := 0; sweep <= 1; sweep++ {
+			e.StartPath(4, 10+8*float32(sweep), -28+8*float32(largeArc))
+			e.RelArcTo(6, 3, 0, largeArc != 0, sweep != 0, 6, 3)
+			e.ClosePathEndPath()
+		}
+	}
+
+	testEncode(t, &e, "testdata/arcs.ivg")
+}
+
 var video005PrimitiveSVGData = []struct {
 	r, g, b uint32
 	x0, y0  int
diff --git a/shiny/iconvg/rasterizer.go b/shiny/iconvg/rasterizer.go
index ce3c95d..30f79c0 100644
--- a/shiny/iconvg/rasterizer.go
+++ b/shiny/iconvg/rasterizer.go
@@ -8,6 +8,7 @@
 	"image"
 	"image/color"
 	"image/draw"
+	"math"
 
 	"golang.org/x/image/math/f32"
 	"golang.org/x/image/vector"
@@ -120,6 +121,9 @@
 	z.lod0, z.lod1 = lod0, lod1
 }
 
+func (z *Rasterizer) unabsX(x float32) float32 { return x/z.scaleX - z.biasX }
+func (z *Rasterizer) unabsY(y float32) float32 { return y/z.scaleY - z.biasY }
+
 func (z *Rasterizer) absX(x float32) float32 { return z.scaleX * (x + z.biasX) }
 func (z *Rasterizer) absY(y float32) float32 { return z.scaleY * (y + z.biasY) }
 func (z *Rasterizer) relX(x float32) float32 { return z.scaleX * x }
@@ -335,13 +339,165 @@
 		return
 	}
 	z.prevSmoothType = smoothTypeNone
-	// TODO: implement.
+
+	// We follow the "Conversion from endpoint to center parameterization"
+	// algorithm as per
+	// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
+
+	// There seems to be a bug in the spec's "implementation notes".
+	//
+	// Actual implementations, such as
+	//	- https://git.gnome.org/browse/librsvg/tree/rsvg-path.c
+	//	- http://svn.apache.org/repos/asf/xmlgraphics/batik/branches/svg11/sources/org/apache/batik/ext/awt/geom/ExtendedGeneralPath.java
+	//	- https://java.net/projects/svgsalamander/sources/svn/content/trunk/svg-core/src/main/java/com/kitfox/svg/pathcmd/Arc.java
+	//	- https://github.com/millermedeiros/SVGParser/blob/master/com/millermedeiros/geom/SVGArc.as
+	// do something slightly different (marked with a †).
+
+	// (†) The Abs isn't part of the spec. Neither is checking that Rx and Ry
+	// are non-zero (and non-NaN).
+	Rx := math.Abs(float64(rx))
+	Ry := math.Abs(float64(ry))
+	if !(Rx > 0 && Ry > 0) {
+		z.z.LineTo(f32.Vec2{x, y})
+		return
+	}
+
+	// We work in IconVG coordinates (e.g. from -32 to +32 by default), rather
+	// than destination image coordinates (e.g. the width of the dst image),
+	// since the rx and ry radii also need to be scaled, but their scaling
+	// factors can be different, and aren't trivial to calculate due to
+	// xAxisRotation.
+	//
+	// We convert back to destination image coordinates via absX and absY calls
+	// later, during arcSegmentTo.
+	pen := z.z.Pen()
+	x1 := float64(z.unabsX(pen[0]))
+	y1 := float64(z.unabsY(pen[1]))
+	x2 := float64(z.unabsX(x))
+	y2 := float64(z.unabsY(y))
+
+	phi := 2 * math.Pi * float64(xAxisRotation)
+
+	// Step 1: Compute (x1′, y1′)
+	halfDx := (x1 - x2) / 2
+	halfDy := (y1 - y2) / 2
+	cosPhi := math.Cos(phi)
+	sinPhi := math.Sin(phi)
+	x1Prime := +cosPhi*halfDx + sinPhi*halfDy
+	y1Prime := -sinPhi*halfDx + cosPhi*halfDy
+
+	// Step 2: Compute (cx′, cy′)
+	rxSq := Rx * Rx
+	rySq := Ry * Ry
+	x1PrimeSq := x1Prime * x1Prime
+	y1PrimeSq := y1Prime * y1Prime
+
+	// (†) Check that the radii are large enough.
+	radiiCheck := x1PrimeSq/rxSq + y1PrimeSq/rySq
+	if radiiCheck > 1 {
+		c := math.Sqrt(radiiCheck)
+		Rx *= c
+		Ry *= c
+		rxSq = Rx * Rx
+		rySq = Ry * Ry
+	}
+
+	denom := rxSq*y1PrimeSq + rySq*x1PrimeSq
+	step2 := 0.0
+	if a := rxSq*rySq/denom - 1; a > 0 {
+		step2 = math.Sqrt(a)
+	}
+	if largeArc == sweep {
+		step2 = -step2
+	}
+	cxPrime := +step2 * Rx * y1Prime / Ry
+	cyPrime := -step2 * Ry * x1Prime / Rx
+
+	// Step 3: Compute (cx, cy) from (cx′, cy′)
+	cx := +cosPhi*cxPrime - sinPhi*cyPrime + (x1+x2)/2
+	cy := +sinPhi*cxPrime + cosPhi*cyPrime + (y1+y2)/2
+
+	// Step 4: Compute θ1 and Δθ
+	ax := (+x1Prime - cxPrime) / Rx
+	ay := (+y1Prime - cyPrime) / Ry
+	bx := (-x1Prime - cxPrime) / Rx
+	by := (-y1Prime - cyPrime) / Ry
+	theta1 := angle(1, 0, ax, ay)
+	deltaTheta := angle(ax, ay, bx, by)
+	if sweep {
+		if deltaTheta < 0 {
+			deltaTheta += 2 * math.Pi
+		}
+	} else {
+		if deltaTheta > 0 {
+			deltaTheta -= 2 * math.Pi
+		}
+	}
+
+	// This ends the
+	// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
+	// algorithm. What follows below is specific to this implementation.
+
+	// We approximate an arc by one or more cubic Bézier curves.
+	n := int(math.Ceil(math.Abs(deltaTheta) / (math.Pi/2 + 0.001)))
+	for i := 0; i < n; i++ {
+		z.arcSegmentTo(cx, cy,
+			theta1+deltaTheta*float64(i+0)/float64(n),
+			theta1+deltaTheta*float64(i+1)/float64(n),
+			Rx, Ry, cosPhi, sinPhi,
+		)
+	}
+}
+
+// arcSegmentTo approximates an arc by a cubic Bézier curve. The mathematical
+// formulae for the control points are the same as that used by librsvg.
+func (z *Rasterizer) arcSegmentTo(cx, cy, theta1, theta2, rx, ry, cosPhi, sinPhi float64) {
+	halfDeltaTheta := (theta2 - theta1) * 0.5
+	q := math.Sin(halfDeltaTheta * 0.5)
+	t := (8 * q * q) / (3 * math.Sin(halfDeltaTheta))
+	cos1 := math.Cos(theta1)
+	sin1 := math.Sin(theta1)
+	cos2 := math.Cos(theta2)
+	sin2 := math.Sin(theta2)
+	x1 := rx * (+cos1 - t*sin1)
+	y1 := ry * (+sin1 + t*cos1)
+	x2 := rx * (+cos2 + t*sin2)
+	y2 := ry * (+sin2 - t*cos2)
+	x3 := rx * (+cos2)
+	y3 := ry * (+sin2)
+	z.z.CubeTo(f32.Vec2{
+		z.absX(float32(cx + cosPhi*x1 - sinPhi*y1)),
+		z.absY(float32(cy + sinPhi*x1 + cosPhi*y1)),
+	}, f32.Vec2{
+		z.absX(float32(cx + cosPhi*x2 - sinPhi*y2)),
+		z.absY(float32(cy + sinPhi*x2 + cosPhi*y2)),
+	}, f32.Vec2{
+		z.absX(float32(cx + cosPhi*x3 - sinPhi*y3)),
+		z.absY(float32(cy + sinPhi*x3 + cosPhi*y3)),
+	})
 }
 
 func (z *Rasterizer) RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
-	if z.disabled {
-		return
+	a := z.relVec2(x, y)
+	z.AbsArcTo(rx, ry, xAxisRotation, largeArc, sweep, a[0], a[1])
+}
+
+// angle returns the angle between the u and v vectors.
+func angle(ux, uy, vx, vy float64) float64 {
+	uNorm := math.Sqrt(ux*ux + uy*uy)
+	vNorm := math.Sqrt(vx*vx + vy*vy)
+	norm := uNorm * vNorm
+	cos := (ux*vx + uy*vy) / norm
+	ret := 0.0
+	if cos <= -1 {
+		ret = math.Pi
+	} else if cos >= +1 {
+		ret = 0
+	} else {
+		ret = math.Acos(cos)
 	}
-	z.prevSmoothType = smoothTypeNone
-	// TODO: implement.
+	if ux*vy < uy*vx {
+		return -ret
+	}
+	return +ret
 }
diff --git a/shiny/iconvg/testdata/README b/shiny/iconvg/testdata/README
index 67eb33c..c883fe3 100644
--- a/shiny/iconvg/testdata/README
+++ b/shiny/iconvg/testdata/README
@@ -15,6 +15,15 @@
 
 
 
+arcs.ivg is inspired by the two examples at
+https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
+
+arcs.ivg.disassembly is a disassembly of that IconVG file.
+
+arcs.png is a rendering of that IconVG file.
+
+
+
 blank.ivg is a blank, square graphic.
 
 blank.ivg.disassembly is a disassembly of that IconVG file.
diff --git a/shiny/iconvg/testdata/arcs.ivg b/shiny/iconvg/testdata/arcs.ivg
new file mode 100644
index 0000000..f03a704
--- /dev/null
+++ b/shiny/iconvg/testdata/arcs.ivg
Binary files differ
diff --git a/shiny/iconvg/testdata/arcs.ivg.disassembly b/shiny/iconvg/testdata/arcs.ivg.disassembly
new file mode 100644
index 0000000..1c2eb05
--- /dev/null
+++ b/shiny/iconvg/testdata/arcs.ivg.disassembly
@@ -0,0 +1,129 @@
+89 49 56 47   IconVG Magic identifier
+00            Number of metadata chunks: 0
+81            Set CREG[CSEL-1] to a 1 byte color
+64                RGBA ff0000ff
+82            Set CREG[CSEL-2] to a 1 byte color
+78                RGBA ffff00ff
+83            Set CREG[CSEL-3] to a 1 byte color
+00                RGBA 000000ff
+84            Set CREG[CSEL-4] to a 1 byte color
+02                RGBA 000080ff
+c1            Start path, filled with CREG[CSEL-1]; M (absolute moveTo)
+6c                -10
+80                +0
+e7            h (relative horizontal lineTo)
+62                -15
+d0            a (relative arcTo), 1 reps
+9e                +15
+9e                +15
+00                0 × 360 degrees (0 degrees)
+02                0x1 (largeArc=1, sweep=0)
+9e                +15
+62                -15
+e1            z (closePath); end path
+c2            Start path, filled with CREG[CSEL-2]; M (absolute moveTo)
+64                -14
+78                -4
+e9            v (relative vertical lineTo)
+62                -15
+d0            a (relative arcTo), 1 reps
+9e                +15
+9e                +15
+00                0 × 360 degrees (0 degrees)
+00                0x0 (largeArc=0, sweep=0)
+62                -15
+9e                +15
+e1            z (closePath); end path
+c3            Start path, filled with CREG[CSEL-3]; M (absolute moveTo)
+62                -15
+bc                +30
+20            l (relative lineTo), 1 reps
+8a                +5
+81 7d             -2.5
+d0            a (relative arcTo), 1 reps
+81 82             +2.5
+81 82             +2.5
+dc                0.9166667 × 360 degrees (330 degrees)
+04                0x2 (largeArc=0, sweep=1)
+8a                +5
+81 7d             -2.5
+20            l (relative lineTo), 1 reps
+8a                +5
+81 7d             -2.5
+d0            a (relative arcTo), 1 reps
+81 82             +2.5
+8a                +5
+dc                0.9166667 × 360 degrees (330 degrees)
+04                0x2 (largeArc=0, sweep=1)
+8a                +5
+81 7d             -2.5
+20            l (relative lineTo), 1 reps
+8a                +5
+81 7d             -2.5
+d0            a (relative arcTo), 1 reps
+81 82             +2.5
+81 87             +7.5
+dc                0.9166667 × 360 degrees (330 degrees)
+04                0x2 (largeArc=0, sweep=1)
+8a                +5
+81 7d             -2.5
+20            l (relative lineTo), 1 reps
+8a                +5
+81 7d             -2.5
+d0            a (relative arcTo), 1 reps
+81 82             +2.5
+94                +10
+dc                0.9166667 × 360 degrees (330 degrees)
+04                0x2 (largeArc=0, sweep=1)
+8a                +5
+81 7d             -2.5
+20            l (relative lineTo), 1 reps
+8a                +5
+81 7d             -2.5
+e8            V (absolute vertical lineTo)
+bc                +30
+e1            z (closePath); end path
+c4            Start path, filled with CREG[CSEL-4]; M (absolute moveTo)
+94                +10
+48                -28
+d0            a (relative arcTo), 1 reps
+8c                +6
+86                +3
+00                0 × 360 degrees (0 degrees)
+00                0x0 (largeArc=0, sweep=0)
+8c                +6
+86                +3
+e1            z (closePath); end path
+c4            Start path, filled with CREG[CSEL-4]; M (absolute moveTo)
+a4                +18
+48                -28
+d0            a (relative arcTo), 1 reps
+8c                +6
+86                +3
+00                0 × 360 degrees (0 degrees)
+04                0x2 (largeArc=0, sweep=1)
+8c                +6
+86                +3
+e1            z (closePath); end path
+c4            Start path, filled with CREG[CSEL-4]; M (absolute moveTo)
+94                +10
+58                -20
+d0            a (relative arcTo), 1 reps
+8c                +6
+86                +3
+00                0 × 360 degrees (0 degrees)
+02                0x1 (largeArc=1, sweep=0)
+8c                +6
+86                +3
+e1            z (closePath); end path
+c4            Start path, filled with CREG[CSEL-4]; M (absolute moveTo)
+a4                +18
+58                -20
+d0            a (relative arcTo), 1 reps
+8c                +6
+86                +3
+00                0 × 360 degrees (0 degrees)
+06                0x3 (largeArc=1, sweep=1)
+8c                +6
+86                +3
+e1            z (closePath); end path
diff --git a/shiny/iconvg/testdata/arcs.png b/shiny/iconvg/testdata/arcs.png
new file mode 100644
index 0000000..acebcaa
--- /dev/null
+++ b/shiny/iconvg/testdata/arcs.png
Binary files differ