text: Bug fix: Treat negative kernings correctly

Fixes #1378
This commit is contained in:
Hajime Hoshi 2020-10-03 20:00:12 +09:00
parent f731c563e4
commit 88aeca7392
2 changed files with 106 additions and 48 deletions

View File

@ -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

View File

@ -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)
}
}
}
}