text/v2: use embedded bitmap glyphs whenever possible

Closes #2956
This commit is contained in:
Hajime Hoshi 2024-04-20 17:45:08 +09:00
parent bf7acd54bb
commit 26744b46ff
3 changed files with 101 additions and 17 deletions

View File

@ -51,6 +51,9 @@ func runImageImportCheck(pass *analysis.Pass) (any, error) {
if strings.HasSuffix(pkgPath, "_test") { if strings.HasSuffix(pkgPath, "_test") {
return nil, nil return nil, nil
} }
if pkgPath == "github.com/hajimehoshi/ebiten/v2/text/v2" {
return nil, nil
}
// TODO: Remove this exception after v3 is released (#2336). // TODO: Remove this exception after v3 is released (#2336).
if pkgPath == "github.com/hajimehoshi/ebiten/v2/ebitenutil" { if pkgPath == "github.com/hajimehoshi/ebiten/v2/ebitenutil" {

View File

@ -328,6 +328,7 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse
} }
func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) { func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) {
if glyph.bitmap != nil {
if g.direction().isHorizontal() { if g.direction().isHorizontal() {
origin.X = adjustGranularity(origin.X, g) origin.X = adjustGranularity(origin.X, g)
origin.Y &^= ((1 << 6) - 1) origin.Y &^= ((1 << 6) - 1)
@ -335,12 +336,20 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im
origin.X &^= ((1 << 6) - 1) origin.X &^= ((1 << 6) - 1)
origin.Y = adjustGranularity(origin.Y, g) origin.Y = adjustGranularity(origin.Y, g)
} }
} else {
origin.X &^= ((1 << 6) - 1)
origin.Y &^= ((1 << 6) - 1)
}
b := glyph.bounds b := glyph.bounds
subpixelOffset := fixed.Point26_6{
var subpixelOffset fixed.Point26_6
if glyph.bitmap != nil {
subpixelOffset = fixed.Point26_6{
X: (origin.X + b.Min.X) & ((1 << 6) - 1), X: (origin.X + b.Min.X) & ((1 << 6) - 1),
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1), Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
} }
}
key := goTextGlyphImageCacheKey{ key := goTextGlyphImageCacheKey{
gid: glyph.shapingGlyph.GlyphID, gid: glyph.shapingGlyph.GlyphID,
xoffset: subpixelOffset.X, xoffset: subpixelOffset.X,
@ -348,6 +357,9 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im
variations: g.ensureVariationsString(), variations: g.ensureVariationsString(),
} }
img := g.Source.getOrCreateGlyphImage(g, key, func() *ebiten.Image { img := g.Source.getOrCreateGlyphImage(g, key, func() *ebiten.Image {
if glyph.bitmap != nil {
return ebiten.NewImageFromImage(glyph.bitmap)
}
return segmentsToImage(glyph.scaledSegments, subpixelOffset, b) return segmentsToImage(glyph.scaledSegments, subpixelOffset, b)
}) })

View File

