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") {
return nil, nil
}
if pkgPath == "github.com/hajimehoshi/ebiten/v2/text/v2" {
return nil, nil
}
// TODO: Remove this exception after v3 is released (#2336).
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) {
if glyph.bitmap != nil {
if g.direction().isHorizontal() {
origin.X = adjustGranularity(origin.X, g)
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.Y = adjustGranularity(origin.Y, g)
}
} else {
origin.X &^= ((1 << 6) - 1)
origin.Y &^= ((1 << 6) - 1)
}
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),
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
}
}
key := goTextGlyphImageCacheKey{
gid: glyph.shapingGlyph.GlyphID,
xoffset: subpixelOffset.X,
@ -348,6 +357,9 @@ func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Im
variations: g.ensureVariationsString(),
}
img := g.Source.getOrCreateGlyphImage(g, key, func() *ebiten.Image {
if glyph.bitmap != nil {
return ebiten.NewImageFromImage(glyph.bitmap)
}
return segmentsToImage(glyph.scaledSegments, subpixelOffset, b)
})

View File

@ -16,6 +16,9 @@ package text
import (
"bytes"
"image"
"image/jpeg"
"image/png" // As typesettings/font already imports image/png, it is fine to ignore side effects (#2336).
"io"
"sync"
@ -26,6 +29,7 @@ import (
"github.com/go-text/typesetting/opentype/loader"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
"golang.org/x/image/tiff"
"github.com/hajimehoshi/ebiten/v2"
)
@ -46,6 +50,7 @@ type glyph struct {
endIndex int
scaledSegments []api.Segment
bounds fixed.Rectangle26_6
bitmap image.Image
}
type goTextOutputCacheValue struct {
@ -73,6 +78,9 @@ type GoTextFaceSource struct {
shaper shaping.HarfbuzzShaper
bitmapSizesResult []api.BitmapSize
bitmapSizesOnce sync.Once
m sync.Mutex
}
@ -180,6 +188,17 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Outpu
f := face.Source.f
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)
input := shaping.Input{
@ -219,22 +238,63 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Outpu
indices = append(indices, len(text))
for _, gl := range out.Glyphs {
gl := gl
shapingGlyph := gl
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:
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
case api.GlyphSVG:
segs = data.Outline.Segments
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 {
segs = data.Outline.Segments
}
}
gl := glyph{
startIndex: indices[shapingGlyph.ClusterIndex],
endIndex: indices[shapingGlyph.ClusterIndex+shapingGlyph.RuneCount],
}
scaledSegs := make([]api.Segment, len(segs))
scale := float32(g.scale(fixed26_6ToFloat64(out.Size)))
for i, seg := range segs {
@ -245,13 +305,15 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Outpu
}
}
gs = append(gs, glyph{
shapingGlyph: &gl,
startIndex: indices[gl.ClusterIndex],
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
scaledSegments: scaledSegs,
bounds: segmentsToBounds(scaledSegs),
})
gl.shapingGlyph = &shapingGlyph
gl.scaledSegments = scaledSegs
gl.bounds = segmentsToBounds(scaledSegs)
if bitmap != nil {
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)
}
func (g *GoTextFaceSource) bitmapSizes() []api.BitmapSize {
g.bitmapSizesOnce.Do(func() {
g.bitmapSizesResult = g.f.BitmapSizes()
})
return g.bitmapSizesResult
}
type singleFontmap struct {
face font.Face
}