shiny/iconvg: implement an encoder.

Change-Id: Ib34dca2ae43ba116784a098d0fa2d4cace92aef1
Reviewed-on: https://go-review.googlesource.com/29135
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/iconvg/buffer.go b/shiny/iconvg/buffer.go
new file mode 100644
index 0000000..473421b
--- /dev/null
+++ b/shiny/iconvg/buffer.go
@@ -0,0 +1,155 @@
+// 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 (
+	"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
+// of bytes that value was encoded in. They return n == 0 if an error occured.
+//
+// The encodeXxx methods append to the buffer, modifying the slice in place.
+type buffer []byte
+
+func (b buffer) decodeNatural() (u uint32, n int) {
+	if len(b) < 1 {
+		return 0, 0
+	}
+	x := b[0]
+	if x&0x01 == 0 {
+		return uint32(x) >> 1, 1
+	}
+	if x&0x02 == 0 {
+		if len(b) >= 2 {
+			y := uint16(b[0]) | uint16(b[1])<<8
+			return uint32(y) >> 2, 2
+		}
+		return 0, 0
+	}
+	if len(b) >= 4 {
+		y := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
+		return y >> 2, 4
+	}
+	return 0, 0
+}
+
+func (b buffer) decodeReal() (f float32, n int) {
+	switch u, n := b.decodeNatural(); n {
+	case 0:
+		return 0, n
+	case 1:
+		return float32(u), n
+	case 2:
+		return float32(u), n
+	default:
+		return math.Float32frombits(u << 2), n
+	}
+}
+
+func (b buffer) decodeCoordinate() (f float32, n int) {
+	switch u, n := b.decodeNatural(); n {
+	case 0:
+		return 0, n
+	case 1:
+		return float32(int32(u) - 64), n
+	case 2:
+		return float32(int32(u)-64*128) / 64, n
+	default:
+		return math.Float32frombits(u << 2), n
+	}
+}
+
+func (b buffer) decodeZeroToOne() (f float32, n int) {
+	switch u, n := b.decodeNatural(); n {
+	case 0:
+		return 0, n
+	case 1:
+		return float32(u) / 120, n
+	case 2:
+		return float32(u) / 15120, n
+	default:
+		return math.Float32frombits(u << 2), n
+	}
+}
+
+func (b *buffer) encodeNatural(u uint32) {
+	if u < 1<<7 {
+		u = (u << 1)
+		*b = append(*b, uint8(u))
+		return
+	}
+	if u < 1<<14 {
+		u = (u << 2) | 1
+		*b = append(*b, uint8(u), uint8(u>>8))
+		return
+	}
+	u = (u << 2) | 3
+	*b = append(*b, uint8(u), uint8(u>>8), uint8(u>>16), uint8(u>>24))
+}
+
+func (b *buffer) encodeReal(f float32) {
+	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
+	}
+	b.encode4ByteReal(f)
+}
+
+func (b *buffer) encode4ByteReal(f float32) {
+	u := math.Float32bits(f)
+
+	// Round the fractional bits (the low 23 bits) to the nearest multiple of
+	// 4, being careful not to overflow into the upper bits.
+	v := u & 0x007fffff
+	if v < 0x007fffffe {
+		v += 2
+	}
+	u = (u & 0xff800000) | v
+
+	// A 4 byte encoding has the low two bits set.
+	u |= 0x03
+	*b = append(*b, uint8(u), uint8(u>>8), uint8(u>>16), uint8(u>>24))
+}
+
+func (b *buffer) encodeCoordinate(f float32) {
+	if i := int32(f); -64 <= i && i < +64 && float32(i) == f {
+		u := uint32(i + 64)
+		u = (u << 1)
+		*b = append(*b, uint8(u))
+		return
+	}
+	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
+	}
+	b.encode4ByteReal(f)
+}
+
+func (b *buffer) encodeZeroToOne(f float32) {
+	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
+	}
+	b.encode4ByteReal(f)
+}
diff --git a/shiny/iconvg/buffer_test.go b/shiny/iconvg/buffer_test.go
new file mode 100644
index 0000000..5e51aa3
--- /dev/null
+++ b/shiny/iconvg/buffer_test.go
@@ -0,0 +1,187 @@
+// 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 (
+	"math"
+	"testing"
+)
+
+var naturalTestCases = []struct {
+	in    buffer
+	want  uint32
+	wantN int
+}{{
+	buffer{},
+	0,
+	0,
+}, {
+	buffer{0x28},
+	20,
+	1,
+}, {
+	buffer{0x59},
+	0,
+	0,
+}, {
+	buffer{0x59, 0x83},
+	8406,
+	2,
+}, {
+	buffer{0x07, 0x00, 0x80},
+	0,
+	0,
+}, {
+	buffer{0x07, 0x00, 0x80, 0x3f},
+	266338305,
+	4,
+}}
+
+func TestDecodeNatural(t *testing.T) {
+	for _, tc := range naturalTestCases {
+		got, gotN := tc.in.decodeNatural()
+		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 TestEncodeNatural(t *testing.T) {
+	for _, tc := range naturalTestCases {
+		if tc.wantN == 0 {
+			continue
+		}
+		var b buffer
+		b.encodeNatural(tc.want)
+		if got, want := string(b), string(tc.in); got != want {
+			t.Errorf("value=%v:\ngot  % x\nwant % x", tc.want, got, want)
+		}
+	}
+}
+
+var realTestCases = []struct {
+	in    buffer
+	want  float32
+	wantN int
+}{{
+	buffer{0x28},
+	20,
+	1,
+}, {
+	buffer{0x59, 0x83},
+	8406,
+	2,
+}, {
+	buffer{0x07, 0x00, 0x80, 0x3f},
+	1.000000476837158203125,
+	4,
+}}
+
+func TestDecodeReal(t *testing.T) {
+	for _, tc := range realTestCases {
+		got, gotN := tc.in.decodeReal()
+		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 TestEncodeReal(t *testing.T) {
+	for _, tc := range realTestCases {
+		var b buffer
+		b.encodeReal(tc.want)
+		if got, want := string(b), string(tc.in); got != want {
+			t.Errorf("value=%v:\ngot  % x\nwant % x", tc.want, got, want)
+		}
+	}
+}
+
+var coordinateTestCases = []struct {
+	in    buffer
+	want  float32
+	wantN int
+}{{
+	buffer{0x8e},
+	7,
+	1,
+}, {
+	buffer{0x81, 0x87},
+	7.5,
+	2,
+}, {
+	buffer{0x03, 0x00, 0xf0, 0x40},
+	7.5,
+	4,
+}, {
+	buffer{0x07, 0x00, 0xf0, 0x40},
+	7.5000019073486328125,
+	4,
+}}
+
+func TestDecodeCoordinate(t *testing.T) {
+	for _, tc := range coordinateTestCases {
+		got, gotN := tc.in.decodeCoordinate()
+		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 TestEncodeCoordinate(t *testing.T) {
+	for _, tc := range coordinateTestCases {
+		if tc.want == 7.5 && tc.wantN == 4 {
+			// 7.5 can be encoded in fewer than 4 bytes.
+			continue
+		}
+		var b buffer
+		b.encodeCoordinate(tc.want)
+		if got, want := string(b), string(tc.in); got != want {
+			t.Errorf("value=%v:\ngot  % x\nwant % x", tc.want, got, want)
+		}
+	}
+}
+
+func trunc(x float32) float32 {
+	u := math.Float32bits(x)
+	u &^= 0x03
+	return math.Float32frombits(u)
+}
+
+var zeroToOneTestCases = []struct {
+	in    buffer
+	want  float32
+	wantN int
+}{{
+	buffer{0x0a},
+	1.0 / 24,
+	1,
+}, {
+	buffer{0x41, 0x1a},
+	1.0 / 9,
+	2,
+}, {
+	buffer{0x63, 0x0b, 0x36, 0x3b},
+	trunc(1.0 / 360),
+	4,
+}}
+
+func TestDecodeZeroToOne(t *testing.T) {
+	for _, tc := range zeroToOneTestCases {
+		got, gotN := tc.in.decodeZeroToOne()
+		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 TestEncodeZeroToOne(t *testing.T) {
+	for _, tc := range zeroToOneTestCases {
+		var b buffer
+		b.encodeZeroToOne(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/encode.go b/shiny/iconvg/encode.go
new file mode 100644
index 0000000..f502ee8
--- /dev/null
+++ b/shiny/iconvg/encode.go
@@ -0,0 +1,293 @@
+// 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 (
+	"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")
+)
+
+// NewEncoder returns a new Encoder for the given Metadata.
+func NewEncoder(m Metadata) *Encoder {
+	e := &Encoder{
+		buf: make(buffer, 0, 1024),
+	}
+	e.Reset(m)
+	return e
+}
+
+// Encoder is an IconVG encoder.
+type Encoder struct {
+	buf      buffer
+	altBuf   buffer
+	metadata Metadata
+	err      error
+
+	mode     mode
+	drawOp   byte
+	drawArgs []float32
+
+	cSel uint32
+	nSel uint32
+	lod0 float32
+	lod1 float32
+}
+
+// Bytes returns the encoded form.
+func (e *Encoder) Bytes() ([]byte, error) {
+	if e.err != nil {
+		return nil, e.err
+	}
+	return []byte(e.buf), nil
+}
+
+// Reset resets the Encoder for the given Metadata.
+func (e *Encoder) Reset(m Metadata) {
+	*e = Encoder{
+		buf:      append(e.buf[:0], magic...),
+		metadata: m,
+		lod1:     positiveInfinity,
+	}
+
+	nMetadataChunks := 0
+	mcViewBox := m.ViewBox != DefaultViewBox
+	if mcViewBox {
+		nMetadataChunks++
+	}
+	mcSuggestedPalette := m.Palette != DefaultPalette
+	if mcSuggestedPalette {
+		nMetadataChunks++
+	}
+	e.buf.encodeNatural(uint32(nMetadataChunks))
+
+	if mcViewBox {
+		e.altBuf = e.altBuf[:0]
+		e.altBuf.encodeNatural(midViewBox)
+		e.altBuf.encodeCoordinate(m.ViewBox.Min[0])
+		e.altBuf.encodeCoordinate(m.ViewBox.Min[1])
+		e.altBuf.encodeCoordinate(m.ViewBox.Max[0])
+		e.altBuf.encodeCoordinate(m.ViewBox.Max[1])
+
+		e.buf.encodeNatural(uint32(len(e.altBuf)))
+		e.buf = append(e.buf, e.altBuf...)
+	}
+
+	if mcSuggestedPalette {
+		panic("TODO: encode mcSuggestedPalette")
+	}
+}
+
+func (e *Encoder) CSel() uint32              { return e.cSel }
+func (e *Encoder) NSel() uint32              { return e.nSel }
+func (e *Encoder) LOD() (lod0, lod1 float32) { return e.lod0, e.lod1 }
+
+func (e *Encoder) SetCSel(cSel uint32) {
+	if e.err != nil {
+		return
+	}
+	if e.mode != modeStyling {
+		e.err = errStylingOpsUsedInDrawingMode
+		return
+	}
+	e.cSel = cSel
+	e.buf = append(e.buf, uint8(cSel&0x3f))
+}
+
+func (e *Encoder) SetNSel(nSel uint32) {
+	if e.err != nil {
+		return
+	}
+	if e.mode != modeStyling {
+		e.err = errStylingOpsUsedInDrawingMode
+		return
+	}
+	e.nSel = nSel
+	e.buf = append(e.buf, uint8((nSel&0x3f)|0x40))
+}
+
+func (e *Encoder) SetLOD(lod0, lod1 float32) {
+	if e.err != nil {
+		return
+	}
+	if e.mode != modeStyling {
+		e.err = errStylingOpsUsedInDrawingMode
+		return
+	}
+	e.lod0 = lod0
+	e.lod1 = lod1
+	e.buf = append(e.buf, 0xe7)
+	e.buf.encodeReal(lod0)
+	e.buf.encodeReal(lod1)
+}
+
+func (e *Encoder) StartPath(adj int, x, y float32) {
+	if e.err != nil {
+		return
+	}
+	if e.mode != modeStyling {
+		e.err = errStylingOpsUsedInDrawingMode
+		return
+	}
+	if adj < -6 || 0 < adj {
+		e.err = errInvalidSelectorAdjustment
+		return
+	}
+	e.buf = append(e.buf, uint8(0xb0-adj))
+	e.buf.encodeCoordinate(x)
+	e.buf.encodeCoordinate(y)
+	e.mode = modeDrawing
+}
+
+func (e *Encoder) AbsHLineTo(x float32)                   { e.draw('H', x, 0, 0, 0, 0, 0) }
+func (e *Encoder) RelHLineTo(x float32)                   { e.draw('h', x, 0, 0, 0, 0, 0) }
+func (e *Encoder) AbsVLineTo(y float32)                   { e.draw('V', y, 0, 0, 0, 0, 0) }
+func (e *Encoder) RelVLineTo(y float32)                   { e.draw('v', y, 0, 0, 0, 0, 0) }
+func (e *Encoder) AbsLineTo(x, y float32)                 { e.draw('L', x, y, 0, 0, 0, 0) }
+func (e *Encoder) RelLineTo(x, y float32)                 { e.draw('l', x, y, 0, 0, 0, 0) }
+func (e *Encoder) AbsSmoothQuadTo(x, y float32)           { e.draw('T', x, y, 0, 0, 0, 0) }
+func (e *Encoder) RelSmoothQuadTo(x, y float32)           { e.draw('t', x, y, 0, 0, 0, 0) }
+func (e *Encoder) AbsQuadTo(x1, y1, x, y float32)         { e.draw('Q', x1, y1, x, y, 0, 0) }
+func (e *Encoder) RelQuadTo(x1, y1, x, y float32)         { e.draw('q', x1, y1, x, y, 0, 0) }
+func (e *Encoder) AbsSmoothCubeTo(x2, y2, x, y float32)   { e.draw('S', x2, y2, x, y, 0, 0) }
+func (e *Encoder) RelSmoothCubeTo(x2, y2, x, y float32)   { e.draw('s', x2, y2, x, y, 0, 0) }
+func (e *Encoder) AbsCubeTo(x1, y1, x2, y2, x, y float32) { e.draw('C', x1, y1, x2, y2, x, y) }
+func (e *Encoder) RelCubeTo(x1, y1, x2, y2, x, y float32) { e.draw('c', x1, y1, x2, y2, x, y) }
+func (e *Encoder) ClosePathEndPath()                      { e.draw('Z', 0, 0, 0, 0, 0, 0) }
+func (e *Encoder) ClosePathAbsMoveTo(x, y float32)        { e.draw('Y', x, y, 0, 0, 0, 0) }
+func (e *Encoder) ClosePathRelMoveTo(x, y float32)        { e.draw('y', x, y, 0, 0, 0, 0) }
+
+func (e *Encoder) AbsArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
+	e.arcTo('A', rx, ry, xAxisRotation, largeArc, sweep, x, y)
+}
+
+func (e *Encoder) RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
+	e.arcTo('a', rx, ry, xAxisRotation, largeArc, sweep, x, y)
+}
+
+func (e *Encoder) arcTo(drawOp byte, rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
+	flags := uint32(0)
+	if largeArc {
+		flags |= 0x01
+	}
+	if sweep {
+		flags |= 0x02
+	}
+	e.draw(drawOp, rx, ry, xAxisRotation, float32(flags), x, y)
+}
+
+func (e *Encoder) draw(drawOp byte, arg0, arg1, arg2, arg3, arg4, arg5 float32) {
+	if e.err != nil {
+		return
+	}
+	if e.mode != modeDrawing {
+		e.err = errDrawingOpsUsedInStylingMode
+		return
+	}
+	if e.drawOp != drawOp {
+		e.flushDrawOps()
+	}
+	e.drawOp = drawOp
+	switch drawOps[drawOp].nArgs {
+	case 0:
+		// No-op.
+	case 1:
+		e.drawArgs = append(e.drawArgs, arg0)
+	case 2:
+		e.drawArgs = append(e.drawArgs, arg0, arg1)
+	case 4:
+		e.drawArgs = append(e.drawArgs, arg0, arg1, arg2, arg3)
+	case 6:
+		e.drawArgs = append(e.drawArgs, arg0, arg1, arg2, arg3, arg4, arg5)
+	default:
+		panic("unreachable")
+	}
+
+	switch drawOp {
+	case 'Z':
+		e.mode = modeStyling
+		fallthrough
+	case 'Y', 'y':
+		e.flushDrawOps()
+	}
+}
+
+func (e *Encoder) flushDrawOps() {
+	if e.drawOp == 0x00 {
+		return
+	}
+
+	if op := drawOps[e.drawOp]; op.nArgs == 0 {
+		e.buf = append(e.buf, op.opCodeBase)
+	} else {
+		n := len(e.drawArgs) / int(op.nArgs)
+		for i := 0; n > 0; {
+			m := n
+			if m > int(op.maxRepCount) {
+				m = int(op.maxRepCount)
+			}
+			e.buf = append(e.buf, op.opCodeBase+uint8(m)-1)
+
+			switch e.drawOp {
+			default:
+				for j := m * int(op.nArgs); j > 0; j-- {
+					e.buf.encodeCoordinate(e.drawArgs[i])
+					i++
+				}
+			case 'A', 'a':
+				for j := m; j > 0; j-- {
+					e.buf.encodeCoordinate(e.drawArgs[i+0])
+					e.buf.encodeCoordinate(e.drawArgs[i+1])
+					e.buf.encodeZeroToOne(e.drawArgs[i+2])
+					e.buf.encodeNatural(uint32(e.drawArgs[i+3]))
+					e.buf.encodeCoordinate(e.drawArgs[i+4])
+					e.buf.encodeCoordinate(e.drawArgs[i+5])
+					i += 6
+				}
+			}
+
+			n -= m
+		}
+	}
+
+	e.drawOp = 0x00
+	e.drawArgs = e.drawArgs[:0]
+}
+
+var drawOps = [256]struct {
+	opCodeBase  byte
+	maxRepCount uint8
+	nArgs       uint8
+}{
+	'L': {0x00, 32, 2},
+	'l': {0x20, 32, 2},
+	'T': {0x40, 16, 2},
+	't': {0x50, 16, 2},
+	'Q': {0x60, 16, 4},
+	'q': {0x70, 16, 4},
+	'S': {0x80, 16, 4},
+	's': {0x90, 16, 4},
+	'C': {0xa0, 16, 6},
+	'c': {0xb0, 16, 6},
+	'A': {0xc0, 16, 6},
+	'a': {0xd0, 16, 6},
+
+	// Z means close path and then end path.
+	'Z': {0xe1, 1, 0},
+	// Y/y means close path and then open a new path (with a MoveTo/moveTo).
+	'Y': {0xe2, 1, 2},
+	'y': {0xe3, 1, 2},
+
+	'H': {0xe6, 1, 1},
+	'h': {0xe7, 1, 1},
+	'V': {0xe8, 1, 1},
+	'v': {0xe9, 1, 1},
+}
diff --git a/shiny/iconvg/encode_test.go b/shiny/iconvg/encode_test.go
new file mode 100644
index 0000000..62c28a3
--- /dev/null
+++ b/shiny/iconvg/encode_test.go
@@ -0,0 +1,60 @@
+// 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"
+	"testing"
+
+	"golang.org/x/image/math/f32"
+)
+
+// 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, 0xb0, 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) {
+	e := NewEncoder(Metadata{
+		ViewBox: Rectangle{
+			Min: f32.Vec2{-24, -24},
+			Max: f32.Vec2{+24, +24},
+		},
+		Palette: DefaultPalette,
+	})
+
+	e.StartPath(0, 0, -20)
+	e.AbsCubeTo(-11.05, -20, -20, -11.05, -20, 0)
+	e.RelSmoothCubeTo(8.95, 20, 20, 20)
+	e.RelSmoothCubeTo(20, -8.95, 20, -20)
+	e.AbsSmoothCubeTo(11.05, -20, 0, -20)
+	e.ClosePathRelMoveTo(2, 30)
+	e.RelHLineTo(-4)
+	e.AbsVLineTo(-2)
+	e.RelHLineTo(4)
+	e.RelVLineTo(12)
+	e.ClosePathRelMoveTo(0, -16)
+	e.RelHLineTo(-4)
+	e.RelVLineTo(-4)
+	e.RelHLineTo(4)
+	e.RelVLineTo(4)
+	e.ClosePathEndPath()
+
+	got, err := e.Bytes()
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := actionInfoIconVG
+	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
new file mode 100644
index 0000000..d65615a
--- /dev/null
+++ b/shiny/iconvg/iconvg.go
@@ -0,0 +1,127 @@
+// 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"
+	"math"
+
+	"golang.org/x/image/math/f32"
+)
+
+const magic = "\x89IVG"
+
+var positiveInfinity = math.Float32frombits(0x7f800000)
+
+const (
+	midViewBox          = 0
+	midSuggestedPalette = 1
+)
+
+type mode bool
+
+const (
+	modeStyling mode = false
+	modeDrawing mode = true
+)
+
+// Rectangle is defined by its minimum and maximum coordinates.
+type Rectangle struct {
+	Min, Max f32.Vec2
+}
+
+// AspectRatio returns the Rectangle's aspect ratio. An IconVG graphic is
+// scalable; these dimensions do not necessarily map 1:1 to pixels.
+func (r *Rectangle) AspectRatio() (dx, dy float32) {
+	return r.Max[0] - r.Min[0], r.Max[1] - r.Min[1]
+}
+
+// Palette is an IconVG palette.
+type Palette [64]color.RGBA
+
+// Metadata is an IconVG's metadata.
+type Metadata struct {
+	ViewBox Rectangle
+
+	// Palette is a 64 color palette. When encoding, it is the suggested
+	// palette to place within the IconVG graphic. When decoding, it is either
+	// the optional palette passed to Decode, or if no optional palette was
+	// given, the suggested palette within the IconVG graphic.
+	Palette Palette
+}
+
+// DefaultViewBox is the default ViewBox. Its values should not be modified.
+var DefaultViewBox = Rectangle{
+	Min: f32.Vec2{-32, -32},
+	Max: f32.Vec2{+32, +32},
+}
+
+// DefaultPalette is the default Palette. Its values should not be modified.
+var DefaultPalette = Palette{
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+	color.RGBA{0x00, 0x00, 0x00, 0xff},
+}