shiny/iconvg: implement a decoder.

Change-Id: I488531c8ae5b929e178de481f30acf0f88833b83
Reviewed-on: https://go-review.googlesource.com/29698
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/iconvg/decode.go b/shiny/iconvg/decode.go
new file mode 100644
index 0000000..7626c24
--- /dev/null
+++ b/shiny/iconvg/decode.go
@@ -0,0 +1,479 @@
+// 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 (
+	"bytes"
+	"errors"
+)
+
+var (
+	errInconsistentMetadataChunkLength = errors.New("iconvg: inconsistent metadata chunk length")
+	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")
+	errInvalidViewBox                  = errors.New("iconvg: invalid view box")
+	errUnsupportedDrawingOpcode        = errors.New("iconvg: unsupported drawing opcode")
+	errUnsupportedMetadataIdentifier   = errors.New("iconvg: unsupported metadata identifier")
+	errUnsupportedStylingOpcode        = errors.New("iconvg: unsupported styling opcode")
+)
+
+var midDescriptions = [...]string{
+	midViewBox:          "viewBox",
+	midSuggestedPalette: "suggested palette",
+}
+
+// Destination handles the actions decoded from an IconVG graphic's opcodes.
+//
+// When passed to Decode, the first method called (if any) will be Reset. No
+// methods will be called at all if an error is encountered in the encoded form
+// before the metadata is fully decoded.
+type Destination interface {
+	Reset(m Metadata)
+
+	// TODO: styling mode ops other than StartPath.
+
+	StartPath(adj int, x, y float32)
+	ClosePathEndPath()
+	ClosePathAbsMoveTo(x, y float32)
+	ClosePathRelMoveTo(x, y float32)
+
+	AbsHLineTo(x float32)
+	RelHLineTo(x float32)
+	AbsVLineTo(y float32)
+	RelVLineTo(y float32)
+	AbsLineTo(x, y float32)
+	RelLineTo(x, y float32)
+	AbsSmoothQuadTo(x, y float32)
+	RelSmoothQuadTo(x, y float32)
+	AbsQuadTo(x1, y1, x, y float32)
+	RelQuadTo(x1, y1, x, y float32)
+	AbsSmoothCubeTo(x2, y2, x, y float32)
+	RelSmoothCubeTo(x2, y2, x, y float32)
+	AbsCubeTo(x1, y1, x2, y2, x, y float32)
+	RelCubeTo(x1, y1, x2, y2, x, y float32)
+	AbsArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32)
+	RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32)
+}
+
+type printer func(b []byte, format string, args ...interface{})
+
+// DecodeOptions are the optional parameters to the Decode function.
+type DecodeOptions struct {
+	// Palette is an optional 64 color palette. If one isn't provided, the
+	// IconVG graphic's suggested palette will be used.
+	Palette *Palette
+}
+
+// DecodeMetadata decodes only the metadata in an IconVG graphic.
+func DecodeMetadata(src []byte) (m Metadata, err error) {
+	m.ViewBox = DefaultViewBox
+	m.Palette = DefaultPalette
+	if err = decode(nil, nil, &m, true, src, nil); err != nil {
+		return Metadata{}, err
+	}
+	return m, nil
+}
+
+// Decode decodes an IconVG graphic.
+func Decode(dst Destination, src []byte, opts *DecodeOptions) error {
+	m := Metadata{
+		ViewBox: DefaultViewBox,
+		Palette: DefaultPalette,
+	}
+	if opts != nil && opts.Palette != nil {
+		m.Palette = *opts.Palette
+	}
+	return decode(dst, nil, &m, false, src, opts)
+}
+
+func decode(dst Destination, p printer, m *Metadata, metadataOnly bool, src buffer, opts *DecodeOptions) (err error) {
+	if !bytes.HasPrefix(src, magicBytes) {
+		return errInvalidMagicIdentifier
+	}
+	if p != nil {
+		p(src[:len(magic)], "Magic identifier\n")
+	}
+	src = src[len(magic):]
+
+	nMetadataChunks, n := src.decodeNatural()
+	if n == 0 {
+		return errInvalidNumberOfMetadataChunks
+	}
+	if p != nil {
+		p(src[:n], "Number of metadata chunks: %d\n", nMetadataChunks)
+	}
+	src = src[n:]
+
+	for ; nMetadataChunks > 0; nMetadataChunks-- {
+		src, err = decodeMetadataChunk(p, m, src, opts)
+		if err != nil {
+			return err
+		}
+	}
+	if metadataOnly {
+		return nil
+	}
+	if dst != nil {
+		dst.Reset(*m)
+	}
+
+	mf := modeFunc(decodeStyling)
+	for len(src) > 0 {
+		mf, src, err = mf(dst, p, src)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func decodeMetadataChunk(p printer, m *Metadata, src buffer, opts *DecodeOptions) (src1 buffer, err error) {
+	length, n := src.decodeNatural()
+	if n == 0 {
+		return nil, errInvalidMetadataChunkLength
+	}
+	if p != nil {
+		p(src[:n], "Metadata chunk length: %d\n", length)
+	}
+	src = src[n:]
+	lenSrcWant := int64(len(src)) - int64(length)
+
+	mid, n := src.decodeNatural()
+	if n == 0 {
+		return nil, errInvalidMetadataIdentifier
+	}
+	if mid >= uint32(len(midDescriptions)) {
+		return nil, errUnsupportedMetadataIdentifier
+	}
+	if p != nil {
+		p(src[:n], "Metadata Identifier: %d (%s)\n", mid, midDescriptions[mid])
+	}
+	src = src[n:]
+
+	switch mid {
+	case midViewBox:
+		if m.ViewBox.Min[0], src, err = decodeNumber(p, src, buffer.decodeCoordinate); err != nil {
+			return nil, errInvalidViewBox
+		}
+		if m.ViewBox.Min[1], src, err = decodeNumber(p, src, buffer.decodeCoordinate); err != nil {
+			return nil, errInvalidViewBox
+		}
+		if m.ViewBox.Max[0], src, err = decodeNumber(p, src, buffer.decodeCoordinate); err != nil {
+			return nil, errInvalidViewBox
+		}
+		if m.ViewBox.Max[1], src, err = decodeNumber(p, src, buffer.decodeCoordinate); err != nil {
+			return nil, errInvalidViewBox
+		}
+		if m.ViewBox.Min[0] > m.ViewBox.Max[0] || m.ViewBox.Min[1] > m.ViewBox.Max[1] ||
+			isNaNOrInfinity(m.ViewBox.Min[0]) || isNaNOrInfinity(m.ViewBox.Min[1]) ||
+			isNaNOrInfinity(m.ViewBox.Max[0]) || isNaNOrInfinity(m.ViewBox.Max[1]) {
+			return nil, errInvalidViewBox
+		}
+
+	case midSuggestedPalette:
+		panic("TODO")
+
+	default:
+		return nil, errUnsupportedMetadataIdentifier
+	}
+
+	if int64(len(src)) != lenSrcWant {
+		return nil, errInconsistentMetadataChunkLength
+	}
+	return src, nil
+}
+
+// modeFunc is the decoding mode: whether we are decoding styling or drawing
+// opcodes.
+//
+// It is a function type. The decoding loop calls this function to decode and
+// execute the next opcode from the src buffer, returning the subsequent mode
+// and the remaining source bytes.
+type modeFunc func(dst Destination, p printer, src buffer) (modeFunc, buffer, error)
+
+func decodeStyling(dst Destination, p printer, src buffer) (modeFunc, buffer, error) {
+	switch opcode := src[0]; {
+	case opcode < 0xc0:
+		panic("TODO")
+	case opcode < 0xc7:
+		return decodeStartPath(dst, p, src, opcode)
+	case opcode == 0xc7:
+		panic("TODO")
+	}
+	return nil, nil, errUnsupportedStylingOpcode
+}
+
+func decodeStartPath(dst Destination, p printer, src buffer, opcode byte) (modeFunc, buffer, error) {
+	adj := int(opcode & 0x07)
+	if p != nil {
+		p(src[:1], "Start path, filled with CREG[CSEL-%d]; M (absolute moveTo)\n", adj)
+	}
+	src = src[1:]
+
+	x, src, err := decodeNumber(p, src, buffer.decodeCoordinate)
+	if err != nil {
+		return nil, nil, err
+	}
+	y, src, err := decodeNumber(p, src, buffer.decodeCoordinate)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if dst != nil {
+		dst.StartPath(adj, x, y)
+	}
+
+	return decodeDrawing, src, nil
+}
+
+func decodeDrawing(dst Destination, p printer, src buffer) (mf modeFunc, src1 buffer, err error) {
+	var coords [6]float32
+
+	switch opcode := src[0]; {
+	case opcode < 0xe0:
+		op, nCoords, nReps := "", 0, 1+int(opcode&0x0f)
+		switch opcode >> 4 {
+		case 0x00, 0x01:
+			op = "L (absolute lineTo)"
+			nCoords = 2
+			nReps = 1 + int(opcode&0x1f)
+		case 0x02, 0x03:
+			op = "l (relative lineTo)"
+			nCoords = 2
+			nReps = 1 + int(opcode&0x1f)
+		case 0x04:
+			op = "T (absolute smooth quadTo)"
+			nCoords = 2
+		case 0x05:
+			op = "t (relative smooth quadTo)"
+			nCoords = 2
+		case 0x06:
+			op = "Q (absolute quadTo)"
+			nCoords = 4
+		case 0x07:
+			op = "q (relative quadTo)"
+			nCoords = 4
+		case 0x08:
+			op = "S (absolute smooth cubeTo)"
+			nCoords = 4
+		case 0x09:
+			op = "s (relative smooth cubeTo)"
+			nCoords = 4
+		case 0x0a:
+			op = "C (absolute cubeTo)"
+			nCoords = 6
+		case 0x0b:
+			op = "c (relative cubeTo)"
+			nCoords = 6
+		case 0x0c:
+			op = "A (absolute arcTo)"
+			nCoords = 0
+		case 0x0d:
+			op = "a (relative arcTo)"
+			nCoords = 0
+		}
+
+		if p != nil {
+			p(src[:1], "%s, %d reps\n", op, nReps)
+		}
+		src = src[1:]
+
+		for i := 0; i < nReps; i++ {
+			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
+			}
+
+			if dst == nil {
+				continue
+			}
+			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' {
+				dst.AbsArcTo(coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5])
+			} else {
+				dst.RelArcTo(coords[0], coords[1], coords[2], largeArc, sweep, coords[4], coords[5])
+			}
+		}
+
+	case opcode == 0xe1:
+		if p != nil {
+			p(src[:1], "z (closePath); end path\n")
+		}
+		src = src[1:]
+		if dst != nil {
+			dst.ClosePathEndPath()
+		}
+		return decodeStyling, src, nil
+
+	case opcode == 0xe2:
+		if p != nil {
+			p(src[:1], "z (closePath); M (absolute moveTo)\n")
+		}
+		src = src[1:]
+		src, err = decodeCoordinates(coords[:2], p, src)
+		if err != nil {
+			return nil, nil, err
+		}
+		if dst != nil {
+			dst.ClosePathAbsMoveTo(coords[0], coords[1])
+		}
+
+	case opcode == 0xe3:
+		if p != nil {
+			p(src[:1], "z (closePath); m (relative moveTo)\n")
+		}
+		src = src[1:]
+		src, err = decodeCoordinates(coords[:2], p, src)
+		if err != nil {
+			return nil, nil, err
+		}
+		if dst != nil {
+			dst.ClosePathRelMoveTo(coords[0], coords[1])
+		}
+
+	case opcode == 0xe6:
+		if p != nil {
+			p(src[:1], "H (absolute horizontal lineTo)\n")
+		}
+		src = src[1:]
+		src, err = decodeCoordinates(coords[:1], p, src)
+		if err != nil {
+			return nil, nil, err
+		}
+		if dst != nil {
+			dst.AbsHLineTo(coords[0])
+		}
+
+	case opcode == 0xe7:
+		if p != nil {
+			p(src[:1], "h (relative horizontal lineTo)\n")
+		}
+		src = src[1:]
+		src, err = decodeCoordinates(coords[:1], p, src)
+		if err != nil {
+			return nil, nil, err
+		}
+		if dst != nil {
+			dst.RelHLineTo(coords[0])
+		}
+
+	case opcode == 0xe8:
+		if p != nil {
+			p(src[:1], "V (absolute vertical lineTo)\n")
+		}
+		src = src[1:]
+		src, err = decodeCoordinates(coords[:1], p, src)
+		if err != nil {
+			return nil, nil, err
+		}
+		if dst != nil {
+			dst.AbsVLineTo(coords[0])
+		}
+
+	case opcode == 0xe9:
+		if p != nil {
+			p(src[:1], "v (relative vertical lineTo)\n")
+		}
+		src = src[1:]
+		src, err = decodeCoordinates(coords[:1], p, src)
+		if err != nil {
+			return nil, nil, err
+		}
+		if dst != nil {
+			dst.RelVLineTo(coords[0])
+		}
+
+	default:
+		return nil, nil, errUnsupportedDrawingOpcode
+	}
+	return decodeDrawing, src, nil
+}
+
+type decodeNumberFunc func(buffer) (float32, int)
+
+func decodeNumber(p printer, src buffer, dnf decodeNumberFunc) (float32, buffer, error) {
+	x, n := dnf(src)
+	if n == 0 {
+		return 0, nil, errInvalidNumber
+	}
+	if p != nil {
+		p(src[:n], "    %+g\n", x)
+	}
+	return x, src[n:], nil
+}
+
+func decodeCoordinates(coords []float32, p printer, src buffer) (src1 buffer, err error) {
+	for i := range coords {
+		coords[i], src, err = decodeNumber(p, src, buffer.decodeCoordinate)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return src, nil
+}
+
+func decodeArcToFlags(p printer, src buffer) (bool, bool, buffer, error) {
+	x, n := src.decodeNatural()
+	if n == 0 {
+		return false, false, nil, errInvalidNumber
+	}
+	if p != nil {
+		p(src[:n], "    %#x (largeArc=%d, sweep=%d)\n", x, (x>>0)&0x01, (x>>1)&0x01)
+	}
+	return (x>>0)&0x01 != 0, (x>>1)&0x01 != 0, src[n:], nil
+}
diff --git a/shiny/iconvg/decode_test.go b/shiny/iconvg/decode_test.go
new file mode 100644
index 0000000..4aa2cd6
--- /dev/null
+++ b/shiny/iconvg/decode_test.go
@@ -0,0 +1,212 @@
+// 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 (
+	"bytes"
+	"fmt"
+	"image"
+	"image/draw"
+	"image/png"
+	"os"
+	"strings"
+	"testing"
+)
+
+// disassemble returns a disassembly of an encoded IconVG graphic. Users of
+// this package aren't expected to want to do this, so it lives in a _test.go
+// file, but it can be useful for debugging.
+func disassemble(src []byte) (string, error) {
+	w := new(bytes.Buffer)
+	p := func(b []byte, format string, args ...interface{}) {
+		const hex = "0123456789abcdef"
+		var buf [14]byte
+		for i := range buf {
+			buf[i] = ' '
+		}
+		for i, x := range b {
+			buf[3*i+0] = hex[x>>4]
+			buf[3*i+1] = hex[x&0x0f]
+		}
+		w.Write(buf[:])
+		fmt.Fprintf(w, format, args...)
+	}
+	m := Metadata{}
+	if err := decode(nil, p, &m, false, buffer(src), nil); err != nil {
+		return "", err
+	}
+	return w.String(), nil
+}
+
+var (
+	_ Destination = (*Encoder)(nil)
+	_ Destination = (*Rasterizer)(nil)
+)
+
+// encodePNG is useful for manually debugging the tests.
+func encodePNG(dstFilename string, src image.Image) error {
+	f, err := os.Create(dstFilename)
+	if err != nil {
+		return err
+	}
+	encErr := png.Encode(f, src)
+	closeErr := f.Close()
+	if encErr != nil {
+		return encErr
+	}
+	return closeErr
+}
+
+func diffLines(t *testing.T, got, want string) {
+	gotLines := strings.Split(got, "\n")
+	wantLines := strings.Split(want, "\n")
+	for i := 1; ; i++ {
+		if len(gotLines) == 0 {
+			t.Errorf("line %d:\ngot  %q\nwant %q", i, "", wantLines[0])
+			return
+		}
+		if len(wantLines) == 0 {
+			t.Errorf("line %d:\ngot  %q\nwant %q", i, gotLines[0], "")
+			return
+		}
+		g, w := gotLines[0], wantLines[0]
+		gotLines = gotLines[1:]
+		wantLines = wantLines[1:]
+		if g != w {
+			t.Errorf("line %d:\ngot  %q\nwant %q", i, g, w)
+			return
+		}
+	}
+}
+
+func rasterizeASCIIArt(width int, encoded []byte) (string, error) {
+	dst := image.NewAlpha(image.Rect(0, 0, width, width))
+	var z Rasterizer
+	z.SetDstImage(dst, dst.Bounds(), draw.Src)
+	if err := Decode(&z, encoded, nil); err != nil {
+		return "", err
+	}
+
+	const asciiArt = ".++8"
+	buf := make([]byte, 0, width*(width+1))
+	for y := 0; y < width; y++ {
+		for x := 0; x < width; x++ {
+			a := dst.AlphaAt(x, y).A
+			buf = append(buf, asciiArt[a>>6])
+		}
+		buf = append(buf, '\n')
+	}
+	return string(buf), nil
+}
+
+func TestDisassembleActionInfo(t *testing.T) {
+	got, err := disassemble(actionInfoIconVG)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := strings.Join([]string{
+		"89 49 56 47   Magic identifier",
+		"02            Number of metadata chunks: 1",
+		"0a            Metadata chunk length: 5",
+		"00            Metadata Identifier: 0 (viewBox)",
+		"50                -24",
+		"50                -24",
+		"b0                +24",
+		"b0                +24",
+		"c0            Start path, filled with CREG[CSEL-0]; M (absolute moveTo)",
+		"80                +0",
+		"58                -20",
+		"a0            C (absolute cubeTo), 1 reps",
+		"cf cc 30 c1       -11.049999",
+		"58                -20",
+		"58                -20",
+		"cf cc 30 c1       -11.049999",
+		"58                -20",
+		"80                +0",
+		"91            s (relative smooth cubeTo), 2 reps",
+		"37 33 0f 41       +8.950001",
+		"a8                +20",
+		"a8                +20",
+		"a8                +20",
+		"              s (relative smooth cubeTo), implicit",
+		"a8                +20",
+		"37 33 0f c1       -8.950001",
+		"a8                +20",
+		"58                -20",
+		"80            S (absolute smooth cubeTo), 1 reps",
+		"cf cc 30 41       +11.049999",
+		"58                -20",
+		"80                +0",
+		"58                -20",
+		"e3            z (closePath); m (relative moveTo)",
+		"84                +2",
+		"bc                +30",
+		"e7            h (relative horizontal lineTo)",
+		"78                -4",
+		"e8            V (absolute vertical lineTo)",
+		"7c                -2",
+		"e7            h (relative horizontal lineTo)",
+		"88                +4",
+		"e9            v (relative vertical lineTo)",
+		"98                +12",
+		"e3            z (closePath); m (relative moveTo)",
+		"80                +0",
+		"60                -16",
+		"e7            h (relative horizontal lineTo)",
+		"78                -4",
+		"e9            v (relative vertical lineTo)",
+		"78                -4",
+		"e7            h (relative horizontal lineTo)",
+		"88                +4",
+		"e9            v (relative vertical lineTo)",
+		"88                +4",
+		"e1            z (closePath); end path",
+	}, "\n") + "\n"
+
+	if got != want {
+		t.Errorf("got:\n%s\nwant:\n%s", got, want)
+		diffLines(t, got, want)
+	}
+}
+
+func TestDecodeActionInfo(t *testing.T) {
+	got, err := rasterizeASCIIArt(24, actionInfoIconVG)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := strings.Join([]string{
+		"........................",
+		"........................",
+		"........++8888++........",
+		"......+8888888888+......",
+		".....+888888888888+.....",
+		"....+88888888888888+....",
+		"...+8888888888888888+...",
+		"...88888888..88888888...",
+		"..+88888888..88888888+..",
+		"..+888888888888888888+..",
+		"..88888888888888888888..",
+		"..888888888..888888888..",
+		"..888888888..888888888..",
+		"..888888888..888888888..",
+		"..+88888888..88888888+..",
+		"..+88888888..88888888+..",
+		"...88888888..88888888...",
+		"...+8888888888888888+...",
+		"....+88888888888888+....",
+		".....+888888888888+.....",
+		"......+8888888888+......",
+		"........++8888++........",
+		"........................",
+		"........................",
+	}, "\n") + "\n"
+
+	if got != want {
+		t.Errorf("got:\n%s\nwant:\n%s", got, want)
+		diffLines(t, got, want)
+	}
+}
diff --git a/shiny/iconvg/encode.go b/shiny/iconvg/encode.go
index 9822bd4..4c0d950 100644
--- a/shiny/iconvg/encode.go
+++ b/shiny/iconvg/encode.go
@@ -16,6 +16,8 @@
 	errStylingOpsUsedInDrawingMode = errors.New("iconvg: styling ops used in drawing mode")
 )
 
