From 26744b46ffa9ba8fffe608f8091faf9d2f2e77a7 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 20 Apr 2024 17:45:08 +0900 Subject: [PATCH] text/v2: use embedded bitmap glyphs whenever possible Closes #2956 --- internal/vettools/imageimportcheck.go | 3 + text/v2/gotext.go | 26 +++++--- text/v2/gotextfacesource.go | 89 ++++++++++++++++++++++++--- 3 files changed, 101 insertions(+), 17 deletions(-) diff --git a/internal/vettools/imageimportcheck.go b/internal/vettools/imageimportcheck.go index 7ec40cb9f..1d1c6e529 100644 --- a/internal/vettools/imageimportcheck.go +++ b/internal/vettools/imageimportcheck.go @@ -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" { diff --git a/text/v2/gotext.go b/text/v2/gotext.go index 65b036180..246990905 100644 --- a/text/v2/gotext.go +++ b/text/v2/gotext.go @@ -328,18 +328,27 @@ func (g *GoTextFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffse } func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) { - if g.direction().isHorizontal() { - origin.X = adjustGranularity(origin.X, g) - origin.Y &^= ((1 << 6) - 1) + if glyph.bitmap != nil { + if g.direction().isHorizontal() { + origin.X = adjustGranularity(origin.X, g) + origin.Y &^= ((1 << 6) - 1) + } else { + origin.X &^= ((1 << 6) - 1) + origin.Y = adjustGranularity(origin.Y, g) + } } else { origin.X &^= ((1 << 6) - 1) - origin.Y = adjustGranularity(origin.Y, g) + origin.Y &^= ((1 << 6) - 1) } b := glyph.bounds - subpixelOffset := fixed.Point26_6{ - X: (origin.X + b.Min.X) & ((1 << 6) - 1), - Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1), + + 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, @@ -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) }) diff --git a/text/v2/gotextfacesource.go b/text/v2/gotextfacesource.go index 3e84adde6..e84702105 100644 --- a/text/v2/gotextfacesource.go +++ b/text/v2/gotextfacesource.go @@ -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 }