| // 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 vector |
| |
| // TODO: add tests for NaN and Inf coordinates. |
| |
| import ( |
| "fmt" |
| "image" |
| "image/color" |
| "image/draw" |
| "image/png" |
| "math" |
| "os" |
| "path/filepath" |
| "testing" |
| |
| "golang.org/x/image/math/f32" |
| ) |
| |
| // 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 pointOnCircle(center, radius, index, number int) f32.Vec2 { |
| c := float64(center) |
| r := float64(radius) |
| i := float64(index) |
| n := float64(number) |
| return f32.Vec2{ |
| float32(c + r*(math.Cos(2*math.Pi*i/n))), |
| float32(c + r*(math.Sin(2*math.Pi*i/n))), |
| } |
| } |
| |
| func TestRasterizeOutOfBounds(t *testing.T) { |
| // Set this to a non-empty string such as "/tmp" to manually inspect the |
| // rasterization. |
| // |
| // If empty, this test simply checks that calling LineTo with points out of |
| // the rasterizer's bounds doesn't panic. |
| const tmpDir = "" |
| |
| const center, radius, n = 16, 20, 16 |
| var z Rasterizer |
| for i := 0; i < n; i++ { |
| for j := 1; j < n/2; j++ { |
| z.Reset(2*center, 2*center) |
| z.MoveTo(f32.Vec2{1 * center, 1 * center}) |
| z.LineTo(pointOnCircle(center, radius, i+0, n)) |
| z.LineTo(pointOnCircle(center, radius, i+j, n)) |
| z.ClosePath() |
| |
| z.MoveTo(f32.Vec2{0 * center, 0 * center}) |
| z.LineTo(f32.Vec2{0 * center, 2 * center}) |
| z.LineTo(f32.Vec2{2 * center, 2 * center}) |
| z.LineTo(f32.Vec2{2 * center, 0 * center}) |
| z.ClosePath() |
| |
| dst := image.NewAlpha(z.Bounds()) |
| z.Draw(dst, dst.Bounds(), image.Opaque, image.Point{}) |
| |
| if tmpDir == "" { |
| continue |
| } |
| |
| filename := filepath.Join(tmpDir, fmt.Sprintf("out-%02d-%02d.png", i, j)) |
| if err := encodePNG(filename, dst); err != nil { |
| t.Error(err) |
| } |
| t.Logf("wrote %s", filename) |
| } |
| } |
| } |
| |
| func TestRasterizePolygon(t *testing.T) { |
| var z Rasterizer |
| for radius := 4; radius <= 1024; radius *= 2 { |
| for n := 3; n <= 19; n += 4 { |
| z.Reset(2*radius, 2*radius) |
| z.MoveTo(f32.Vec2{ |
| float32(2 * radius), |
| float32(1 * radius), |
| }) |
| for i := 1; i < n; i++ { |
| z.LineTo(pointOnCircle(radius, radius, i, n)) |
| } |
| z.ClosePath() |
| |
| dst := image.NewAlpha(z.Bounds()) |
| z.Draw(dst, dst.Bounds(), image.Opaque, image.Point{}) |
| |
| if err := checkCornersCenter(dst); err != nil { |
| t.Errorf("radius=%d, n=%d: %v", radius, n, err) |
| } |
| } |
| } |
| } |
| |
| func TestRasterizeAlmostAxisAligned(t *testing.T) { |
| z := NewRasterizer(8, 8) |
| z.MoveTo(f32.Vec2{2, 2}) |
| z.LineTo(f32.Vec2{6, math.Nextafter32(2, 0)}) |
| z.LineTo(f32.Vec2{6, 6}) |
| z.LineTo(f32.Vec2{math.Nextafter32(2, 0), 6}) |
| z.ClosePath() |
| |
| dst := image.NewAlpha(z.Bounds()) |
| z.Draw(dst, dst.Bounds(), image.Opaque, image.Point{}) |
| |
| if err := checkCornersCenter(dst); err != nil { |
| t.Error(err) |
| } |
| } |
| |
| // checkCornersCenter checks that the corners of the image are all 0x00 and the |
| // center is 0xff. |
| func checkCornersCenter(m *image.Alpha) error { |
| size := m.Bounds().Size() |
| corners := [4]uint8{ |
| m.Pix[(0*size.Y+0)*m.Stride+(0*size.X+0)], |
| m.Pix[(0*size.Y+0)*m.Stride+(1*size.X-1)], |
| m.Pix[(1*size.Y-1)*m.Stride+(0*size.X+0)], |
| m.Pix[(1*size.Y-1)*m.Stride+(1*size.X-1)], |
| } |
| if corners != [4]uint8{} { |
| return fmt.Errorf("corners were not all zero: %v", corners) |
| } |
| center := m.Pix[(size.Y/2)*m.Stride+(size.X/2)] |
| if center != 0xff { |
| return fmt.Errorf("center: got %#02x, want 0xff", center) |
| } |
| return nil |
| } |
| |
| var basicMask = []byte{ |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xaa, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfa, 0x5f, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x24, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa1, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x14, 0x00, 0x00, |
| 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x4a, 0x00, 0x00, |
| 0x00, 0x00, 0xcc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0x00, 0x00, |
| 0x00, 0x00, 0x66, 0xff, 0xff, 0xff, 0xff, 0xff, 0xef, 0xe4, 0xff, 0xff, 0xff, 0xb6, 0x00, 0x00, |
| 0x00, 0x00, 0x0c, 0xf2, 0xff, 0xff, 0xfe, 0x9e, 0x15, 0x00, 0x15, 0x96, 0xff, 0xce, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x88, 0xfc, 0xe3, 0x43, 0x00, 0x00, 0x00, 0x00, 0x06, 0xcd, 0xdc, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x10, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x25, 0xde, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| } |
| |
| func testBasicPath(t *testing.T, prefix string, dst draw.Image, src image.Image, op draw.Op, want []byte) { |
| z := NewRasterizer(16, 16) |
| z.MoveTo(f32.Vec2{2, 2}) |
| z.LineTo(f32.Vec2{8, 2}) |
| z.QuadTo(f32.Vec2{14, 2}, f32.Vec2{14, 14}) |
| z.CubeTo(f32.Vec2{8, 2}, f32.Vec2{5, 20}, f32.Vec2{2, 8}) |
| z.ClosePath() |
| |
| z.DrawOp = op |
| z.Draw(dst, z.Bounds(), src, image.Point{}) |
| |
| var got []byte |
| switch dst := dst.(type) { |
| case *image.Alpha: |
| got = dst.Pix |
| case *image.RGBA: |
| got = dst.Pix |
| default: |
| t.Errorf("%s: unrecognized dst image type %T", prefix, dst) |
| } |
| |
| if len(got) != len(want) { |
| t.Errorf("%s: len(got)=%d and len(want)=%d differ", prefix, len(got), len(want)) |
| return |
| } |
| for i := range got { |
| delta := int(got[i]) - int(want[i]) |
| // The +/- 2 allows different implementations to give different |
| // rounding errors. |
| if delta < -2 || +2 < delta { |
| t.Errorf("%s: i=%d: got %#02x, want %#02x", prefix, i, got[i], want[i]) |
| return |
| } |
| } |
| } |
| |
| func TestBasicPathDstAlpha(t *testing.T) { |
| for _, background := range []uint8{0x00, 0x80} { |
| for _, op := range []draw.Op{draw.Over, draw.Src} { |
| for _, xPadding := range []int{0, 7} { |
| bounds := image.Rect(0, 0, 16+xPadding, 16) |
| dst := image.NewAlpha(bounds) |
| for i := range dst.Pix { |
| dst.Pix[i] = background |
| } |
| |
| want := make([]byte, len(dst.Pix)) |
| copy(want, dst.Pix) |
| |
| if op == draw.Over && background == 0x80 { |
| for y := 0; y < 16; y++ { |
| for x := 0; x < 16; x++ { |
| ma := basicMask[16*y+x] |
| i := dst.PixOffset(x, y) |
| want[i] = 0xff - (0xff-ma)/2 |
| } |
| } |
| } else { |
| for y := 0; y < 16; y++ { |
| for x := 0; x < 16; x++ { |
| ma := basicMask[16*y+x] |
| i := dst.PixOffset(x, y) |
| want[i] = ma |
| } |
| } |
| } |
| |
| prefix := fmt.Sprintf("background=%#02x, op=%v, xPadding=%d", background, op, xPadding) |
| testBasicPath(t, prefix, dst, image.Opaque, op, want) |
| } |
| } |
| } |
| } |
| |
| func TestBasicPathDstRGBA(t *testing.T) { |
| blue := image.NewUniform(color.RGBA{0x00, 0x00, 0xff, 0xff}) |
| |
| for _, op := range []draw.Op{draw.Over, draw.Src} { |
| for _, xPadding := range []int{0, 7} { |
| bounds := image.Rect(0, 0, 16+xPadding, 16) |
| dst := image.NewRGBA(bounds) |
| for y := bounds.Min.Y; y < bounds.Max.Y; y++ { |
| for x := bounds.Min.X; x < bounds.Max.X; x++ { |
| dst.SetRGBA(x, y, color.RGBA{ |
| R: uint8(y * 0x07), |
| G: uint8(x * 0x05), |
| B: 0x00, |
| A: 0x80, |
| }) |
| } |
| } |
| |
| want := make([]byte, len(dst.Pix)) |
| copy(want, dst.Pix) |
| |
| if op == draw.Over { |
| for y := 0; y < 16; y++ { |
| for x := 0; x < 16; x++ { |
| ma := basicMask[16*y+x] |
| i := dst.PixOffset(x, y) |
| want[i+0] = uint8((uint32(0xff-ma) * uint32(y*0x07)) / 0xff) |
| want[i+1] = uint8((uint32(0xff-ma) * uint32(x*0x05)) / 0xff) |
| want[i+2] = ma |
| want[i+3] = ma/2 + 0x80 |
| } |
| } |
| } else { |
| for y := 0; y < 16; y++ { |
| for x := 0; x < 16; x++ { |
| ma := basicMask[16*y+x] |
| i := dst.PixOffset(x, y) |
| want[i+0] = 0x00 |
| want[i+1] = 0x00 |
| want[i+2] = ma |
| want[i+3] = ma |
| } |
| } |
| } |
| |
| prefix := fmt.Sprintf("op=%v, xPadding=%d", op, xPadding) |
| testBasicPath(t, prefix, dst, blue, op, want) |
| } |
| } |
| } |
| |
| const ( |
| benchmarkGlyphWidth = 893 |
| benchmarkGlyphHeight = 1122 |
| ) |
| |
| type benchmarkGlyphDatum struct { |
| // n being 0, 1 or 2 means moveTo, lineTo or quadTo. |
| n uint32 |
| p f32.Vec2 |
| q f32.Vec2 |
| } |
| |
| // benchmarkGlyphData is the 'a' glyph from the Roboto Regular font, translated |
| // so that its top left corner is (0, 0). |
| var benchmarkGlyphData = []benchmarkGlyphDatum{ |
| {0, f32.Vec2{699, 1102}, f32.Vec2{0, 0}}, |
| {2, f32.Vec2{683, 1070}, f32.Vec2{673, 988}}, |
| {2, f32.Vec2{544, 1122}, f32.Vec2{365, 1122}}, |
| {2, f32.Vec2{205, 1122}, f32.Vec2{102.5, 1031.5}}, |
| {2, f32.Vec2{0, 941}, f32.Vec2{0, 802}}, |
| {2, f32.Vec2{0, 633}, f32.Vec2{128.5, 539.5}}, |
| {2, f32.Vec2{257, 446}, f32.Vec2{490, 446}}, |
| {1, f32.Vec2{670, 446}, f32.Vec2{0, 0}}, |
| {1, f32.Vec2{670, 361}, f32.Vec2{0, 0}}, |
| {2, f32.Vec2{670, 264}, f32.Vec2{612, 206.5}}, |
| {2, f32.Vec2{554, 149}, f32.Vec2{441, 149}}, |
| {2, f32.Vec2{342, 149}, f32.Vec2{275, 199}}, |
| {2, f32.Vec2{208, 249}, f32.Vec2{208, 320}}, |
| {1, f32.Vec2{22, 320}, f32.Vec2{0, 0}}, |
| {2, f32.Vec2{22, 239}, f32.Vec2{79.5, 163.5}}, |
| {2, f32.Vec2{137, 88}, f32.Vec2{235.5, 44}}, |
| {2, f32.Vec2{334, 0}, f32.Vec2{452, 0}}, |
| {2, f32.Vec2{639, 0}, f32.Vec2{745, 93.5}}, |
| {2, f32.Vec2{851, 187}, f32.Vec2{855, 351}}, |
| {1, f32.Vec2{855, 849}, f32.Vec2{0, 0}}, |
| {2, f32.Vec2{855, 998}, f32.Vec2{893, 1086}}, |
| {1, f32.Vec2{893, 1102}, f32.Vec2{0, 0}}, |
| {1, f32.Vec2{699, 1102}, f32.Vec2{0, 0}}, |
| {0, f32.Vec2{392, 961}, f32.Vec2{0, 0}}, |
| {2, f32.Vec2{479, 961}, f32.Vec2{557, 916}}, |
| {2, f32.Vec2{635, 871}, f32.Vec2{670, 799}}, |
| {1, f32.Vec2{670, 577}, f32.Vec2{0, 0}}, |
| {1, f32.Vec2{525, 577}, f32.Vec2{0, 0}}, |
| {2, f32.Vec2{185, 577}, f32.Vec2{185, 776}}, |
| {2, f32.Vec2{185, 863}, f32.Vec2{243, 912}}, |
| {2, f32.Vec2{301, 961}, f32.Vec2{392, 961}}, |
| } |
| |
| func scaledBenchmarkGlyphData(height int) (width int, data []benchmarkGlyphDatum) { |
| scale := float32(height) / benchmarkGlyphHeight |
| |
| // Clone the benchmarkGlyphData slice and scale its coordinates. |
| data = append(data, benchmarkGlyphData...) |
| for i := range data { |
| data[i].p[0] *= scale |
| data[i].p[1] *= scale |
| data[i].q[0] *= scale |
| data[i].q[1] *= scale |
| } |
| |
| return int(math.Ceil(float64(benchmarkGlyphWidth * scale))), data |
| } |
| |
| // benchGlyph benchmarks rasterizing a TrueType glyph. |
| // |
| // Note that, compared to the github.com/google/font-go prototype, the height |
| // here is the height of the bounding box, not the pixels per em used to scale |
| // a glyph's vectors. A height of 64 corresponds to a ppem greater than 64. |
| func benchGlyph(b *testing.B, colorModel byte, loose bool, height int, op draw.Op) { |
| width, data := scaledBenchmarkGlyphData(height) |
| z := NewRasterizer(width, height) |
| |
| bounds := z.Bounds() |
| if loose { |
| bounds.Max.X++ |
| } |
| dst, src := draw.Image(nil), image.Image(nil) |
| switch colorModel { |
| case 'A': |
| dst = image.NewAlpha(bounds) |
| src = image.Opaque |
| case 'N': |
| dst = image.NewNRGBA(bounds) |
| src = image.NewUniform(color.NRGBA{0x40, 0x80, 0xc0, 0xff}) |
| case 'R': |
| dst = image.NewRGBA(bounds) |
| src = image.NewUniform(color.RGBA{0x40, 0x80, 0xc0, 0xff}) |
| default: |
| b.Fatal("unsupported color model") |
| } |
| bounds = z.Bounds() |
| |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| z.Reset(width, height) |
| z.DrawOp = op |
| for _, d := range data { |
| switch d.n { |
| case 0: |
| z.MoveTo(d.p) |
| case 1: |
| z.LineTo(d.p) |
| case 2: |
| z.QuadTo(d.p, d.q) |
| } |
| } |
| z.Draw(dst, bounds, src, image.Point{}) |
| } |
| } |
| |
| func BenchmarkGlyphAlpha16Over(b *testing.B) { benchGlyph(b, 'A', false, 16, draw.Over) } |
| func BenchmarkGlyphAlpha16Src(b *testing.B) { benchGlyph(b, 'A', false, 16, draw.Src) } |
| func BenchmarkGlyphAlpha32Over(b *testing.B) { benchGlyph(b, 'A', false, 32, draw.Over) } |
| func BenchmarkGlyphAlpha32Src(b *testing.B) { benchGlyph(b, 'A', false, 32, draw.Src) } |
| func BenchmarkGlyphAlpha64Over(b *testing.B) { benchGlyph(b, 'A', false, 64, draw.Over) } |
| func BenchmarkGlyphAlpha64Src(b *testing.B) { benchGlyph(b, 'A', false, 64, draw.Src) } |
| func BenchmarkGlyphAlpha128Over(b *testing.B) { benchGlyph(b, 'A', false, 128, draw.Over) } |
| func BenchmarkGlyphAlpha128Src(b *testing.B) { benchGlyph(b, 'A', false, 128, draw.Src) } |
| func BenchmarkGlyphAlpha256Over(b *testing.B) { benchGlyph(b, 'A', false, 256, draw.Over) } |
| func BenchmarkGlyphAlpha256Src(b *testing.B) { benchGlyph(b, 'A', false, 256, draw.Src) } |
| |
| func BenchmarkGlyphAlphaLoose16Over(b *testing.B) { benchGlyph(b, 'A', true, 16, draw.Over) } |
| func BenchmarkGlyphAlphaLoose16Src(b *testing.B) { benchGlyph(b, 'A', true, 16, draw.Src) } |
| func BenchmarkGlyphAlphaLoose32Over(b *testing.B) { benchGlyph(b, 'A', true, 32, draw.Over) } |
| func BenchmarkGlyphAlphaLoose32Src(b *testing.B) { benchGlyph(b, 'A', true, 32, draw.Src) } |
| func BenchmarkGlyphAlphaLoose64Over(b *testing.B) { benchGlyph(b, 'A', true, 64, draw.Over) } |
| func BenchmarkGlyphAlphaLoose64Src(b *testing.B) { benchGlyph(b, 'A', true, 64, draw.Src) } |
| func BenchmarkGlyphAlphaLoose128Over(b *testing.B) { benchGlyph(b, 'A', true, 128, draw.Over) } |
| func BenchmarkGlyphAlphaLoose128Src(b *testing.B) { benchGlyph(b, 'A', true, 128, draw.Src) } |
| func BenchmarkGlyphAlphaLoose256Over(b *testing.B) { benchGlyph(b, 'A', true, 256, draw.Over) } |
| func BenchmarkGlyphAlphaLoose256Src(b *testing.B) { benchGlyph(b, 'A', true, 256, draw.Src) } |
| |
| func BenchmarkGlyphRGBA16Over(b *testing.B) { benchGlyph(b, 'R', false, 16, draw.Over) } |
| func BenchmarkGlyphRGBA16Src(b *testing.B) { benchGlyph(b, 'R', false, 16, draw.Src) } |
| func BenchmarkGlyphRGBA32Over(b *testing.B) { benchGlyph(b, 'R', false, 32, draw.Over) } |
| func BenchmarkGlyphRGBA32Src(b *testing.B) { benchGlyph(b, 'R', false, 32, draw.Src) } |
| func BenchmarkGlyphRGBA64Over(b *testing.B) { benchGlyph(b, 'R', false, 64, draw.Over) } |
| func BenchmarkGlyphRGBA64Src(b *testing.B) { benchGlyph(b, 'R', false, 64, draw.Src) } |
| func BenchmarkGlyphRGBA128Over(b *testing.B) { benchGlyph(b, 'R', false, 128, draw.Over) } |
| func BenchmarkGlyphRGBA128Src(b *testing.B) { benchGlyph(b, 'R', false, 128, draw.Src) } |
| func BenchmarkGlyphRGBA256Over(b *testing.B) { benchGlyph(b, 'R', false, 256, draw.Over) } |
| func BenchmarkGlyphRGBA256Src(b *testing.B) { benchGlyph(b, 'R', false, 256, draw.Src) } |
| |
| func BenchmarkGlyphNRGBA16Over(b *testing.B) { benchGlyph(b, 'N', false, 16, draw.Over) } |
| func BenchmarkGlyphNRGBA16Src(b *testing.B) { benchGlyph(b, 'N', false, 16, draw.Src) } |
| func BenchmarkGlyphNRGBA32Over(b *testing.B) { benchGlyph(b, 'N', false, 32, draw.Over) } |
| func BenchmarkGlyphNRGBA32Src(b *testing.B) { benchGlyph(b, 'N', false, 32, draw.Src) } |
| func BenchmarkGlyphNRGBA64Over(b *testing.B) { benchGlyph(b, 'N', false, 64, draw.Over) } |
| func BenchmarkGlyphNRGBA64Src(b *testing.B) { benchGlyph(b, 'N', false, 64, draw.Src) } |
| func BenchmarkGlyphNRGBA128Over(b *testing.B) { benchGlyph(b, 'N', false, 128, draw.Over) } |
| func BenchmarkGlyphNRGBA128Src(b *testing.B) { benchGlyph(b, 'N', false, 128, draw.Src) } |
| func BenchmarkGlyphNRGBA256Over(b *testing.B) { benchGlyph(b, 'N', false, 256, draw.Over) } |
| func BenchmarkGlyphNRGBA256Src(b *testing.B) { benchGlyph(b, 'N', false, 256, draw.Src) } |