@ -16,6 +16,9 @@ package text
import ( import (
"bytes" "bytes"
"image"
"image/jpeg"
"image/png" // As typesettings/font already imports image/png, it is fine to ignore side effects (#2336).
"io" "io"
"sync" "sync"
@ -26,6 +29,7 @@ import (
"github.com/go-text/typesetting/opentype/loader" "github.com/go-text/typesetting/opentype/loader"
"github.com/go-text/typesetting/shaping" "github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
"golang.org/x/image/tiff"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
@ -46,6 +50,7 @@ type glyph struct {
endIndex int endIndex int
scaledSegments []api.Segment scaledSegments []api.Segment
bounds fixed.Rectangle26_6 bounds fixed.Rectangle26_6
bitmap image.Image
} }
type goTextOutputCacheValue struct { type goTextOutputCacheValue struct {
@ -73,6 +78,9 @@ type GoTextFaceSource struct {
shaper shaping.HarfbuzzShaper shaper shaping.HarfbuzzShaper
bitmapSizesResult []api.BitmapSize
bitmapSizesOnce sync.Once
m sync.Mutex m sync.Mutex
} }
@ -180,6 +188,17 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Outpu
f := face.Source.f f := face.Source.f
f.SetVariations(face.variations) f.SetVariations(face.variations)
f.XPpem = 0
f.YPpem = 0
var useBitmap bool
for _, bs := range g.bitmapSizes() {
if float64(bs.YPpem) == face.Size {
f.XPpem = bs.XPpem
f.YPpem = bs.YPpem
useBitmap = true
break
}
}
runes := []rune(text) runes := []rune(text)
input := shaping.Input{ input := shaping.Input{
@ -219,22 +238,63 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Outpu
indices = append(indices, len(text)) indices = append(indices, len(text))
for _, gl := range out.Glyphs { for _, gl := range out.Glyphs {
gl := gl shapingGlyph := gl
var segs []api.Segment var segs []api.Segment
switch data := g.f.GlyphData(gl.GlyphID).(type) { var bitmap image.Image
switch data := g.f.GlyphData(shapingGlyph.GlyphID).(type) {
case api.GlyphOutline: case api.GlyphOutline:
if out.Direction.IsSideways() { if out.Direction.IsSideways() {
data.Sideways(fixed26_6ToFloat32(-gl.YOffset) / fixed26_6ToFloat32(out.Size) * float32(f.Upem())) data.Sideways(fixed26_6ToFloat32(-shapingGlyph.YOffset) / fixed26_6ToFloat32(out.Size) * float32(f.Upem()))
} }
segs = data.Segments segs = data.Segments
case api.GlyphSVG: case api.GlyphSVG:
segs = data.Outline.Segments segs = data.Outline.Segments
case api.GlyphBitmap: case api.GlyphBitmap:
if useBitmap {
switch data.Format {
case api.BlackAndWhite:
img := image.NewAlpha(image.Rect(0, 0, data.Width, data.Height))
for j := 0; j < data.Height; j++ {
for i := 0; i < data.Width; i++ {
idx := j*data.Width + i
if data.Data[idx/8]&(1<<(7-idx%8)) != 0 {
img.Pix[j*img.Stride+i] = 0xff
}
}
}
bitmap = img
case api.PNG:
img, err := png.Decode(bytes.NewReader(data.Data))
if err != nil {
break
}
bitmap = img
case api.JPG:
img, err := jpeg.Decode(bytes.NewReader(data.Data))
if err != nil {
break
}
bitmap = img
case api.TIFF:
img, err := tiff.Decode(bytes.NewReader(data.Data))
if err != nil {
break
}
bitmap = img
}
}
// Use outline segments in any cases for vector rendering.
if data.Outline != nil { if data.Outline != nil {
segs = data.Outline.Segments segs = data.Outline.Segments
} }
} }
gl := glyph{
startIndex: indices[shapingGlyph.ClusterIndex],
endIndex: indices[shapingGlyph.ClusterIndex+shapingGlyph.RuneCount],
}
scaledSegs := make([]api.Segment, len(segs)) scaledSegs := make([]api.Segment, len(segs))
scale := float32(g.scale(fixed26_6ToFloat64(out.Size))) scale := float32(g.scale(fixed26_6ToFloat64(out.Size)))
for i, seg := range segs { for i, seg := range segs {
@ -245,13 +305,15 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Outpu
} }
} }
gs = append(gs, glyph{ gl.shapingGlyph = &shapingGlyph
shapingGlyph: &gl, gl.scaledSegments = scaledSegs
startIndex: indices[gl.ClusterIndex], gl.bounds = segmentsToBounds(scaledSegs)
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
scaledSegments: scaledSegs, if bitmap != nil {
bounds: segmentsToBounds(scaledSegs), gl.bitmap = bitmap
}) }
gs = append(gs, gl)
} }
} }
@ -292,6 +354,13 @@ func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goT
return g.glyphImageCache[goTextFace.Size].getOrCreate(goTextFace, key, create) return g.glyphImageCache[goTextFace.Size].getOrCreate(goTextFace, key, create)
} }
func (g *GoTextFaceSource) bitmapSizes() []api.BitmapSize {
g.bitmapSizesOnce.Do(func() {
g.bitmapSizesResult = g.f.BitmapSizes()
})
return g.bitmapSizesResult
}
type singleFontmap struct { type singleFontmap struct {
face font.Face face font.Face
} }