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>