Skip to content

Commit

Permalink
text/v2: use embedded bitmap glyphs whenever possible
Browse files Browse the repository at this point in the history
Closes #2956
  • Loading branch information
hajimehoshi committed Apr 21, 2024
1 parent bf7acd5 commit 4465280
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 17 deletions.
26 changes: 19 additions & 7 deletions text/v2/gotext.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
})

Expand Down
89 changes: 79 additions & 10 deletions text/v2/gotextfacesource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"
)
Expand All @@ -46,6 +50,7 @@ type glyph struct {
endIndex int
scaledSegments []api.Segment
bounds fixed.Rectangle26_6
bitmap image.Image
}

type goTextOutputCacheValue struct {
Expand Down Expand Up @@ -73,6 +78,9 @@ type GoTextFaceSource struct {

shaper shaping.HarfbuzzShaper

bitmapSizesResult []api.BitmapSize
bitmapSizesOnce sync.Once

m sync.Mutex
}

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit 4465280

Please sign in to comment.