From 88aeca7392423dd9f011a7deaac3f8ca793c3a62 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 3 Oct 2020 20:00:12 +0900 Subject: [PATCH] text: Bug fix: Treat negative kernings correctly Fixes #1378 --- text/text.go | 61 +++++++------------------------ text/text_test.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 48 deletions(-) diff --git a/text/text.go b/text/text.go index 7c0847274..ed68e31fc 100644 --- a/text/text.go +++ b/text/text.go @@ -47,16 +47,16 @@ const ( cacheLimit = 512 // This is an arbitrary number. ) -func drawGlyph(dst *ebiten.Image, face font.Face, r rune, img *glyphImage, x, y fixed.Int26_6, clr ebiten.ColorM) { +func drawGlyph(dst *ebiten.Image, face font.Face, r rune, img *ebiten.Image, x, y fixed.Int26_6, clr ebiten.ColorM) { if img == nil { return } b := getGlyphBounds(face, r) op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(fixed26_6ToFloat64(x+b.Min.X), fixed26_6ToFloat64(y+b.Min.Y)) + op.GeoM.Translate(fixed26_6ToFloat64((x + b.Min.X)), fixed26_6ToFloat64((y + b.Min.Y))) op.ColorM = clr - _ = dst.DrawImage(img.image.SubImage(image.Rect(img.x, img.y, img.x+img.width, img.y+img.height)).(*ebiten.Image), op) + _ = dst.DrawImage(img, op) } var ( @@ -76,16 +76,8 @@ func getGlyphBounds(face font.Face, r rune) *fixed.Rectangle26_6 { return &b } -type glyphImage struct { - image *ebiten.Image - x int - y int - width int - height int -} - type glyphImageCacheEntry struct { - image *glyphImage + image *ebiten.Image atime int64 } @@ -94,7 +86,7 @@ var ( emptyGlyphs = map[font.Face]map[rune]struct{}{} ) -func getGlyphImages(face font.Face, runes []rune) []*glyphImage { +func getGlyphImages(face font.Face, runes []rune) []*ebiten.Image { if _, ok := emptyGlyphs[face]; !ok { emptyGlyphs[face] = map[rune]struct{}{} } @@ -102,7 +94,7 @@ func getGlyphImages(face font.Face, runes []rune) []*glyphImage { glyphImageCache[face] = map[rune]*glyphImageCacheEntry{} } - imgs := make([]*glyphImage, len(runes)) + imgs := make([]*ebiten.Image, len(runes)) glyphBounds := map[rune]*fixed.Rectangle26_6{} neededGlyphIndices := map[int]rune{} for i, r := range runes { @@ -141,54 +133,27 @@ func getGlyphImages(face font.Face, runes []rune) []*glyphImage { } if len(neededGlyphIndices) > 0 { - // TODO: What if w2 is too big (e.g. > 4096)? - w2 := 0 - h2 := 0 - for _, b := range glyphBounds { + for i, r := range neededGlyphIndices { + b := glyphBounds[r] w, h := (b.Max.X - b.Min.X).Ceil(), (b.Max.Y - b.Min.Y).Ceil() - w2 += w - if h2 < h { - h2 = h - } - } - rgba := image.NewRGBA(image.Rect(0, 0, w2, h2)) - - x := 0 - xs := map[rune]int{} - for r, b := range glyphBounds { - w := (b.Max.X - b.Min.X).Ceil() + rgba := image.NewRGBA(image.Rect(0, 0, w, h)) d := font.Drawer{ Dst: rgba, Src: image.White, Face: face, } - d.Dot = fixed.Point26_6{X: fixed.I(x) - b.Min.X, Y: -b.Min.Y} + d.Dot = fixed.Point26_6{X: -b.Min.X, Y: -b.Min.Y} d.DrawString(string(r)) - xs[r] = x - x += w - } - - img, _ := ebiten.NewImageFromImage(rgba, ebiten.FilterDefault) - for i, r := range neededGlyphIndices { - b := glyphBounds[r] - w, h := (b.Max.X - b.Min.X).Ceil(), (b.Max.Y - b.Min.Y).Ceil() - - g := &glyphImage{ - image: img, - x: xs[r], - y: 0, - width: w, - height: h, - } + img, _ := ebiten.NewImageFromImage(rgba, ebiten.FilterDefault) if _, ok := glyphImageCache[face][r]; !ok { glyphImageCache[face][r] = &glyphImageCacheEntry{ - image: g, + image: img, atime: now(), } } - imgs[i] = g + imgs[i] = img } } return imgs diff --git a/text/text_test.go b/text/text_test.go index 11c99801f..f7cea9014 100644 --- a/text/text_test.go +++ b/text/text_test.go @@ -15,10 +15,13 @@ package text_test import ( + "image" "image/color" "testing" "github.com/hajimehoshi/bitmapfont" + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" "github.com/hajimehoshi/ebiten" t "github.com/hajimehoshi/ebiten/internal/testing" @@ -53,3 +56,93 @@ func TestTextColor(t *testing.T) { t.Fail() } } + +const testFaceSize = 6 + +type testFace struct{} + +func (f *testFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { + dr = image.Rect(0, 0, testFaceSize, testFaceSize) + a := image.NewAlpha(dr) + switch r { + case 'a': + for j := 0; j < testFaceSize; j++ { + for i := 0; i < testFaceSize; i++ { + a.SetAlpha(i, j, color.Alpha{0x80}) + } + } + case 'b': + for j := 0; j < testFaceSize; j++ { + for i := 0; i < testFaceSize; i++ { + a.SetAlpha(i, j, color.Alpha{0xff}) + } + } + } + mask = a + advance = fixed.I(testFaceSize) + ok = true + return +} + +func (f *testFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { + bounds = fixed.R(0, 0, testFaceSize, testFaceSize) + advance = fixed.I(testFaceSize) + ok = true + return +} + +func (f *testFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) { + return fixed.I(testFaceSize), true +} + +func (f *testFace) Kern(r0, r1 rune) fixed.Int26_6 { + if r1 == 'b' { + return fixed.I(-testFaceSize) + } + return 0 +} + +func (f *testFace) Close() error { + return nil +} + +func (f *testFace) Metrics() font.Metrics { + return font.Metrics{ + Height: fixed.I(testFaceSize), + Ascent: fixed.I(testFaceSize), + Descent: 0, + XHeight: 0, + CapHeight: fixed.I(testFaceSize), + CaretSlope: image.Pt(0, 1), + } +} + +func TestTextOverlap(t *testing.T) { + f := &testFace{} + dst, _ := ebiten.NewImage(testFaceSize*2, testFaceSize, ebiten.FilterDefault) + + // With testFace, 'b' is rendered at the previous position as 0xff. + // 'a' is rendered at the current position as 0x80. + Draw(dst, "ab", f, 0, 0, color.White) + for j := 0; j < testFaceSize; j++ { + for i := 0; i < testFaceSize; i++ { + got := dst.At(i, j) + want := color.RGBA{0xff, 0xff, 0xff, 0xff} + if got != want { + t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } + + // The glyph 'a' should be treated correctly. + Draw(dst, "a", f, testFaceSize, 0, color.White) + for j := 0; j < testFaceSize; j++ { + for i := testFaceSize; i < testFaceSize*2; i++ { + got := dst.At(i, j) + want := color.RGBA{0x80, 0x80, 0x80, 0x80} + if got != want { + t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +}