shiny/iconvg: implement colors.

Gradients remains a TODO.

Change-Id: Ia3eb4a2340cccffc83d0e051176b47615b3eed71
Reviewed-on: https://go-review.googlesource.com/30172
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/iconvg/buffer.go b/shiny/iconvg/buffer.go
index 473421b..fe5f44c 100644
--- a/shiny/iconvg/buffer.go
+++ b/shiny/iconvg/buffer.go
@@ -5,11 +5,10 @@
 package iconvg
 
 import (
+	"image/color"
 	"math"
 )
 
-// TODO: decoding and encoding colors, not just numbers.
-
 // buffer holds an encoded IconVG graphic.
 //
 // The decodeXxx methods return the decoded value and an integer n, the number
@@ -79,6 +78,56 @@
 	}
 }
 
+func (b buffer) decodeColor1() (c Color, n int) {
+	if len(b) < 1 {
+		return Color{}, 0
+	}
+	return decodeColor1(b[0]), 1
+}
+
+func (b buffer) decodeColor2() (c Color, n int) {
+	if len(b) < 2 {
+		return Color{}, 0
+	}
+	return RGBAColor(color.RGBA{
+		R: 0x11 * (b[0] >> 4),
+		G: 0x11 * (b[0] & 0x0f),
+		B: 0x11 * (b[1] >> 4),
+		A: 0x11 * (b[1] & 0x0f),
+	}), 2
+}
+
+func (b buffer) decodeColor3Direct() (c Color, n int) {
+	if len(b) < 3 {
+		return Color{}, 0
+	}
+	return RGBAColor(color.RGBA{
+		R: b[0],
+		G: b[1],
+		B: b[2],
+		A: 0xff,
+	}), 3
+}
+
+func (b buffer) decodeColor4() (c Color, n int) {
+	if len(b) < 4 {
+		return Color{}, 0
+	}
+	return RGBAColor(color.RGBA{
+		R: b[0],
+		G: b[1],
+		B: b[2],
+		A: b[3],
+	}), 4
+}
+
+func (b buffer) decodeColor3Indirect() (c Color, n int) {
+	if len(b) < 3 {
+		return Color{}, 0
+	}
+	return BlendColor(b[0], b[1], b[2]), 3
+}
+
 func (b *buffer) encodeNatural(u uint32) {
 	if u < 1<<7 {
 		u = (u << 1)
@@ -94,18 +143,19 @@
 	*b = append(*b, uint8(u), uint8(u>>8), uint8(u>>16), uint8(u>>24))
 }
 
-func (b *buffer) encodeReal(f float32) {
+func (b *buffer) encodeReal(f float32) int {
 	if u := uint32(f); float32(u) == f && u < 1<<14 {
 		if u < 1<<7 {
 			u = (u << 1)
 			*b = append(*b, uint8(u))
-		} else {
-			u = (u << 2) | 1
-			*b = append(*b, uint8(u), uint8(u>>8))
+			return 1
 		}
-		return
+		u = (u << 2) | 1
+		*b = append(*b, uint8(u), uint8(u>>8))
+		return 2
 	}
 	b.encode4ByteReal(f)
+	return 4
 }
 
 func (b *buffer) encode4ByteReal(f float32) {
@@ -124,32 +174,79 @@
 	*b = append(*b, uint8(u), uint8(u>>8), uint8(u>>16), uint8(u>>24))
 }
 
-func (b *buffer) encodeCoordinate(f float32) {
+func (b *buffer) encodeCoordinate(f float32) int {
 	if i := int32(f); -64 <= i && i < +64 && float32(i) == f {
 		u := uint32(i + 64)
 		u = (u << 1)
 		*b = append(*b, uint8(u))
-		return
+		return 1
 	}
 	if i := int32(f * 64); -128*64 <= i && i < +128*64 && float32(i) == f*64 {
 		u := uint32(i + 128*64)
 		u = (u << 2) | 1
 		*b = append(*b, uint8(u), uint8(u>>8))
-		return
+		return 2
 	}
 	b.encode4ByteReal(f)
+	return 4
 }
 
-func (b *buffer) encodeZeroToOne(f float32) {
+func (b *buffer) encodeZeroToOne(f float32) int {
 	if u := uint32(f * 15120); float32(u) == f*15120 && u < 15120 {
 		if u%126 == 0 {
 			u = ((u / 126) << 1)
 			*b = append(*b, uint8(u))
-		} else {
-			u = (u << 2) | 1
-			*b = append(*b, uint8(u), uint8(u>>8))
+			return 1
 		}
-		return
+		u = (u << 2) | 1
+		*b = append(*b, uint8(u), uint8(u>>8))
+		return 2
 	}
 	b.encode4ByteReal(f)
+	return 4
+}
+
+func (b *buffer) encodeColor1(c Color) {
+	if x, ok := encodeColor1(c); ok {
+		*b = append(*b, x)
+		return
+	}
+	// Default to opaque black.
+	*b = append(*b, 0x00)
+}
+
+func (b *buffer) encodeColor2(c Color) {
+	if x, ok := encodeColor2(c); ok {
+		*b = append(*b, x[0], x[1])
+		return
+	}
+	// Default to opaque black.
+	*b = append(*b, 0x00, 0x0f)
+}
+
+func (b *buffer) encodeColor3Direct(c Color) {
+	if x, ok := encodeColor3Direct(c); ok {
+		*b = append(*b, x[0], x[1], x[2])
+		return
+	}
+	// Default to opaque black.
+	*b = append(*b, 0x00, 0x00, 0x00)
+}
+
+func (b *buffer) encodeColor4(c Color) {
+	if x, ok := encodeColor4(c); ok {
+		*b = append(*b, x[0], x[1], x[2], x[3])
+		return
+	}
+	// Default to opaque black.
+	*b = append(*b, 0x00, 0x00, 0x00, 0xff)
+}
+
+func (b *buffer) encodeColor3Indirect(c Color) {
+	if x, ok := encodeColor3Indirect(c); ok {
+		*b = append(*b, x[0], x[1], x[2])
+		return
+	}
+	// Default to opaque black.
+	*b = append(*b, 0x00, 0x00, 0x00)
 }
diff --git a/shiny/iconvg/buffer_test.go b/shiny/iconvg/buffer_test.go
index 5e51aa3..2c15821 100644
--- a/shiny/iconvg/buffer_test.go
+++ b/shiny/iconvg/buffer_test.go
@@ -5,6 +5,7 @@
 package iconvg
 
 import (
+	"image/color"
 	"math"
 	"testing"
 )
@@ -185,3 +186,147 @@
 		}
 	}
 }
+
+var colorTestCases = []struct {
+	in     buffer
+	decode func(buffer) (Color, int)
+	encode func(*buffer, Color)
+	want   Color
+	wantN  int
+}{{
+	buffer{},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	Color{},
+	0,
+}, {
+	buffer{0x00},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	RGBAColor(color.RGBA{0x00, 0x00, 0x00, 0xff}),
+	1,
+}, {
+	buffer{0x30},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	RGBAColor(color.RGBA{0x40, 0xff, 0xc0, 0xff}),
+	1,
+}, {
+	buffer{0x7c},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	RGBAColor(color.RGBA{0xff, 0xff, 0xff, 0xff}),
+	1,
+}, {
+	buffer{0x7d},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	RGBAColor(color.RGBA{0xc0, 0xc0, 0xc0, 0xc0}),
+	1,
+}, {
+	buffer{0x7e},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	RGBAColor(color.RGBA{0x80, 0x80, 0x80, 0x80}),
+	1,
+}, {
+	buffer{0x7f},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	RGBAColor(color.RGBA{0x00, 0x00, 0x00, 0x00}),
+	1,
+}, {
+	buffer{0x80},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	PaletteIndexColor(0x00),
+	1,
+}, {
+	buffer{0xbf},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	PaletteIndexColor(0x3f),
+	1,
+}, {
+	buffer{0xc0},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	CRegColor(0x00),
+	1,
+}, {
+	buffer{0xff},
+	buffer.decodeColor1,
+	(*buffer).encodeColor1,
+	CRegColor(0x3f),
+	1,
+}, {
+	buffer{0x01},
+	buffer.decodeColor2,
+	(*buffer).encodeColor2,
+	Color{},
+	0,
+}, {
+	buffer{0x38, 0x0f},
+	buffer.decodeColor2,
+	(*buffer).encodeColor2,
+	RGBAColor(color.RGBA{0x33, 0x88, 0x00, 0xff}),
+	2,
+}, {
+	buffer{0x00, 0x02},
+	buffer.decodeColor3Direct,
+	(*buffer).encodeColor3Direct,
+	Color{},
+	0,
+}, {
+	buffer{0x30, 0x66, 0x07},
+	buffer.decodeColor3Direct,
+	(*buffer).encodeColor3Direct,
+	RGBAColor(color.RGBA{0x30, 0x66, 0x07, 0xff}),
+	3,
+}, {
+	buffer{0x00, 0x00, 0x03},
+	buffer.decodeColor4,
+	(*buffer).encodeColor4,
+	Color{},
+	0,
+}, {
+	buffer{0x30, 0x66, 0x07, 0x80},
+	buffer.decodeColor4,
+	(*buffer).encodeColor4,
+	RGBAColor(color.RGBA{0x30, 0x66, 0x07, 0x80}),
+	4,
+}, {
+	buffer{0x00, 0x04},
+	buffer.decodeColor3Indirect,
+	(*buffer).encodeColor3Indirect,
+	Color{},
+	0,
+}, {
+	buffer{0x40, 0x7f, 0x82},
+	buffer.decodeColor3Indirect,
+	(*buffer).encodeColor3Indirect,
+	BlendColor(0x40, 0x7f, 0x82),
+	3,
+}}
+
+func TestDecodeColor(t *testing.T) {
+	for _, tc := range colorTestCases {
+		got, gotN := tc.decode(tc.in)
+		if got != tc.want || gotN != tc.wantN {
+			t.Errorf("in=%x: got %v, %d, want %v, %d", tc.in, got, gotN, tc.want, tc.wantN)
+		}
+	}
+}
+
+func TestEncodeColor(t *testing.T) {
+	for _, tc := range colorTestCases {
+		if tc.wantN == 0 {
+			continue
+		}
+		var b buffer
+		tc.encode(&b, tc.want)
+		if got, want := string(b), string(tc.in); got != want {
+			t.Errorf("value=%v:\ngot  % x\nwant % x", tc.want, got, want)
+		}
+	}
+}
diff --git a/shiny/iconvg/color.go b/shiny/iconvg/color.go
new file mode 100644
index 0000000..131d303
--- /dev/null
+++ b/shiny/iconvg/color.go
@@ -0,0 +1,179 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package iconvg
+
+import (
+	"image/color"
+)
+
+func validAlphaPremulColor(c color.RGBA) bool {
+	return c.R <= c.A && c.G <= c.A && c.B <= c.A
+}
+
+// ColorType distinguishes types of Colors.
+type ColorType uint8
+
+const (
+	// ColorTypeRGBA is a direct RGBA color.
+	ColorTypeRGBA ColorType = iota
+
+	// ColorTypePaletteIndex is an indirect color, indexing the custom palette.
+	ColorTypePaletteIndex
+
+	// ColorTypeCReg is an indirect color, indexing the CREG color registers.
+	ColorTypeCReg
+
+	// ColorTypeBlend is an indirect color, blending two other colors.
+	ColorTypeBlend
+)
+
+// Color is an IconVG color, whose RGBA values can depend on context. Some
+// Colors are direct RGBA values. Other Colors are indirect, referring to an
+// index of the custom palette, a color register of the decoder virtual
+// machine, or a blend of two other Colors.
+//
+// See the "Colors" section in the package documentation for details.
+type Color struct {
+	typ  ColorType
+	data color.RGBA
+}
+
+func (c Color) rgba() color.RGBA         { return c.data }
+func (c Color) paletteIndex() uint8      { return c.data.R }
+func (c Color) cReg() uint8              { return c.data.R }
+func (c Color) blend() (t, c0, c1 uint8) { return c.data.R, c.data.G, c.data.B }
+
+// Resolve resolves the Color's RGBA value, given its context: the custom
+// palette and the color registers of the decoder virtual machine.
+func (c Color) Resolve(pal *Palette, cReg *[64]color.RGBA) color.RGBA {
+	switch c.typ {
+	case ColorTypeRGBA:
+		return c.rgba()
+	case ColorTypePaletteIndex:
+		return pal[c.paletteIndex()&0x3f]
+	case ColorTypeCReg:
+		return cReg[c.cReg()&0x3f]
+	}
+	t, c0, c1 := c.blend()
+	s := 255 - t
+	rgba0 := decodeColor1(c0).Resolve(pal, cReg)
+	rgba1 := decodeColor1(c1).Resolve(pal, cReg)
+	return color.RGBA{
+		((s * rgba0.R) + (t * rgba1.R) + 128) / 255,
+		((s * rgba0.G) + (t * rgba1.G) + 128) / 255,
+		((s * rgba0.B) + (t * rgba1.B) + 128) / 255,
+		((s * rgba0.A) + (t * rgba1.A) + 128) / 255,
+	}
+}
+
+// RGBAColor returns a direct Color.
+func RGBAColor(c color.RGBA) Color { return Color{ColorTypeRGBA, c} }
+
+// PaletteIndexColor returns an indirect Color referring to an index of the
+// custom palette.
+func PaletteIndexColor(i uint8) Color { return Color{ColorTypePaletteIndex, color.RGBA{R: i & 0x3f}} }
+
+// CRegColor returns an indirect Color referring to a color register of the
+// decoder virtual machine.
+func CRegColor(i uint8) Color { return Color{ColorTypeCReg, color.RGBA{R: i & 0x3f}} }
+
+// BlendColor returns an indirect Color that blends two other Colors. Those two
+// other Colors must both be encodable as a 1 byte color.
+//
+// To blend a Color that is not encodable as a 1 byte color, first load that
+// Color into a CREG color register, then call CRegColor to produce a Color
+// that is encodable as a 1 byte color.
+//
+// See the "Colors" section in the package documentation for details.
+func BlendColor(t, c0, c1 uint8) Color { return Color{ColorTypeBlend, color.RGBA{R: t, G: c0, B: c1}} }
+
+func decodeColor1(x byte) Color {
+	if x >= 0x80 {
+		if x >= 0xc0 {
+			return CRegColor(x)
+		} else {
+			return PaletteIndexColor(x)
+		}
+	}
+	if x >= 125 {
+		switch x - 125 {
+		case 0:
+			return RGBAColor(color.RGBA{0xc0, 0xc0, 0xc0, 0xc0})
+		case 1:
+			return RGBAColor(color.RGBA{0x80, 0x80, 0x80, 0x80})
+		case 2:
+			return RGBAColor(color.RGBA{0x00, 0x00, 0x00, 0x00})
+		}
+	}
+	blue := dc1Table[x%5]
+	x = x / 5
+	green := dc1Table[x%5]
+	x = x / 5
+	red := dc1Table[x]
+	return RGBAColor(color.RGBA{red, green, blue, 0xff})
+}
+
+var dc1Table = [5]byte{0x00, 0x40, 0x80, 0xc0, 0xff}
+
+func is1(u uint8) bool { return u&0x3f == 0 || u == 0xff }
+
+func encodeColor1(c Color) (x byte, ok bool) {
+	switch c.typ {
+	case ColorTypeRGBA:
+		if c.data.A != 0xff {
+			switch c.data {
+			case color.RGBA{0x00, 0x00, 0x00, 0x00}:
+				return 127, true
+			case color.RGBA{0x80, 0x80, 0x80, 0x80}:
+				return 126, true
+			case color.RGBA{0xc0, 0xc0, 0xc0, 0xc0}:
+				return 125, true
+			}
+		} else if is1(c.data.R) && is1(c.data.G) && is1(c.data.B) && is1(c.data.A) {
+			r := c.data.R / 0x3f
+			g := c.data.G / 0x3f
+			b := c.data.B / 0x3f
+			return 25*r + 5*g + b, true
+		}
+	case ColorTypePaletteIndex:
+		return c.data.R | 0x80, true
+	case ColorTypeCReg:
+		return c.data.R | 0xc0, true
+	}
+	return 0, false
+}
+
+func is2(u uint8) bool { return u%0x11 == 0 }
+
+func encodeColor2(c Color) (x [2]byte, ok bool) {
+	if c.typ == ColorTypeRGBA && is2(c.data.R) && is2(c.data.G) && is2(c.data.B) && is2(c.data.A) {
+		return [2]byte{
+			(c.data.R/0x11)<<4 | (c.data.G / 0x11),
+			(c.data.B/0x11)<<4 | (c.data.A / 0x11),
+		}, true
+	}
+	return [2]byte{}, false
+}
+
+func encodeColor3Direct(c Color) (x [3]byte, ok bool) {
+	if c.typ == ColorTypeRGBA && c.data.A == 0xff {
+		return [3]byte{c.data.R, c.data.G, c.data.B}, true
+	}
+	return [3]byte{}, false
+}
+
+func encodeColor4(c Color) (x [4]byte, ok bool) {
+	if c.typ == ColorTypeRGBA {
+		return [4]byte{c.data.R, c.data.G, c.data.B, c.data.A}, true
+	}
+	return [4]byte{}, false
+}
+
+func encodeColor3Indirect(c Color) (x [3]byte, ok bool) {
+	if c.typ == ColorTypeBlend {
+		return [3]byte{c.data.R, c.data.G, c.data.B}, true
+	}
+	return [3]byte{}, false
+}
diff --git a/shiny/iconvg/decode.go b/shiny/iconvg/decode.go
index 7626c24..f5af1ad 100644
--- a/shiny/iconvg/decode.go
+++ b/shiny/iconvg/decode.go
@@ -7,15 +7,18 @@
 import (
 	"bytes"
 	"errors"
+	"image/color"
 )
 
 var (
 	errInconsistentMetadataChunkLength = errors.New("iconvg: inconsistent metadata chunk length")
+	errInvalidColor                    = errors.New("iconvg: invalid color")
 	errInvalidMagicIdentifier          = errors.New("iconvg: invalid magic identifier")
 	errInvalidMetadataChunkLength      = errors.New("iconvg: invalid metadata chunk length")
 	errInvalidMetadataIdentifier       = errors.New("iconvg: invalid metadata identifier")
 	errInvalidNumber                   = errors.New("iconvg: invalid number")
 	errInvalidNumberOfMetadataChunks   = errors.New("iconvg: invalid number of metadata chunks")
+	errInvalidSuggestedPalette         = errors.New("iconvg: invalid suggested palette")
 	errInvalidViewBox                  = errors.New("iconvg: invalid view box")
 	errUnsupportedDrawingOpcode        = errors.New("iconvg: unsupported drawing opcode")
 	errUnsupportedMetadataIdentifier   = errors.New("iconvg: unsupported metadata identifier")
@@ -35,9 +38,13 @@
 type Destination interface {
 	Reset(m Metadata)
 
-	// TODO: styling mode ops other than StartPath.
+	SetCSel(cSel uint8)
+	SetNSel(nSel uint8)
+	SetCReg(adj uint8, incr bool, c Color)
+	SetNReg(adj uint8, incr bool, f float32)
+	SetLOD(lod0, lod1 float32)
 
-	StartPath(adj int, x, y float32)
+	StartPath(adj uint8, x, y float32)
 	ClosePathEndPath()
 	ClosePathAbsMoveTo(x, y float32)
 	ClosePathRelMoveTo(x, y float32)
@@ -176,7 +183,41 @@
 		}
 
 	case midSuggestedPalette:
-		panic("TODO")
+		if len(src) == 0 {
+			return nil, errInvalidSuggestedPalette
+		}
+		length, format := 1+int(src[0]&0x3f), src[0]>>6
+		decode := buffer.decodeColor4
+		switch format {
+		case 0:
+			decode = buffer.decodeColor1
+		case 1:
+			decode = buffer.decodeColor2
+		case 2:
+			decode = buffer.decodeColor3Direct
+		}
+		if p != nil {
+			p(src[:1], "    %d palette colors, %d bytes per color\n", length, 1+format)
+		}
+		src = src[1:]
+
+		for i := 0; i < length; i++ {
+			c, n := decode(src)
+			if n == 0 {
+				return nil, errInvalidSuggestedPalette
+			}
+			rgba := c.rgba()
+			if c.typ != ColorTypeRGBA || !validAlphaPremulColor(rgba) {
+				rgba = color.RGBA{0x00, 0x00, 0x00, 0xff}
+			}
+			if p != nil {
+				p(src[:n], "    RGBA %02x%02x%02x%02x\n", rgba.R, rgba.G, rgba.B, rgba.A)
+			}
+			src = src[n:]
+			if opts == nil || opts.Palette == nil {
+				m.Palette[i] = rgba
+			}
+		}
 
 	default:
 		return nil, errUnsupportedMetadataIdentifier
@@ -198,18 +239,154 @@
 
 func decodeStyling(dst Destination, p printer, src buffer) (modeFunc, buffer, error) {
 	switch opcode := src[0]; {
+	case opcode < 0x80:
+		if opcode < 0x40 {
+			opcode &= 0x3f
+			if p != nil {
+				p(src[:1], "Set CSEL = %d\n", opcode)
+			}
+			src = src[1:]
+			if dst != nil {
+				dst.SetCSel(opcode)
+			}
+		} else {
+			opcode &= 0x3f
+			if p != nil {
+				p(src[:1], "Set NSEL = %d\n", opcode)
+			}
+			src = src[1:]
+			if dst != nil {
+				dst.SetNSel(opcode)
+			}
+		}
+		return decodeStyling, src, nil
+	case opcode < 0xa8:
+		return decodeSetCReg(dst, p, src, opcode)
 	case opcode < 0xc0:
-		panic("TODO")
+		return decodeSetNReg(dst, p, src, opcode)
 	case opcode < 0xc7:
 		return decodeStartPath(dst, p, src, opcode)
 	case opcode == 0xc7:
-		panic("TODO")
+		return decodeSetLOD(dst, p, src)
 	}
 	return nil, nil, errUnsupportedStylingOpcode
 }
 
+func decodeSetCReg(dst Destination, p printer, src buffer, opcode byte) (modeFunc, buffer, error) {
+	nBytes, directness, adj := 0, "", opcode&0x07
+	var decode func(buffer) (Color, int)
+	incr := adj == 7
+	if incr {
+		adj = 0
+	}
+
+	switch (opcode - 0x80) >> 3 {
+	case 0:
+		nBytes, directness, decode = 1, "", buffer.decodeColor1
+	case 1:
+		nBytes, directness, decode = 2, "", buffer.decodeColor2
+	case 2:
+		nBytes, directness, decode = 3, " (direct)", buffer.decodeColor3Direct
+	case 3:
+		nBytes, directness, decode = 4, "", buffer.decodeColor4
+	case 4:
+		nBytes, directness, decode = 3, " (indirect)", buffer.decodeColor3Indirect
+	}
+	if p != nil {
+		if incr {
+			p(src[:1], "Set CREG[CSEL-0] to a %d byte%s color; CSEL++\n", nBytes, directness)
+		} else {
+			p(src[:1], "Set CREG[CSEL-%d] to a %d byte%s color\n", adj, nBytes, directness)
+		}
+	}
+	src = src[1:]
+
+	c, n := decode(src)
+	if n == 0 {
+		return nil, nil, errInvalidColor
+	}
+
+	if p != nil {
+		printColor(src[:n], p, c, "")
+	}
+	src = src[n:]
+
+	if dst != nil {
+		dst.SetCReg(adj, incr, c)
+	}
+
+	return decodeStyling, src, nil
+}
+
+func printColor(src []byte, p printer, c Color, prefix string) {
+	switch c.typ {
+	case ColorTypeRGBA:
+		if rgba := c.rgba(); validAlphaPremulColor(rgba) {
+			p(src, "    %sRGBA %02x%02x%02x%02x\n", prefix, rgba.R, rgba.G, rgba.B, rgba.A)
+		} else if rgba.A == 0 && rgba.B&0x80 != 0 {
+			p(src, "    %sgradient (NSTOPS=%d, CBASE=%d, NBASE=%d, %s, %s)\n",
+				prefix,
+				rgba.R&0x3f,
+				rgba.G&0x3f,
+				rgba.B&0x3f,
+				gradientShapeNames[(rgba.B>>6)&0x01],
+				gradientSpreadNames[rgba.G>>6],
+			)
+		} else {
+			p(src, "    %snonsensical color\n", prefix)
+		}
+	case ColorTypePaletteIndex:
+		p(src, "    %scustomPalette[%d]\n", prefix, c.paletteIndex())
+	case ColorTypeCReg:
+		p(src, "    %sCREG[%d]\n", prefix, c.cReg())
+	case ColorTypeBlend:
+		t, c0, c1 := c.blend()
+		p(src[:1], "    blend %d:%d c0:c1\n", 0xff-t, t)
+		printColor(src[1:2], p, decodeColor1(c0), "    c0: ")
+		printColor(src[2:3], p, decodeColor1(c1), "    c1: ")
+	}
+}
+
+func decodeSetNReg(dst Destination, p printer, src buffer, opcode byte) (modeFunc, buffer, error) {
+	decode, typ, adj := buffer.decodeZeroToOne, "zero-to-one", opcode&0x07
+	incr := adj == 7
+	if incr {
+		adj = 0
+	}
+
+	switch (opcode - 0xa8) >> 3 {
+	case 0:
+		decode, typ = buffer.decodeReal, "real"
+	case 1:
+		decode, typ = buffer.decodeCoordinate, "coordinate"
+	}
+	if p != nil {
+		if incr {
+			p(src[:1], "Set NREG[NSEL-0] to a %s number; NSEL++\n", typ)
+		} else {
+			p(src[:1], "Set NREG[NSEL-%d] to a %s number\n", adj, typ)
+		}
+	}
+	src = src[1:]
+
+	f, n := decode(src)
+	if n == 0 {
+		return nil, nil, errInvalidNumber
+	}
+	if p != nil {
+		p(src[:n], "    %g\n", f)
+	}
+	src = src[n:]
+
+	if dst != nil {
+		dst.SetNReg(adj, incr, f)
+	}
+
+	return decodeStyling, src, nil
+}
+
 func decodeStartPath(dst Destination, p printer, src buffer, opcode byte) (modeFunc, buffer, error) {
-	adj := int(opcode & 0x07)
+	adj := opcode & 0x07
 	if p != nil {
 		p(src[:1], "Start path, filled with CREG[CSEL-%d]; M (absolute moveTo)\n", adj)
 	}
@@ -231,6 +408,27 @@
 	return decodeDrawing, src, nil
 }
 
+func decodeSetLOD(dst Destination, p printer, src buffer) (modeFunc, buffer, error) {
+	if p != nil {
+		p(src[:1], "Set LOD\n")
+	}
+	src = src[1:]
+
+	lod0, src, err := decodeNumber(p, src, buffer.decodeReal)
+	if err != nil {
+		return nil, nil, err
+	}
+	lod1, src, err := decodeNumber(p, src, buffer.decodeReal)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if dst != nil {
+		dst.SetLOD(lod0, lod1)
+	}
+	return decodeStyling, src, nil
+}
+
 func decodeDrawing(dst Destination, p printer, src buffer) (mf modeFunc, src1 buffer, err error) {
 	var coords [6]float32
 
diff --git a/shiny/iconvg/decode_test.go b/shiny/iconvg/decode_test.go
index 4aa2cd6..5d57480 100644
--- a/shiny/iconvg/decode_test.go
+++ b/shiny/iconvg/decode_test.go
@@ -10,7 +10,9 @@
 	"image"
 	"image/draw"
 	"image/png"
+	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 )
@@ -59,6 +61,44 @@
 	return closeErr
 }
 
+func decodePNG(srcFilename string) (image.Image, error) {
+	f, err := os.Open(srcFilename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return png.Decode(f)
+}
+
+func checkApproxEqual(m0, m1 image.Image) error {
+	diff := func(a, b uint32) uint32 {
+		if a < b {
+			return b - a
+		}
+		return a - b
+	}
+
+	bounds0 := m0.Bounds()
+	bounds1 := m1.Bounds()
+	if bounds0 != bounds1 {
+		return fmt.Errorf("bounds differ: got %v, want %v", bounds0, bounds1)
+	}
+	for y := bounds0.Min.Y; y < bounds0.Max.Y; y++ {
+		for x := bounds0.Min.X; x < bounds0.Max.X; x++ {
+			r0, g0, b0, a0 := m0.At(x, y).RGBA()
+			r1, g1, b1, a1 := m1.At(x, y).RGBA()
+			const D = 0xffff * 5 / 100 // Diff threshold of 5%.
+			if diff(r0, r1) > D || diff(g0, g1) > D || diff(b0, b1) > D || diff(a0, a1) > D {
+				return fmt.Errorf("at (%d, %d):\n"+
+					"got  RGBA %#04x, %#04x, %#04x, %#04x\n"+
+					"want RGBA %#04x, %#04x, %#04x, %#04x",
+					x, y, r0, g0, b0, a0, r1, g1, b1, a1)
+			}
+		}
+	}
+	return nil
+}
+
 func diffLines(t *testing.T, got, want string) {
 	gotLines := strings.Split(got, "\n")
 	wantLines := strings.Split(want, "\n")
@@ -102,7 +142,11 @@
 }
 
 func TestDisassembleActionInfo(t *testing.T) {
-	got, err := disassemble(actionInfoIconVG)
+	ivgData, err := ioutil.ReadFile(filepath.FromSlash("testdata/action-info.ivg"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	got, err := disassemble(ivgData)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -173,7 +217,11 @@
 }
 
 func TestDecodeActionInfo(t *testing.T) {
-	got, err := rasterizeASCIIArt(24, actionInfoIconVG)
+	ivgData, err := ioutil.ReadFile(filepath.FromSlash("testdata/action-info.ivg"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	got, err := rasterizeASCIIArt(24, ivgData)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -210,3 +258,47 @@
 		diffLines(t, got, want)
 	}
 }
+
+func TestRasterizer(t *testing.T) {
+	testCases := []string{
+		"testdata/action-info",
+		"testdata/video-005.primitive",
+	}
+
+	for _, tc := range testCases {
+		ivgData, err := ioutil.ReadFile(filepath.FromSlash(tc) + ".ivg")
+		if err != nil {
+			t.Errorf("%s: ReadFile: %v", tc, err)
+			continue
+		}
+		md, err := DecodeMetadata(ivgData)
+		if err != nil {
+			t.Errorf("%s: DecodeMetadata: %v", tc, err)
+			continue
+		}
+		width, height := 256, 256
+		if dx, dy := md.ViewBox.AspectRatio(); dx < dy {
+			width = int(256 * dx / dy)
+		} else {
+			height = int(256 * dy / dx)
+		}
+
+		got := image.NewRGBA(image.Rect(0, 0, width, height))
+		var z Rasterizer
+		z.SetDstImage(got, got.Bounds(), draw.Src)
+		if err := Decode(&z, ivgData, nil); err != nil {
+			t.Errorf("%s: Decode: %v", tc, err)
+			continue
+		}
+
+		want, err := decodePNG(filepath.FromSlash(tc) + ".png")
+		if err != nil {
+			t.Errorf("%s: decodePNG: %v", tc, err)
+			continue
+		}
+		if err := checkApproxEqual(got, want); err != nil {
+			t.Errorf("%s: %v", tc, err)
+			continue
+		}
+	}
+}
diff --git a/shiny/iconvg/doc.go b/shiny/iconvg/doc.go
index c169d0c..f82e2a2 100644
--- a/shiny/iconvg/doc.go
+++ b/shiny/iconvg/doc.go
@@ -66,7 +66,8 @@
 Register indexing is done modulo 64, so CREG[70] is the same as CREG[6], and
 CREG[-1] is the same as CREG[63].
 
-Each CREG and NREG register is 32 bits wide, and all initial values are 0. The
+Each CREG and NREG register is 32 bits wide. The CREG registers are initialized
+to the custom palette (see below); the NREG registers are initialized to 0. The
 machine state also includes two selector registers, denoted CSEL and NSEL. They
 are effectively 6 bit integers, as they index CREG and NREG, and are also
 initialized to 0.
diff --git a/shiny/iconvg/encode.go b/shiny/iconvg/encode.go
index 9d5fe45..bddec5e 100644
--- a/shiny/iconvg/encode.go
+++ b/shiny/iconvg/encode.go
@@ -8,12 +8,11 @@
 	"errors"
 )
 
-// TODO: encode colors; opcodes for setting CREGs and NREGs.
-
 var (
-	errDrawingOpsUsedInStylingMode = errors.New("iconvg: drawing ops used in styling mode")
-	errInvalidSelectorAdjustment   = errors.New("iconvg: invalid selector adjustment")
-	errStylingOpsUsedInDrawingMode = errors.New("iconvg: styling ops used in drawing mode")
+	errDrawingOpsUsedInStylingMode   = errors.New("iconvg: drawing ops used in styling mode")
+	errInvalidSelectorAdjustment     = errors.New("iconvg: invalid selector adjustment")
+	errInvalidIncrementingAdjustment = errors.New("iconvg: invalid incrementing adjustment")
+	errStylingOpsUsedInDrawingMode   = errors.New("iconvg: styling ops used in drawing mode")
 )
 
 type mode uint8
@@ -35,14 +34,16 @@
 	metadata Metadata
 	err      error
 
+	lod0 float32
+	lod1 float32
+	cSel uint8
+	nSel uint8
+
 	mode     mode
 	drawOp   byte
 	drawArgs []float32
 
-	cSel uint32
-	nSel uint32
-	lod0 float32
-	lod1 float32
+	scratch [12]byte
 }
 
 // Bytes returns the encoded form.
@@ -99,14 +100,14 @@
 	e.mode = modeStyling
 }
 
-func (e *Encoder) CSel() uint32 {
+func (e *Encoder) CSel() uint8 {
 	if e.mode == modeInitial {
 		e.appendDefaultMetadata()
 	}
 	return e.cSel
 }
 
-func (e *Encoder) NSel() uint32 {
+func (e *Encoder) NSel() uint8 {
 	if e.mode == modeInitial {
 		e.appendDefaultMetadata()
 	}
@@ -120,7 +121,7 @@
 	return e.lod0, e.lod1
 }
 
-func (e *Encoder) SetCSel(cSel uint32) {
+func (e *Encoder) SetCSel(cSel uint8) {
 	if e.err != nil {
 		return
 	}
@@ -132,11 +133,11 @@
 			return
 		}
 	}
-	e.cSel = cSel
-	e.buf = append(e.buf, uint8(cSel&0x3f))
+	e.cSel = cSel & 0x3f
+	e.buf = append(e.buf, e.cSel)
 }
 
-func (e *Encoder) SetNSel(nSel uint32) {
+func (e *Encoder) SetNSel(nSel uint8) {
 	if e.err != nil {
 		return
 	}
@@ -148,8 +149,79 @@
 			return
 		}
 	}
-	e.nSel = nSel
-	e.buf = append(e.buf, uint8((nSel&0x3f)|0x40))
+	e.nSel = nSel & 0x3f
+	e.buf = append(e.buf, e.nSel|0x40)
+}
+
+func (e *Encoder) SetCReg(adj uint8, incr bool, c Color) {
+	if e.err != nil {
+		return
+	}
+	if adj < 0 || 6 < adj {
+		e.err = errInvalidSelectorAdjustment
+		return
+	}
+	if incr {
+		if adj != 0 {
+			e.err = errInvalidIncrementingAdjustment
+		}
+		adj = 7
+	}
+
+	if x, ok := encodeColor1(c); ok {
+		e.buf = append(e.buf, adj|0x80, x)
+		return
+	}
+	if x, ok := encodeColor2(c); ok {
+		e.buf = append(e.buf, adj|0x88, x[0], x[1])
+		return
+	}
+	if x, ok := encodeColor3Direct(c); ok {
+		e.buf = append(e.buf, adj|0x90, x[0], x[1], x[2])
+		return
+	}
+	if x, ok := encodeColor4(c); ok {
+		e.buf = append(e.buf, adj|0x98, x[0], x[1], x[2], x[3])
+		return
+	}
+	if x, ok := encodeColor3Indirect(c); ok {
+		e.buf = append(e.buf, adj|0xa0, x[0], x[1], x[2])
+		return
+	}
+	panic("unreachable")
+}
+
+func (e *Encoder) SetNReg(adj uint8, incr bool, f float32) {
+	if e.err != nil {
+		return
+	}
+	if adj < 0 || 6 < adj {
+		e.err = errInvalidSelectorAdjustment
+		return
+	}
+	if incr {
+		if adj != 0 {
+			e.err = errInvalidIncrementingAdjustment
+		}
+		adj = 7
+	}
+
+	// Try three different encodings and pick the shortest.
+	b := buffer(e.scratch[0:0])
+	opcode, iBest, nBest := uint8(0xa8), 0, b.encodeReal(f)
+
+	b = buffer(e.scratch[4:4])
+	if n := b.encodeCoordinate(f); n < nBest {
+		opcode, iBest, nBest = 0xb0, 4, n
+	}
+
+	b = buffer(e.scratch[8:8])
+	if n := b.encodeZeroToOne(f); n < nBest {
+		opcode, iBest, nBest = 0xb8, 8, n
+	}
+
+	e.buf = append(e.buf, adj|opcode)
+	e.buf = append(e.buf, e.scratch[iBest:iBest+nBest]...)
 }
 
 func (e *Encoder) SetLOD(lod0, lod1 float32) {
@@ -171,7 +243,7 @@
 	e.buf.encodeReal(lod1)
 }
 
-func (e *Encoder) StartPath(adj int, x, y float32) {
+func (e *Encoder) StartPath(adj uint8, x, y float32) {
 	if e.err != nil {
 		return
 	}
diff --git a/shiny/iconvg/encode_test.go b/shiny/iconvg/encode_test.go
index 6c42c5e..cb8ed3d 100644
--- a/shiny/iconvg/encode_test.go
+++ b/shiny/iconvg/encode_test.go
@@ -6,6 +6,9 @@
 
 import (
 	"bytes"
+	"image/color"
+	"io/ioutil"
+	"path/filepath"
 	"testing"
 
 	"golang.org/x/image/math/f32"
@@ -26,18 +29,6 @@
 	}
 }
 
-// actionInfoIconVG is the IconVG encoding of the "action/info" icon from the
-// Material Design icon set.
-//
-// See doc.go for an annotated version.
-var actionInfoIconVG = []byte{
-	0x89, 0x49, 0x56, 0x47, 0x02, 0x0a, 0x00, 0x50, 0x50, 0xb0, 0xb0, 0xc0, 0x80, 0x58, 0xa0, 0xcf,
-	0xcc, 0x30, 0xc1, 0x58, 0x58, 0xcf, 0xcc, 0x30, 0xc1, 0x58, 0x80, 0x91, 0x37, 0x33, 0x0f, 0x41,
-	0xa8, 0xa8, 0xa8, 0xa8, 0x37, 0x33, 0x0f, 0xc1, 0xa8, 0x58, 0x80, 0xcf, 0xcc, 0x30, 0x41, 0x58,
-	0x80, 0x58, 0xe3, 0x84, 0xbc, 0xe7, 0x78, 0xe8, 0x7c, 0xe7, 0x88, 0xe9, 0x98, 0xe3, 0x80, 0x60,
-	0xe7, 0x78, 0xe9, 0x78, 0xe7, 0x88, 0xe9, 0x88, 0xe1,
-}
-
 func TestEncodeActionInfo(t *testing.T) {
 	var e Encoder
 	e.Reset(Metadata{
@@ -69,7 +60,99 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	want := actionInfoIconVG
+	want, err := ioutil.ReadFile(filepath.FromSlash("testdata/action-info.ivg"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(got, want) {
+		t.Errorf("\ngot  %d bytes:\n% x\nwant %d bytes:\n% x", len(got), got, len(want), want)
+	}
+}
+
+var video005PrimitiveSVGData = []struct {
+	r, g, b uint32
+	x0, y0  int
+	x1, y1  int
+	x2, y2  int
+}{
+	{0x17, 0x06, 0x05, 162, 207, 271, 186, 195, -16},
+	{0xe9, 0xf5, 0xf8, -16, 179, 140, -11, 16, -8},
+	{0x00, 0x04, 0x27, 97, 96, 221, 21, 214, 111},
+	{0x89, 0xd9, 0xff, 262, -6, 271, 104, 164, -16},
+	{0x94, 0xbd, 0xc5, 204, 104, 164, 207, 59, 104},
+	{0xd4, 0x81, 0x3d, -16, 36, 123, 195, -16, 194},
+	{0x00, 0x00, 0x00, 164, 19, 95, 77, 138, 13},
+	{0x39, 0x11, 0x19, 50, 143, 115, 185, -4, 165},
+	{0x00, 0x3d, 0x81, 86, 109, 53, 76, 90, 24},
+	{0xfc, 0xc6, 0x9c, 31, 161, 80, 105, -16, 28},
+	{0x9e, 0xdd, 0xff, 201, -7, 31, -16, 2, 60},
+	{0x01, 0x20, 0x39, 132, 85, 240, -5, 173, 130},
+	{0xfd, 0xbc, 0x8f, 193, 127, 231, 94, 250, 124},
+	{0x43, 0x06, 0x00, 251, 207, 237, 83, 271, 97},
+	{0x80, 0xbf, 0xee, 117, 134, 88, 177, 90, 28},
+	{0x00, 0x00, 0x00, 127, 38, 172, 68, 223, 55},
+	{0x19, 0x0e, 0x16, 201, 204, 161, 101, 271, 192},
+	{0xf6, 0xaa, 0x71, 201, 164, 226, 141, 261, 152},
+	{0xe0, 0x36, 0x00, -16, -2, 29, -16, -6, 58},
+	{0xff, 0xe4, 0xba, 146, 45, 118, 75, 148, 76},
+	{0x00, 0x00, 0x12, 118, 44, 107, 109, 100, 51},
+	{0xbd, 0xd5, 0xe4, 271, 41, 253, -16, 211, 89},
+	{0x52, 0x00, 0x00, 87, 127, 83, 150, 55, 111},
+	{0x00, 0xb3, 0xa1, 124, 185, 135, 207, 194, 176},
+	{0x22, 0x00, 0x00, 59, 151, 33, 124, 52, 169},
+	{0xbe, 0xcb, 0xcb, 149, 42, 183, -16, 178, 47},
+	{0xff, 0xd4, 0xb1, 211, 119, 184, 100, 182, 124},
+	{0xff, 0xe1, 0x39, 73, 207, 140, 180, -13, 187},
+	{0xa7, 0xb0, 0xad, 122, 181, 200, 182, 93, 82},
+	{0x00, 0x00, 0x00, 271, 168, 170, 185, 221, 207},
+}
+
+func TestEncodeVideo005Primitive(t *testing.T) {
+	// The division by 4 is because the SVG width is 256 units and the IconVG
+	// width is 64 (from -32 to +32).
+	//
+	// The subtraction by 0.5 is because the SVG file contains the line:
+	// <g transform="translate(0.5 0.5)">
+	scaleX := func(i int) float32 { return float32(i)/4 - (32 - 0.5/4) }
+	scaleY := func(i int) float32 { return float32(i)/4 - (24 - 0.5/4) }
+
+	var e Encoder
+	e.Reset(Metadata{
+		ViewBox: Rectangle{
+			Min: f32.Vec2{-32, -24},
+			Max: f32.Vec2{+32, +24},
+		},
+		Palette: DefaultPalette,
+	})
+
+	e.SetCReg(0, false, RGBAColor(color.RGBA{0x7c, 0x7e, 0x7c, 0xff}))
+	e.StartPath(0, -32, -24)
+	e.AbsHLineTo(+32)
+	e.AbsVLineTo(+24)
+	e.AbsHLineTo(-32)
+	e.ClosePathEndPath()
+
+	for _, v := range video005PrimitiveSVGData {
+		e.SetCReg(0, false, RGBAColor(color.RGBA{
+			uint8(v.r * 128 / 255),
+			uint8(v.g * 128 / 255),
+			uint8(v.b * 128 / 255),
+			128,
+		}))
+		e.StartPath(0, scaleX(v.x0), scaleY(v.y0))
+		e.AbsLineTo(scaleX(v.x1), scaleY(v.y1))
+		e.AbsLineTo(scaleX(v.x2), scaleY(v.y2))
+		e.ClosePathEndPath()
+	}
+
+	got, err := e.Bytes()
+	if err != nil {
+		t.Fatal(err)
+	}
+	want, err := ioutil.ReadFile(filepath.FromSlash("testdata/video-005.primitive.ivg"))
+	if err != nil {
+		t.Fatal(err)
+	}
 	if !bytes.Equal(got, want) {
 		t.Errorf("\ngot  %d bytes:\n% x\nwant %d bytes:\n% x", len(got), got, len(want), want)
 	}
diff --git a/shiny/iconvg/iconvg.go b/shiny/iconvg/iconvg.go
index 75e78cc..5d5fff5 100644
--- a/shiny/iconvg/iconvg.go
+++ b/shiny/iconvg/iconvg.go
@@ -26,6 +26,18 @@
 	midSuggestedPalette = 1
 )
 
+var gradientShapeNames = [2]string{
+	"linear",
+	"radial",
+}
+
+var gradientSpreadNames = [4]string{
+	"none",
+	"pad",
+	"reflect",
+	"repeat",
+}
+
 // Rectangle is defined by its minimum and maximum coordinates.
 type Rectangle struct {
 	Min, Max f32.Vec2
diff --git a/shiny/iconvg/rasterizer.go b/shiny/iconvg/rasterizer.go
index e774b6a..4a8c85f 100644
--- a/shiny/iconvg/rasterizer.go
+++ b/shiny/iconvg/rasterizer.go
@@ -42,17 +42,21 @@
 
 	metadata Metadata
 
+	lod0 float32
+	lod1 float32
+	cSel uint8
+	nSel uint8
+
 	firstStartPath  bool
 	prevSmoothType  uint8
 	prevSmoothPoint f32.Vec2
 
-	cSel uint32
-	nSel uint32
-	lod0 float32
-	lod1 float32
+	fill      image.Image
+	flatColor color.RGBA
+	flatImage image.Uniform
 
-	creg [64]color.RGBA
-	nreg [64]float32
+	cReg [64]color.RGBA
+	nReg [64]float32
 }
 
 // SetDstImage sets the Rasterizer to draw onto a destination image, given by
@@ -74,15 +78,15 @@
 // Reset resets the Rasterizer for the given Metadata.
 func (z *Rasterizer) Reset(m Metadata) {
 	z.metadata = m
+	z.lod0 = 0
+	z.lod1 = positiveInfinity
+	z.cSel = 0
+	z.nSel = 0
 	z.firstStartPath = true
 	z.prevSmoothType = smoothTypeNone
 	z.prevSmoothPoint = f32.Vec2{}
-	z.cSel = 0
-	z.nSel = 0
-	z.lod0 = 0
-	z.lod1 = positiveInfinity
-	z.creg = [64]color.RGBA{}
-	z.nreg = [64]float32{}
+	z.cReg = m.Palette
+	z.nReg = [64]float32{}
 	z.recalcTransform()
 }
 
@@ -93,6 +97,28 @@
 	z.biasY = -z.metadata.ViewBox.Min[1]
 }
 
+func (z *Rasterizer) SetCSel(cSel uint8) { z.cSel = cSel & 0x3f }
+func (z *Rasterizer) SetNSel(nSel uint8) { z.nSel = nSel & 0x3f }
+
+func (z *Rasterizer) SetCReg(adj uint8, incr bool, c Color) {
+	z.cReg[(z.cSel-adj)&0x3f] = c.Resolve(&z.metadata.Palette, &z.cReg)
+	if incr {
+		z.cSel++
+	}
+}
+
+func (z *Rasterizer) SetNReg(adj uint8, incr bool, f float32) {
+	z.nReg[(z.nSel-adj)&0x3f] = f
+	if incr {
+		z.nSel++
+	}
+}
+
+func (z *Rasterizer) SetLOD(lod0, lod1 float32) {
+	z.lod0, z.lod1 = lod0, lod1
+	// TODO: check the LODs against z.r.Dy().
+}
+
 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 }
@@ -126,8 +152,11 @@
 	}
 }
 
-func (z *Rasterizer) StartPath(adj int, x, y float32) {
-	// TODO: note adj, use it in ClosePathEndPath.
+func (z *Rasterizer) StartPath(adj uint8, x, y float32) {
+	// TODO: gradient fills, not just flat colors.
+	z.flatColor = z.cReg[(z.cSel-adj)&0x3f]
+	z.flatImage.C = &z.flatColor
+	z.fill = &z.flatImage
 
 	z.z.Reset(z.r.Dx(), z.r.Dy())
 	if z.firstStartPath {
@@ -143,8 +172,7 @@
 	if z.dst == nil {
 		return
 	}
-	// TODO: don't assume image.Opaque.
-	z.z.Draw(z.dst, z.r, image.Opaque, image.Point{})
+	z.z.Draw(z.dst, z.r, z.fill, image.Point{})
 }
 
 func (z *Rasterizer) ClosePathAbsMoveTo(x, y float32) {
diff --git a/shiny/iconvg/testdata/README b/shiny/iconvg/testdata/README
new file mode 100644
index 0000000..b290983
--- /dev/null
+++ b/shiny/iconvg/testdata/README
@@ -0,0 +1,18 @@
+action-info.svg comes from the Material Design icon set. See
+action/svg/production/ic_info_48px.svg in the
+github.com/google/material-design-icons repository.
+
+action-info.ivg is an IconVG version of action-info.svg. See ../doc.go for an
+annotated breakdown.
+
+action-info.png is a rendering of action-info.ivg.
+
+video-005.jpeg comes from an old version of the Go repository. See
+https://codereview.appspot.com/5758047/
+
+video-005.primitive.svg was based on running github.com/fogleman/primitive on a
+256x192 scaled version of video-005.jpeg.
+
+video-005.primitive.ivg is an IconVG version of video-005.primitive.svg.
+
+video-005.primitive.png is a rendering of video-005.primitive.ivg.
diff --git a/shiny/iconvg/testdata/action-info.ivg b/shiny/iconvg/testdata/action-info.ivg
new file mode 100644
index 0000000..9532529
--- /dev/null
+++ b/shiny/iconvg/testdata/action-info.ivg
Binary files differ
diff --git a/shiny/iconvg/testdata/action-info.png b/shiny/iconvg/testdata/action-info.png
new file mode 100644
index 0000000..49f8038
--- /dev/null
+++ b/shiny/iconvg/testdata/action-info.png
Binary files differ
diff --git a/shiny/iconvg/testdata/action-info.svg b/shiny/iconvg/testdata/action-info.svg
new file mode 100644
index 0000000..22f40f9
--- /dev/null
+++ b/shiny/iconvg/testdata/action-info.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm2 30h-4V22h4v12zm0-16h-4v-4h4v4z"/></svg>
\ No newline at end of file
diff --git a/shiny/iconvg/testdata/video-005.jpeg b/shiny/iconvg/testdata/video-005.jpeg
new file mode 100644
index 0000000..3237158
--- /dev/null
+++ b/shiny/iconvg/testdata/video-005.jpeg
Binary files differ
diff --git a/shiny/iconvg/testdata/video-005.primitive.ivg b/shiny/iconvg/testdata/video-005.primitive.ivg
new file mode 100644
index 0000000..098c928
--- /dev/null
+++ b/shiny/iconvg/testdata/video-005.primitive.ivg
Binary files differ
diff --git a/shiny/iconvg/testdata/video-005.primitive.png b/shiny/iconvg/testdata/video-005.primitive.png
new file mode 100644
index 0000000..e25d7a7
--- /dev/null
+++ b/shiny/iconvg/testdata/video-005.primitive.png
Binary files differ
diff --git a/shiny/iconvg/testdata/video-005.primitive.svg b/shiny/iconvg/testdata/video-005.primitive.svg
new file mode 100644
index 0000000..53be42c
--- /dev/null
+++ b/shiny/iconvg/testdata/video-005.primitive.svg
@@ -0,0 +1,35 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="256" height="192">
+<rect x="0" y="0" width="256" height="192" fill="#7c7e7c" />
+<g transform="translate(0.5 0.5)">
+<polygon fill="#170605" fill-opacity="0.501961" points="162,207 271,186 195,-16" />
+<polygon fill="#e9f5f8" fill-opacity="0.501961" points="-16,179 140,-11 16,-8" />
+<polygon fill="#000427" fill-opacity="0.501961" points="97,96 221,21 214,111" />
+<polygon fill="#89d9ff" fill-opacity="0.501961" points="262,-6 271,104 164,-16" />
+<polygon fill="#94bdc5" fill-opacity="0.501961" points="204,104 164,207 59,104" />
+<polygon fill="#d4813d" fill-opacity="0.501961" points="-16,36 123,195 -16,194" />
+<polygon fill="#000000" fill-opacity="0.501961" points="164,19 95,77 138,13" />
+<polygon fill="#391119" fill-opacity="0.501961" points="50,143 115,185 -4,165" />
+<polygon fill="#003d81" fill-opacity="0.501961" points="86,109 53,76 90,24" />
+<polygon fill="#fcc69c" fill-opacity="0.501961" points="31,161 80,105 -16,28" />
+<polygon fill="#9eddff" fill-opacity="0.501961" points="201,-7 31,-16 2,60" />
+<polygon fill="#012039" fill-opacity="0.501961" points="132,85 240,-5 173,130" />
+<polygon fill="#fdbc8f" fill-opacity="0.501961" points="193,127 231,94 250,124" />
+<polygon fill="#430600" fill-opacity="0.501961" points="251,207 237,83 271,97" />
+<polygon fill="#80bfee" fill-opacity="0.501961" points="117,134 88,177 90,28" />
+<polygon fill="#000000" fill-opacity="0.501961" points="127,38 172,68 223,55" />
+<polygon fill="#190e16" fill-opacity="0.501961" points="201,204 161,101 271,192" />
+<polygon fill="#f6aa71" fill-opacity="0.501961" points="201,164 226,141 261,152" />
+<polygon fill="#e03600" fill-opacity="0.501961" points="-16,-2 29,-16 -6,58" />
+<polygon fill="#ffe4ba" fill-opacity="0.501961" points="146,45 118,75 148,76" />
+<polygon fill="#000012" fill-opacity="0.501961" points="118,44 107,109 100,51" />
+<polygon fill="#bdd5e4" fill-opacity="0.501961" points="271,41 253,-16 211,89" />
+<polygon fill="#520000" fill-opacity="0.501961" points="87,127 83,150 55,111" />
+<polygon fill="#00b3a1" fill-opacity="0.501961" points="124,185 135,207 194,176" />
+<polygon fill="#220000" fill-opacity="0.501961" points="59,151 33,124 52,169" />
+<polygon fill="#becbcb" fill-opacity="0.501961" points="149,42 183,-16 178,47" />
+<polygon fill="#ffd4b1" fill-opacity="0.501961" points="211,119 184,100 182,124" />
+<polygon fill="#ffe139" fill-opacity="0.501961" points="73,207 140,180 -13,187" />
+<polygon fill="#a7b0ad" fill-opacity="0.501961" points="122,181 200,182 93,82" />
+<polygon fill="#000000" fill-opacity="0.501961" points="271,168 170,185 221,207" />
+</g>
+</svg>