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