ebiten/text/v2/gotextfacesource.go

304 lines
7.4 KiB
Go
Raw Normal View History

// Copyright 2023 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package text
import (
"bytes"
"io"
2024-10-27 10:39:10 +01:00
"slices"
"sync"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
)
type goTextOutputCacheKey struct {
text string
direction Direction
size float64
language string
script string
variations string
features string
}
type glyph struct {
shapingGlyph *shaping.Glyph
startIndex int
endIndex int
scaledSegments []opentype.Segment
bounds fixed.Rectangle26_6
}
type goTextOutputCacheValue struct {
outputs []shaping.Output
glyphs []glyph
atime int64
}
type goTextGlyphImageCacheKey struct {
gid opentype.GID
xoffset fixed.Int26_6
yoffset fixed.Int26_6
variations string
}
// GoTextFaceSource is a source of a GoTextFace. This can be shared by multiple GoTextFace objects.
type GoTextFaceSource struct {
f *font.Face
metadata Metadata
outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue
glyphImageCache map[float64]*glyphImageCache[goTextGlyphImageCacheKey]
addr *GoTextFaceSource
shaper shaping.HarfbuzzShaper
m sync.Mutex
}
func toFontResource(source io.Reader) (font.Resource, error) {
// font.Resource has io.Seeker and io.ReaderAt in addition to io.Reader.
// If source has it, use it as it is.
if s, ok := source.(font.Resource); ok {
return s, nil
}
// Read all the bytes and convert this to bytes.Reader.
// This is a very rough solution, but it works.
// TODO: Implement io.ReaderAt in a more efficient way when source is io.Seeker.
bs, err := io.ReadAll(source)
if err != nil {
return nil, err
}
return bytes.NewReader(bs), nil
}
// NewGoTextFaceSource parses an OpenType or TrueType font and returns a GoTextFaceSource object.
func NewGoTextFaceSource(source io.Reader) (*GoTextFaceSource, error) {
src, err := toFontResource(source)
if err != nil {
return nil, err
}
l, err := opentype.NewLoader(src)
if err != nil {
return nil, err
}
f, err := font.NewFont(l)
if err != nil {
return nil, err
}
s := &GoTextFaceSource{
f: font.NewFace(f),
}
s.addr = s
s.metadata = metadataFromLoader(l)
return s, nil
}
// NewGoTextFaceSourcesFromCollection parses an OpenType or TrueType font collection and returns a slice of GoTextFaceSource objects.
func NewGoTextFaceSourcesFromCollection(source io.Reader) ([]*GoTextFaceSource, error) {
src, err := toFontResource(source)
if err != nil {
return nil, err
}
ls, err := opentype.NewLoaders(src)
if err != nil {
return nil, err
}
sources := make([]*GoTextFaceSource, len(ls))
for i, l := range ls {
f, err := font.NewFont(l)
if err != nil {
return nil, err
}
s := &GoTextFaceSource{
f: &font.Face{Font: f},
}
s.addr = s
s.metadata = metadataFromLoader(l)
sources[i] = s
}
return sources, nil
}
func (g *GoTextFaceSource) copyCheck() {
if g.addr != g {
panic("text: illegal use of non-zero GoTextFaceSource copied by value")
}
}
// Metadata returns its metadata.
func (g *GoTextFaceSource) Metadata() Metadata {
return g.metadata
}
// UnsafeInternal returns its font.Face.
// The return value type is any since github.com/go-text/typesettings's API is now unstable.
//
// UnsafeInternal is unsafe since this might make internal cache states out of sync.
//
// UnsafeInternal might have breaking changes even in the same major version.
func (g *GoTextFaceSource) UnsafeInternal() any {
return g.f
}
func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Output, []glyph) {
g.copyCheck()
g.m.Lock()
defer g.m.Unlock()
key := face.outputCacheKey(text)
if out, ok := g.outputCache[key]; ok {
out.atime = now()
return out.outputs, out.glyphs
}
2024-04-21 15:46:51 +02:00
f := face.Source.f
f.SetVariations(face.variations)
runes := []rune(text)
input := shaping.Input{
Text: runes,
RunStart: 0,
RunEnd: len(runes),
Direction: face.diDirection(),
2024-04-21 15:46:51 +02:00
Face: f,
FontFeatures: face.features,
Size: float64ToFixed26_6(face.Size),
Script: face.gScript(),
Language: language.Language(face.Language.String()),
}
var seg shaping.Segmenter
2024-04-21 15:46:51 +02:00
inputs := seg.Split(input, &singleFontmap{face: f})
2024-10-27 10:39:10 +01:00
// Reverse the input for RTL texts.
if face.Direction == DirectionRightToLeft {
2024-10-27 10:39:10 +01:00
slices.Reverse(inputs)
}
outputs := make([]shaping.Output, len(inputs))
var gs []glyph
for i, input := range inputs {
out := g.shaper.Shape(input)
outputs[i] = out
(shaping.Line{out}).AdjustBaselines()
var indices []int
for i := range text {
indices = append(indices, i)
}
indices = append(indices, len(text))
for _, gl := range out.Glyphs {
gl := gl
var segs []opentype.Segment
switch data := g.f.GlyphData(gl.GlyphID).(type) {
case font.GlyphOutline:
if out.Direction.IsSideways() {
2024-04-21 15:46:51 +02:00
data.Sideways(fixed26_6ToFloat32(-gl.YOffset) / fixed26_6ToFloat32(out.Size) * float32(f.Upem()))
}
segs = data.Segments
case font.GlyphSVG:
segs = data.Outline.Segments
case font.GlyphBitmap:
if data.Outline != nil {
segs = data.Outline.Segments
}
}
scaledSegs := make([]opentype.Segment, len(segs))
scale := float32(g.scale(fixed26_6ToFloat64(out.Size)))
for i, seg := range segs {
scaledSegs[i] = seg
for j := range seg.Args {
scaledSegs[i].Args[j].X *= scale
scaledSegs[i].Args[j].Y *= -scale
}
}
gs = append(gs, glyph{
shapingGlyph: &gl,
startIndex: indices[gl.ClusterIndex],
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
scaledSegments: scaledSegs,
bounds: segmentsToBounds(scaledSegs),
})
}
}
if g.outputCache == nil {
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{}
}
g.outputCache[key] = &goTextOutputCacheValue{
outputs: outputs,
glyphs: gs,
atime: now(),
}
const cacheSoftLimit = 512
if len(g.outputCache) > cacheSoftLimit {
for key, e := range g.outputCache {
// 60 is an arbitrary number.
if e.atime >= now()-60 {
continue
}
delete(g.outputCache, key)
}
}
return outputs, gs
}
2023-11-15 04:34:02 +01:00
func (g *GoTextFaceSource) scale(size float64) float64 {
return size / float64(g.f.Upem())
2023-11-15 04:34:02 +01:00
}
func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image {
if g.glyphImageCache == nil {
g.glyphImageCache = map[float64]*glyphImageCache[goTextGlyphImageCacheKey]{}
}
if _, ok := g.glyphImageCache[goTextFace.Size]; !ok {
2024-10-27 13:44:17 +01:00
g.glyphImageCache[goTextFace.Size] = &glyphImageCache[goTextGlyphImageCacheKey]{
glyphVariationCount: glyphVariationCount(goTextFace),
}
}
2024-10-27 13:44:17 +01:00
return g.glyphImageCache[goTextFace.Size].getOrCreate(key, create)
}
type singleFontmap struct {
face *font.Face
}
func (s *singleFontmap) ResolveFace(r rune) *font.Face {
return s.face
}