shiny/iconvg: implement Level Of Detail (LOD).
Change-Id: I39dd38b828eae2a00cbca6a6dc9354ff4b21e027
Reviewed-on: https://go-review.googlesource.com/30830
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/shiny/iconvg/decode_test.go b/shiny/iconvg/decode_test.go
index 226e71a..9a12c55 100644
--- a/shiny/iconvg/decode_test.go
+++ b/shiny/iconvg/decode_test.go
@@ -121,34 +121,38 @@
}
}
-var testdataTestCases = []string{
- "testdata/action-info",
- "testdata/blank",
- "testdata/video-005.primitive",
+var testdataTestCases = []struct {
+ filename string
+ variants string
+}{
+ {"testdata/action-info", ""},
+ {"testdata/blank", ""},
+ {"testdata/lod-polygon", ";64"},
+ {"testdata/video-005.primitive", ""},
}
func TestDisassembly(t *testing.T) {
for _, tc := range testdataTestCases {
- ivgData, err := ioutil.ReadFile(filepath.FromSlash(tc) + ".ivg")
+ ivgData, err := ioutil.ReadFile(filepath.FromSlash(tc.filename) + ".ivg")
if err != nil {
- t.Errorf("%s: ReadFile: %v", tc, err)
+ t.Errorf("%s: ReadFile: %v", tc.filename, err)
continue
}
got, err := disassemble(ivgData)
if err != nil {
- t.Errorf("%s: disassemble: %v", tc, err)
+ t.Errorf("%s: disassemble: %v", tc.filename, err)
continue
}
- wantFilename := filepath.FromSlash(tc) + ".ivg.disassembly"
+ wantFilename := filepath.FromSlash(tc.filename) + ".ivg.disassembly"
if overwriteTestdataFiles {
if err := ioutil.WriteFile(filepath.FromSlash(wantFilename), got, 0666); err != nil {
- t.Errorf("%s: WriteFile: %v", tc, err)
+ t.Errorf("%s: WriteFile: %v", tc.filename, err)
}
continue
}
want, err := ioutil.ReadFile(wantFilename)
if err != nil {
- t.Errorf("%s: ReadFile: %v", tc, err)
+ t.Errorf("%s: ReadFile: %v", tc.filename, err)
continue
}
if !bytes.Equal(got, want) {
@@ -160,46 +164,57 @@
func TestRasterizer(t *testing.T) {
for _, tc := range testdataTestCases {
- ivgData, err := ioutil.ReadFile(filepath.FromSlash(tc) + ".ivg")
+ ivgData, err := ioutil.ReadFile(filepath.FromSlash(tc.filename) + ".ivg")
if err != nil {
- t.Errorf("%s: ReadFile: %v", tc, err)
+ t.Errorf("%s: ReadFile: %v", tc.filename, 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)
+ t.Errorf("%s: DecodeMetadata: %v", tc.filename, err)
continue
}
- wantFilename := filepath.FromSlash(tc) + ".png"
- if overwriteTestdataFiles {
- if err := encodePNG(filepath.FromSlash(wantFilename), got); err != nil {
- t.Errorf("%s: encodePNG: %v", tc, err)
+ for _, variant := range strings.Split(tc.variants, ";") {
+ length := 256
+ if variant == "64" {
+ length = 64
}
- 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
+ width, height := length, length
+ if dx, dy := md.ViewBox.AspectRatio(); dx < dy {
+ width = int(float32(length) * dx / dy)
+ } else {
+ height = int(float32(length) * 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 %q variant: Decode: %v", tc.filename, variant, err)
+ continue
+ }
+
+ wantFilename := filepath.FromSlash(tc.filename)
+ if variant != "" {
+ wantFilename += "." + variant
+ }
+ wantFilename += ".png"
+ if overwriteTestdataFiles {
+ if err := encodePNG(filepath.FromSlash(wantFilename), got); err != nil {
+ t.Errorf("%s %q variant: encodePNG: %v", tc.filename, variant, err)
+ }
+ continue
+ }
+ want, err := decodePNG(wantFilename)
+ if err != nil {
+ t.Errorf("%s %q variant: decodePNG: %v", tc.filename, variant, err)
+ continue
+ }
+ if err := checkApproxEqual(got, want); err != nil {
+ t.Errorf("%s %q variant: %v", tc.filename, variant, err)
+ continue
+ }
}
}
}
diff --git a/shiny/iconvg/doc.go b/shiny/iconvg/doc.go
index 94316ee..791b4f7 100644
--- a/shiny/iconvg/doc.go
+++ b/shiny/iconvg/doc.go
@@ -545,11 +545,10 @@
88 +4
e1 z (closePath); end path
+There are more examples in the ./testdata directory.
*/
package iconvg
-// TODO: more examples, using multiple colors and Level of Detail.
-
// TODO: shapes (circles, rects) and strokes? Or can we assume that authoring
// tools will convert shapes and strokes to paths?
diff --git a/shiny/iconvg/encode_test.go b/shiny/iconvg/encode_test.go
index 425f747..00e0e31 100644
--- a/shiny/iconvg/encode_test.go
+++ b/shiny/iconvg/encode_test.go
@@ -8,15 +8,26 @@
"bytes"
"image/color"
"io/ioutil"
+ "math"
"path/filepath"
"testing"
"golang.org/x/image/math/f32"
)
-// overwriteTestdataFiles is set to true when adding new testdataTestCases.
+// overwriteTestdataFiles is temporarily set to true when adding new
+// testdataTestCases.
const overwriteTestdataFiles = false
+// TestOverwriteTestdataFilesIsFalse tests that any change to
+// overwriteTestdataFiles is only temporary. Programmers are assumed to run "go
+// test" before sending out for code review or committing code.
+func TestOverwriteTestdataFilesIsFalse(t *testing.T) {
+ if overwriteTestdataFiles {
+ t.Errorf("overwriteTestdataFiles is true; do not commit code changes")
+ }
+}
+
func testEncode(t *testing.T, e *Encoder, wantFilename string) {
got, err := e.Bytes()
if err != nil {
@@ -150,3 +161,39 @@
testEncode(t, &e, "testdata/video-005.primitive.ivg")
}
+
+func TestEncodeLODPolygon(t *testing.T) {
+ var e Encoder
+
+ poly := func(n int) {
+ const r = 28
+ angle := 2 * math.Pi / float64(n)
+ e.StartPath(0, r, 0)
+ for i := 1; i < n; i++ {
+ e.AbsLineTo(
+ float32(r*math.Cos(angle*float64(i))),
+ float32(r*math.Sin(angle*float64(i))),
+ )
+ }
+ e.ClosePathEndPath()
+ }
+
+ e.StartPath(0, -28, -20)
+ e.AbsVLineTo(-28)
+ e.AbsHLineTo(-20)
+ e.ClosePathEndPath()
+
+ e.SetLOD(0, 80)
+ poly(3)
+
+ e.SetLOD(80, positiveInfinity)
+ poly(5)
+
+ e.SetLOD(0, positiveInfinity)
+ e.StartPath(0, +28, +20)
+ e.AbsVLineTo(+28)
+ e.AbsHLineTo(+20)
+ e.ClosePathEndPath()
+
+ testEncode(t, &e, "testdata/lod-polygon.ivg")
+}
diff --git a/shiny/iconvg/rasterizer.go b/shiny/iconvg/rasterizer.go
index 4a8c85f..ce3c95d 100644
--- a/shiny/iconvg/rasterizer.go
+++ b/shiny/iconvg/rasterizer.go
@@ -47,6 +47,8 @@
cSel uint8
nSel uint8
+ disabled bool
+
firstStartPath bool
prevSmoothType uint8
prevSmoothPoint f32.Vec2
@@ -116,7 +118,6 @@
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) }
@@ -158,7 +159,14 @@
z.flatImage.C = &z.flatColor
z.fill = &z.flatImage
- z.z.Reset(z.r.Dx(), z.r.Dy())
+ width, height := z.r.Dx(), z.r.Dy()
+ h := float32(height)
+ z.disabled = z.flatColor.A == 0 || !(z.lod0 <= h && h < z.lod1)
+ if z.disabled {
+ return
+ }
+
+ z.z.Reset(width, height)
if z.firstStartPath {
z.firstStartPath = false
z.z.DrawOp = z.drawOp
@@ -168,6 +176,9 @@
}
func (z *Rasterizer) ClosePathEndPath() {
+ if z.disabled {
+ return
+ }
z.z.ClosePath()
if z.dst == nil {
return
@@ -176,76 +187,115 @@
}
func (z *Rasterizer) ClosePathAbsMoveTo(x, y float32) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeNone
z.z.ClosePath()
z.z.MoveTo(z.absVec2(x, y))
}
func (z *Rasterizer) ClosePathRelMoveTo(x, y float32) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeNone
z.z.ClosePath()
z.z.MoveTo(z.relVec2(x, y))
}
func (z *Rasterizer) AbsHLineTo(x float32) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeNone
pen := z.z.Pen()
z.z.LineTo(f32.Vec2{z.absX(x), pen[1]})
}
func (z *Rasterizer) RelHLineTo(x float32) {
+ if z.disabled {
+ return
+ }
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) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeNone
pen := z.z.Pen()
z.z.LineTo(f32.Vec2{pen[0], z.absY(y)})
}
func (z *Rasterizer) RelVLineTo(y float32) {
+ if z.disabled {
+ return
+ }
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) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeNone
z.z.LineTo(z.absVec2(x, y))
}
func (z *Rasterizer) RelLineTo(x, y float32) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeNone
z.z.LineTo(z.relVec2(x, y))
}
func (z *Rasterizer) AbsSmoothQuadTo(x, y float32) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeQuad
z.prevSmoothPoint = z.implicitSmoothPoint(smoothTypeQuad)
z.z.QuadTo(z.prevSmoothPoint, z.absVec2(x, y))
}
func (z *Rasterizer) RelSmoothQuadTo(x, y float32) {
+ if z.disabled {
+ return
+ }
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) {
+ if z.disabled {
+ return
+ }
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) {
+ if z.disabled {
+ return
+ }
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) {
+ if z.disabled {
+ return
+ }
p1 := z.implicitSmoothPoint(smoothTypeCube)
z.prevSmoothType = smoothTypeCube
z.prevSmoothPoint = z.absVec2(x2, y2)
@@ -253,6 +303,9 @@
}
func (z *Rasterizer) RelSmoothCubeTo(x2, y2, x, y float32) {
+ if z.disabled {
+ return
+ }
p1 := z.implicitSmoothPoint(smoothTypeCube)
z.prevSmoothType = smoothTypeCube
z.prevSmoothPoint = z.relVec2(x2, y2)
@@ -260,23 +313,35 @@
}
func (z *Rasterizer) AbsCubeTo(x1, y1, x2, y2, x, y float32) {
+ if z.disabled {
+ return
+ }
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) {
+ if z.disabled {
+ return
+ }
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) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeNone
// TODO: implement.
}
func (z *Rasterizer) RelArcTo(rx, ry, xAxisRotation float32, largeArc, sweep bool, x, y float32) {
+ if z.disabled {
+ return
+ }
z.prevSmoothType = smoothTypeNone
// TODO: implement.
}
diff --git a/shiny/iconvg/testdata/README b/shiny/iconvg/testdata/README
index bf297de..663f256 100644
--- a/shiny/iconvg/testdata/README
+++ b/shiny/iconvg/testdata/README
@@ -18,6 +18,14 @@
+lod-polygon.ivg was created manually.
+
+lod-polygon.ivg.disassembly is a disassembly of that IconVG file.
+
+lod-polygon.png and lod-polygon.64.png are renderings of that IconVG file.
+
+
+
video-005.jpeg comes from an old version of the Go repository. See
https://codereview.appspot.com/5758047/
diff --git a/shiny/iconvg/testdata/lod-polygon.64.png b/shiny/iconvg/testdata/lod-polygon.64.png
new file mode 100644
index 0000000..0a13565
--- /dev/null
+++ b/shiny/iconvg/testdata/lod-polygon.64.png
Binary files differ
diff --git a/shiny/iconvg/testdata/lod-polygon.ivg b/shiny/iconvg/testdata/lod-polygon.ivg
new file mode 100644
index 0000000..86a1c1c
--- /dev/null
+++ b/shiny/iconvg/testdata/lod-polygon.ivg
Binary files differ
diff --git a/shiny/iconvg/testdata/lod-polygon.ivg.disassembly b/shiny/iconvg/testdata/lod-polygon.ivg.disassembly
new file mode 100644
index 0000000..32267b3
--- /dev/null
+++ b/shiny/iconvg/testdata/lod-polygon.ivg.disassembly
@@ -0,0 +1,53 @@
+89 49 56 47 IconVG Magic identifier
+00 Number of metadata chunks: 0
+c0 Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+48 -28
+58 -20
+e8 V (absolute vertical lineTo)
+48 -28
+e6 H (absolute horizontal lineTo)
+58 -20
+e1 z (closePath); end path
+c7 Set LOD
+00 +0
+a0 +80
+c0 Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+b8 +28
+80 +0
+01 L (absolute lineTo), 2 reps
+64 -14
+5f fd c1 41 +24.24871
+ L (absolute lineTo), implicit
+64 -14
+5f fd c1 c1 -24.24871
+e1 z (closePath); end path
+c7 Set LOD
+a0 +80
+03 00 80 7f +Inf
+c0 Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+b8 +28
+80 +0
+03 L (absolute lineTo), 4 reps
+8f 70 0a 41 +8.652477
+67 09 d5 41 +26.629585
+ L (absolute lineTo), implicit
+47 38 b5 c1 -22.652473
+f7 a9 83 41 +16.457985
+ L (absolute lineTo), implicit
+47 38 b5 c1 -22.652473
+f7 a9 83 c1 -16.457985
+ L (absolute lineTo), implicit
+8f 70 0a 41 +8.652477
+67 09 d5 c1 -26.629585
+e1 z (closePath); end path
+c7 Set LOD
+00 +0
+03 00 80 7f +Inf
+c0 Start path, filled with CREG[CSEL-0]; M (absolute moveTo)
+b8 +28
+a8 +20
+e8 V (absolute vertical lineTo)
+b8 +28
+e6 H (absolute horizontal lineTo)
+a8 +20
+e1 z (closePath); end path
diff --git a/shiny/iconvg/testdata/lod-polygon.png b/shiny/iconvg/testdata/lod-polygon.png
new file mode 100644
index 0000000..30af4e7
--- /dev/null
+++ b/shiny/iconvg/testdata/lod-polygon.png
Binary files differ