+// TODO: delete the NewEncoder function, and just make the zero value usable.
+
 // NewEncoder returns a new Encoder for the given Metadata.
 func NewEncoder(m Metadata) *Encoder {
 	e := &Encoder{
@@ -226,7 +228,7 @@
 	}
 
 	if op := drawOps[e.drawOp]; op.nArgs == 0 {
-		e.buf = append(e.buf, op.opCodeBase)
+		e.buf = append(e.buf, op.opcodeBase)
 	} else {
 		n := len(e.drawArgs) / int(op.nArgs)
 		for i := 0; n > 0; {
@@ -234,7 +236,7 @@
 			if m > int(op.maxRepCount) {
 				m = int(op.maxRepCount)
 			}
-			e.buf = append(e.buf, op.opCodeBase+uint8(m)-1)
+			e.buf = append(e.buf, op.opcodeBase+uint8(m)-1)
 
 			switch e.drawOp {
 			default:
@@ -263,7 +265,7 @@
 }
 
 var drawOps = [256]struct {
-	opCodeBase  byte
+	opcodeBase  byte
 	maxRepCount uint8
 	nArgs       uint8
 }{
diff --git a/shiny/iconvg/iconvg.go b/shiny/iconvg/iconvg.go
index d65615a..ec5a881 100644
--- a/shiny/iconvg/iconvg.go
+++ b/shiny/iconvg/iconvg.go
@@ -13,8 +13,14 @@
 
 const magic = "\x89IVG"
 
+var magicBytes = []byte(magic)
+
 var positiveInfinity = math.Float32frombits(0x7f800000)
 
+func isNaNOrInfinity(f float32) bool {
+	return math.Float32bits(f)&0x7f800000 == 0x7f800000
+}
+
 const (
 	midViewBox          = 0
 	midSuggestedPalette = 1
diff --git a/shiny/iconvg/rasterizer.go b/shiny/iconvg/rasterizer.go
new file mode 100644
index 0000000..e774b6a
--- /dev/null
+++ b/shiny/iconvg/rasterizer.go
@@ -0,0 +1,254 @@
+// 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"
+	"image/color"
+	"image/draw"
+
+	"golang.org/x/image/math/f32"
+	"golang.org/x/image/vector"
+)
+
+const (
+	smoothTypeNone = iota
+	smoothTypeQuad
+	smoothTypeCube
+)
+
+// Rasterizer is a Destination that draws an IconVG graphic onto a raster
+// image.
+//
+// The zero value is usable, in that it has no raster image to draw onto, so
+// that calling Decode with this Destination is a no-op (other than checking
+// the encoded form for errors in the byte code). Call SetDstImage to change
+// the raster image, before calling Decode or between calls to Decode.
+type Rasterizer struct {
+	z vector.Rasterizer
+
+	dst    draw.Image
+	r      image.Rectangle
+	drawOp draw.Op
+
+	// scale and bias transforms the metadata.ViewBox rectangle to the (0, 0) -
+	// (r.Dx(), r.Dy()) rectangle.
+	scaleX float32
+	biasX  float32
+	scaleY float32
+	biasY  float32
+
+	metadata Metadata
+
+	firstStartPath  bool
+	prevSmoothType  uint8
+	prevSmoothPoint f32.Vec2
+
+	cSel uint32
+	nSel uint32
+	lod0 float32
+	lod1 float32
+
+	creg [64]color.RGBA
+	nreg [64]float32
+}
+
+// SetDstImage sets the Rasterizer to draw onto a destination image, given by
+// dst and r, with the given compositing operator.
+//
+// The IconVG graphic (which does not have a fixed size in pixels) will be
+// scaled in the X and Y dimensions to fit the rectangle r. The scaling factors
+// may differ in the two dimensions.
+func (z *Rasterizer) SetDstImage(dst draw.Image, r image.Rectangle, drawOp draw.Op) {
+	z.dst = dst
+	if r.Empty() {
+		r = image.Rectangle{}
+	}
+	z.r = r
+	z.drawOp = drawOp
+	z.recalcTransform()
+}
+
+// Reset resets the Rasterizer for the given Metadata.
+func (z *Rasterizer) Reset(m Metadata) {
+	z.metadata = m
+	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.recalcTransform()
+}
+
+func (z *Rasterizer) recalcTransform() {
+	z.scaleX = float32(z.r.Dx()) / (z.metadata.ViewBox.Max[0] - z.metadata.ViewBox.Min[0])
+	z.biasX = -z.metadata.ViewBox.Min[0]
+	z.scaleY = float32(z.r.Dy()) / (z.metadata.ViewBox.Max[1] - z.metadata.ViewBox.Min[1])
+	z.biasY = -z.metadata.ViewBox.Min[1]
+}
+
+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 }
+func (z *Rasterizer) relY(y float32) float32 { return z.scaleY * y }
+
+func (z *Rasterizer) absVec2(x, y float32) f32.Vec2 {
+	return f32.Vec2{z.absX(x), z.absY(y)}
+}
+
+func (z *Rasterizer) relVec2(x, y float32) f32.Vec2 {
+	pen := z.z.Pen()
+	return f32.Vec2{pen[0] + z.relX(x), pen[1] + z.relY(y)}
+}
+
+// implicitSmoothPoint returns the implicit control point for smooth-quadratic
+// and smooth-cubic Bézier curves.
+//
+// https://www.w3.org/TR/SVG/paths.html#PathDataCurveCommands says, "The first
+// control point is assumed to be the reflection of the second control point on
+// the previous command relative to the current point. (If there is no previous
+// command or if the previous command was not [a quadratic or cubic command],
+// assume the first control point is coincident with the current point.)"
+func (z *Rasterizer) implicitSmoothPoint(thisSmoothType uint8) f32.Vec2 {
+	pen := z.z.Pen()
+	if z.prevSmoothType != thisSmoothType {
+		return pen
+	}
+	return f32.Vec2{
+		2*pen[0] - z.prevSmoothPoint[0],
+		2*pen[1] - z.prevSmoothPoint[1],
+	}
+}
+
+func (z *Rasterizer) StartPath(adj int, x, y float32) {
+	// TODO: note adj, use it in ClosePathEndPath.
+
+	z.z.Reset(z.r.Dx(), z.r.Dy())
+	if z.firstStartPath {
+		z.firstStartPath = false
+		z.z.DrawOp = z.drawOp
+	}
+	z.prevSmoothType = smoothTypeNone
+	z.z.MoveTo(z.absVec2(x, y))
+}
+
+func (z *Rasterizer) ClosePathEndPath() {
+	z.z.ClosePath()
+	if z.dst == nil {
+		return
+	}
+	// TODO: don't assume image.Opaque.
+	z.z.Draw(z.dst, z.r, image.Opaque, image.Point{})
+}
+
+func (z *Rasterizer) ClosePathAbsMoveTo(x, y float32) {
+	z.prevSmoothType = smoothTypeNone
+	z.z.ClosePath()
+	z.z.MoveTo(z.absVec2(x, y))
+}
+
+func (z *Rasterizer) ClosePathRelMoveTo(x, y float32) {
+	z.prevSmoothType = smoothTypeNone
+	z.z.ClosePath()
+	z.z.MoveTo(z.relVec2(x, y))
+}
+
+func (z *Rasterizer) AbsHLineTo(x float32) {
+	z.prevSmoothType = smoothTypeNone
+	pen := z.z.Pen()
+	z.z.LineTo(f32.Vec2{z.absX(x), pen[1]})
+}
+
+func (z *Rasterizer) RelHLineTo(x float32) {
+	z.prevSmoothType = smoothTypeNone
+	pen := z.z.Pen()
+	z.z.LineTo(f32.Vec2{pen[0] + z.relX(x), pen[1]})
+}
+
+func (z *Rasterizer) AbsVLineTo(y float32) {
+	z.prevSmoothType = smoothTypeNone
+	pen := z.z.Pen()
+	z.z.LineTo(f32.Vec2{pen[0], z.absY(y)})
+}
+
+func (z *Rasterizer) RelVLineTo(y float32) {
+	z.prevSmoothType = smoothTypeNone
+	pen := z.z.Pen()
+	z.z.LineTo(f32.Vec2{pen[0], pen[1] + z.relY(y)})
+}
+
+func (z *Rasterizer) AbsLineTo(x, y float32) {
+	z.prevSmoothType = smoothTypeNone
+	z.z.LineTo(z.absVec2(x, y))
+}
+
+func (z *Rasterizer) RelLineTo(x, y float32) {
+	z.prevSmoothType = smoothTypeNone
+	z.z.LineTo(z.relVec2(x, y))
+}
+
+func (z *Rasterizer) AbsSmoothQuadTo(x, y float32) {
+	z.prevSmoothType = smoothTypeQuad
+	z.prevSmoothPoint = z.implicitSmoothPoint(smoothTypeQuad)
+	z.z.QuadTo(z.prevSmoothPoint, z.absVec2(x, y))
+}
+
+func (z *Rasterizer) RelSmoothQuadTo(x, y float32) {
+	z.prevSmoothType = smoothTypeQuad
+	z.prevSmoothPoint = z.implicitSmoothPoint(smoothTypeQuad)
+	z.z.QuadTo(z.prevSmoothPoint, z.relVec2(x, y))
+}
+
+func (z *Rasterizer) AbsQuadTo(x1, y1, x, y float32) {
+	z.prevSmoothType = smoothTypeQuad
+	z.prevSmoothPoint = z.absVec2(x1, y1)
+	z.z.QuadTo(z.prevSmoothPoint, z.absVec2(x, y))
+}
+
+func (z *Rasterizer) RelQuadTo(x1, y1, x, y float32) {
+	z.prevSmoothType = smoothTypeQuad
+	z.prevSmoothPoint = z.relVec2(x1, y1)
+	z.z.QuadTo(z.prevSmoothPoint, z.relVec2(x, y))
+}
+
+func (z *Rasterizer) AbsSmoothCubeTo(x2, y2, x, y float32) {
+	p1 := z.implicitSmoothPoint(smoothTypeCube)
+	z.prevSmoothType = smoothTypeCube
+	z.prevSmoothPoint = z.absVec2(x2, y2)
+	z.z.CubeTo(p1, z.prevSmoothPoint, z.absVec2(x, y))
+}
+
+func (z *Rasterizer) RelSmoothCubeTo(x2, y2, x, y float32) {
+	p1 := z.implicitSmoothPoint(smoothTypeCube)
+	z.prevSmoothType = smoothTypeCube
+	z.prevSmoothPoint = z.relVec2(x2, y2)
+	z.z.CubeTo(p1, z.prevSmoothPoint, z.relVec2(x, y))
+}
+
+func (z *Rasterizer) AbsCubeTo(x1, y1, x2, y2, x, y float32) {
+	z.prevSmoothType = smoothTypeCube
+	z.prevSmoothPoint = z.absVec2(x2, y2)
+	z.z.CubeTo(z.absVec2(x1, y1), z.prevSmoothPoint, z.absVec2(x, y))
+}
+
+func (z *Rasterizer) RelCubeTo(x1, y1, x2, y2, x, y float32) {
+	z.prevSmoothType = smoothTypeCube
+	z.prevSmoothPoint = z.relVec2(x2, y2)
+	z.z.CubeTo(z.relVec2(x1, y1), z.prevSmoothPoint, z.relVec2(x, y))
+}
+
+func (z *Rasterizer) AbsArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
+	z.prevSmoothType = smoothTypeNone
+	// TODO: implement.
+}
+
+func (z *Rasterizer) RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
+	z.prevSmoothType = smoothTypeNone
+	// TODO: implement.
+}