text/v2: implement GoTextFace

Closes #675
Updates #2143
Updates #2454
This commit is contained in:
Hajime Hoshi 2023-11-12 17:47:31 +09:00
parent 46600b42f9
commit fe35180b78
7 changed files with 809 additions and 247 deletions

View File

@ -22,115 +22,62 @@ import (
_ "embed"
"image"
"image/color"
"image/draw"
"log"
"math"
"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
"golang.org/x/image/vector"
"golang.org/x/text/language"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/v2/text/v2"
)
//go:embed NotoSansArabic-Regular.ttf
var arabicTTF []byte
var arabicOut shaping.Output
var arabicFaceSource *text.GoTextFaceSource
func init() {
face, err := font.ParseTTF(bytes.NewReader(arabicTTF))
s, err := text.NewGoTextFaceSource(bytes.NewReader(arabicTTF))
if err != nil {
log.Fatal(err)
}
runes := []rune("لمّا كان الاعتراف بالكرامة المتأصلة في جميع")
input := shaping.Input{
Text: runes,
RunStart: 0,
RunEnd: len(runes),
Direction: di.DirectionRTL,
Face: face,
Size: fixed.I(24),
Script: language.Arabic,
Language: "ar",
}
arabicOut = (&shaping.HarfbuzzShaper{}).Shape(input)
arabicFaceSource = s
}
//go:embed NotoSansDevanagari-Regular.ttf
var devanagariTTF []byte
var devanagariOut shaping.Output
var devanagariFaceSource *text.GoTextFaceSource
func init() {
face, err := font.ParseTTF(bytes.NewReader(devanagariTTF))
s, err := text.NewGoTextFaceSource(bytes.NewReader(devanagariTTF))
if err != nil {
log.Fatal(err)
}
runes := []rune("चूंकि मानव परिवार के सभी सदस्यों के जन्मजात गौरव और समान")
input := shaping.Input{
Text: runes,
RunStart: 0,
RunEnd: len(runes),
Direction: di.DirectionLTR,
Face: face,
Size: fixed.I(24),
Script: language.Devanagari,
Language: "hi",
}
devanagariOut = (&shaping.HarfbuzzShaper{}).Shape(input)
devanagariFaceSource = s
}
//go:embed NotoSansThai-Regular.ttf
var thaiTTF []byte
var thaiOut shaping.Output
var thaiFaceSource *text.GoTextFaceSource
func init() {
face, err := font.ParseTTF(bytes.NewReader(thaiTTF))
s, err := text.NewGoTextFaceSource(bytes.NewReader(thaiTTF))
if err != nil {
log.Fatal(err)
}
runes := []rune("โดยที่การไม่นำพาและการหมิ่นในคุณค่าของสิทธิมนุษยชน")
input := shaping.Input{
Text: runes,
RunStart: 0,
RunEnd: len(runes),
Direction: di.DirectionLTR,
Face: face,
Size: fixed.I(24),
Script: language.Thai,
Language: "th",
}
thaiOut = (&shaping.HarfbuzzShaper{}).Shape(input)
thaiFaceSource = s
}
var japaneseOut shaping.Output
var japaneseFaceSource *text.GoTextFaceSource
func init() {
const japanese = language.Script(('j' << 24) | ('p' << 16) | ('a' << 8) | 'n')
face, err := font.ParseTTF(bytes.NewReader(fonts.MPlus1pRegular_ttf))
s, err := text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
if err != nil {
log.Fatal(err)
}
runes := []rune("ラーメン。")
input := shaping.Input{
Text: runes,
RunStart: 0,
RunEnd: len(runes),
Direction: di.DirectionTTB,
Face: face,
Size: fixed.I(24),
Script: japanese,
Language: "ja",
}
japaneseOut = (&shaping.HarfbuzzShaper{}).Shape(input)
japaneseFaceSource = s
}
var (
@ -151,21 +98,6 @@ const (
)
type Game struct {
vertices []ebiten.Vertex
indices []uint16
glyphCache map[glyphCacheKey]glyphCacheValue
}
type glyphCacheKey struct {
output *shaping.Output // TODO: This should be a font.Face instead of shaping.Output.
glyphID api.GID
origin fixed.Point26_6
}
type glyphCacheValue struct {
image *ebiten.Image
point image.Point
}
func (g *Game) Update() error {
@ -173,67 +105,47 @@ func (g *Game) Update() error {
}
func (g *Game) Draw(screen *ebiten.Image) {
g.drawGlyphs(screen, &arabicOut, 20, 100)
g.drawGlyphs(screen, &devanagariOut, 20, 150)
g.drawGlyphs(screen, &thaiOut, 20, 200)
g.drawGlyphs(screen, &japaneseOut, 20, 250)
}
const arabicText = "لمّا كان الاعتراف بالكرامة المتأصلة في جميع"
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
return float32(x>>6) + (float32(x&(1<<6-1)) / (1 << 6))
}
op := &text.DrawOptions{}
op.GeoM.Translate(screenWidth-20, 100)
text.Draw(screen, arabicText, &text.GoTextFace{
Source: arabicFaceSource,
Direction: text.DirectionRightToLeft,
SizeInPoints: 24,
Language: language.Arabic,
}, op)
func float32ToFixed26_6(x float32) fixed.Int26_6 {
i := float32(math.Floor(float64(x)))
return (fixed.Int26_6(i) << 6) + fixed.Int26_6((x-i)*(1<<6))
}
const hindiText = "चूंकि मानव परिवार के सभी सदस्यों के जन्मजात गौरव और समान"
func (g *Game) drawGlyphs(dst *ebiten.Image, output *shaping.Output, originX, originY float32) {
g.vertices = g.vertices[:0]
g.indices = g.indices[:0]
op.GeoM.Reset()
op.GeoM.Translate(20, 150)
text.Draw(screen, hindiText, &text.GoTextFace{
Source: devanagariFaceSource,
SizeInPoints: 24,
Language: language.Hindi,
}, op)
scale := fixed26_6ToFloat32(output.Size) / float32(output.Face.Font.Upem())
const thaiText = "โดยที่การไม่นำพาและการหมิ่นในคุณค่าของสิทธิมนุษยชน"
orig := fixed.Point26_6{
X: float32ToFixed26_6(originX),
Y: float32ToFixed26_6(originY),
}
for _, glyph := range output.Glyphs {
key := glyphCacheKey{
output: output,
glyphID: glyph.GlyphID,
origin: orig,
}
op.GeoM.Reset()
op.GeoM.Translate(20, 200)
text.Draw(screen, thaiText, &text.GoTextFace{
Source: thaiFaceSource,
SizeInPoints: 24,
Language: language.Thai,
}, op)
v, ok := g.glyphCache[key]
if !ok {
data := output.Face.GlyphData(glyph.GlyphID).(api.GlyphOutline)
if len(data.Segments) > 0 {
segs := make([]api.Segment, len(data.Segments))
for i, seg := range data.Segments {
segs[i] = seg
for j := range seg.Args {
segs[i].Args[j].X *= scale
segs[i].Args[j].Y *= scale
segs[i].Args[j].Y *= -1
}
}
v.image, v.point = segmentsToImage(segs, orig)
}
if g.glyphCache == nil {
g.glyphCache = map[glyphCacheKey]glyphCacheValue{}
}
g.glyphCache[key] = v
}
const japaneseText = "ラーメン。"
if v.image != nil {
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(v.point.X), float64(v.point.Y))
dst.DrawImage(v.image, op)
}
orig = orig.Add(fixed.Point26_6{X: glyph.XAdvance, Y: glyph.YAdvance * -1})
}
op.GeoM.Reset()
op.GeoM.Translate(20, 250)
text.Draw(screen, japaneseText, &text.GoTextFace{
Source: japaneseFaceSource,
Direction: text.DirectionTopToBottomAndRightToLeft,
SizeInPoints: 24,
Language: language.Thai,
}, op)
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
@ -247,94 +159,3 @@ func main() {
log.Fatal(err)
}
}
func segmentsToRect(segs []api.Segment) fixed.Rectangle26_6 {
if len(segs) == 0 {
return fixed.Rectangle26_6{}
}
minX := float32(math.Inf(1))
minY := float32(math.Inf(1))
maxX := float32(math.Inf(-1))
maxY := float32(math.Inf(-1))
for _, seg := range segs {
n := 1
switch seg.Op {
case api.SegmentOpQuadTo:
n = 2
case api.SegmentOpCubeTo:
n = 3
}
for i := 0; i < n; i++ {
x := seg.Args[i].X
y := seg.Args[i].Y
if minX > x {
minX = x
}
if minY > y {
minY = y
}
if maxX < x {
maxX = x
}
if maxY < y {
maxY = y
}
}
}
return fixed.Rectangle26_6{
Min: fixed.Point26_6{
X: float32ToFixed26_6(minX),
Y: float32ToFixed26_6(minY),
},
Max: fixed.Point26_6{
X: float32ToFixed26_6(maxX),
Y: float32ToFixed26_6(maxY),
},
}
}
func segmentsToImage(segs []api.Segment, orig fixed.Point26_6) (*ebiten.Image, image.Point) {
dBounds := segmentsToRect(segs).Add(orig)
dr := image.Rect(
dBounds.Min.X.Floor(),
dBounds.Min.Y.Floor(),
dBounds.Max.X.Ceil(),
dBounds.Max.Y.Ceil(),
)
biasX := fixed26_6ToFloat32(orig.X) - float32(dr.Min.X)
biasY := fixed26_6ToFloat32(orig.Y) - float32(dr.Min.Y)
width, height := dr.Dx(), dr.Dy()
if width <= 0 || height <= 0 {
return nil, image.Point{}
}
rast := vector.NewRasterizer(width, height)
rast.DrawOp = draw.Src
for _, seg := range segs {
switch seg.Op {
case api.SegmentOpMoveTo:
rast.MoveTo(seg.Args[0].X+biasX, seg.Args[0].Y+biasY)
case api.SegmentOpLineTo:
rast.LineTo(seg.Args[0].X+biasX, seg.Args[0].Y+biasY)
case api.SegmentOpQuadTo:
rast.QuadTo(
seg.Args[0].X+biasX, seg.Args[0].Y+biasY,
seg.Args[1].X+biasX, seg.Args[1].Y+biasY,
)
case api.SegmentOpCubeTo:
rast.CubeTo(
seg.Args[0].X+biasX, seg.Args[0].Y+biasY,
seg.Args[1].X+biasX, seg.Args[1].Y+biasY,
seg.Args[2].X+biasX, seg.Args[2].Y+biasY,
)
}
}
dst := image.NewAlpha(image.Rect(0, 0, width, height))
rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
return ebiten.NewImageFromImage(dst), dr.Min
}

View File

@ -16,7 +16,6 @@ package text
import (
"math"
"runtime"
"sync"
"golang.org/x/image/math/fixed"
@ -40,10 +39,24 @@ func init() {
})
}
type faceCacheKey struct {
stdFaceID uint64
goTextFaceSourceID uint64
goTextFaceDirection Direction
goTextFaceSizeInPoints float64
goTextFaceLanguage string
goTextFaceScript string
goTextFaceVariations string
goTextFaceFeatures string
}
type glyphImageCacheKey struct {
// For StdFace
rune rune
// id is rune for StdFace, and GID for GoTextFace.
id uint32
xoffset fixed.Int26_6
yoffset fixed.Int26_6
}
type glyphImageCacheEntry struct {
@ -109,10 +122,13 @@ func (g *glyphImageCache) getOrCreate(face Face, key glyphImageCacheKey, create
return img
}
func (g *glyphImageCache) clear(face Face) {
runtime.SetFinalizer(face, nil)
func (g *glyphImageCache) clear(comp func(key faceCacheKey) bool) {
g.m.Lock()
defer g.m.Unlock()
delete(g.cache, face.faceCacheKey())
for key := range g.cache {
if comp(key) {
delete(g.cache, key)
}
}
}

347
text/v2/gotext.go Normal file
View File

@ -0,0 +1,347 @@
// 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"
"encoding/binary"
"fmt"
"strings"
"github.com/go-text/typesetting/di"
glanguage "github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/opentype/api/font"
"github.com/go-text/typesetting/opentype/loader"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
"golang.org/x/text/language"
"github.com/hajimehoshi/ebiten/v2"
)
var _ Face = (*GoTextFace)(nil)
// GoTextFace is a Face implementation for go-text's font.Face (github.com/go-text/typesetting).
// With a GoTextFace, shaping.HarfBuzzShaper is always used as a shaper internally.
// GoTextFace includes the source and various options.
type GoTextFace struct {
Source *GoTextFaceSource
Direction Direction
SizeInPoints float64
// Language is a hiint for a language (BCP 47).
Language language.Tag
// Script is a hint for a script code hint of (ISO 15924).
// If this is empty, the script is guessed from the specified language.
Script language.Script
variations []font.Variation
features []shaping.FontFeature
variationsString string
featuresString string
}
// SetVariation sets a variation value.
// For font variations, see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts/Variable_fonts_guide for more details.
func (g *GoTextFace) SetVariation(tag Tag, value float32) {
idx := len(g.variations)
for i, v := range g.variations {
if uint32(v.Tag) < uint32(tag) {
continue
}
if uint32(v.Tag) > uint32(tag) {
idx = i
break
}
if v.Value == value {
return
}
g.variations[i].Value = value
g.variationsString = ""
return
}
// Keep the alphabetical order in order to make the cache key deterministic.
g.variations = append(g.variations, font.Variation{})
copy(g.variations[idx+1:], g.variations[idx:])
g.variations[idx] = font.Variation{
Tag: loader.Tag(tag),
Value: value,
}
g.variationsString = ""
}
// RemoveVariation removes a variation value.
func (g *GoTextFace) RemoveVariation(tag Tag) {
for i, v := range g.variations {
if uint32(v.Tag) < uint32(tag) {
continue
}
if uint32(v.Tag) > uint32(tag) {
return
}
copy(g.variations[i:], g.variations[i+1:])
g.variations = g.variations[:len(g.variations)-1]
g.variationsString = ""
return
}
}
// SetFeature sets a feature value.
// For font features, see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_fonts/OpenType_fonts_guide for more details.
func (g *GoTextFace) SetFeature(tag Tag, value uint32) {
idx := len(g.features)
for i, f := range g.features {
if uint32(f.Tag) < uint32(tag) {
continue
}
if uint32(f.Tag) > uint32(tag) {
idx = i
break
}
if f.Value == value {
return
}
g.features[i].Value = value
g.featuresString = ""
return
}
// Keep the alphabetical order in order to make the cache key deterministic.
g.features = append(g.features, shaping.FontFeature{})
copy(g.features[idx+1:], g.features[idx:])
g.features[idx] = shaping.FontFeature{
Tag: loader.Tag(tag),
Value: value,
}
g.featuresString = ""
}
// RemoveFeature removes a feature value.
func (g *GoTextFace) RemoveFeature(tag Tag) {
for i, v := range g.features {
if uint32(v.Tag) < uint32(tag) {
continue
}
if uint32(v.Tag) > uint32(tag) {
return
}
copy(g.features[i:], g.features[i+1:])
g.features = g.features[:len(g.features)-1]
g.featuresString = ""
return
}
}
// Tag is a tag for font variations and features.
// Tag is a 4-byte value like 'cmap'.
type Tag uint32
// String returns the Tag's string representation.
func (t Tag) String() string {
return string([]byte{byte(t >> 24), byte(t >> 16), byte(t >> 8), byte(t)})
}
// PraseTag converts a string to Tag.
func ParseTag(str string) (Tag, error) {
if len(str) != 4 {
return 0, fmt.Errorf("text: a string's length must be 4 but was %d at ParseTag", len(str))
}
return Tag((uint32(str[0]) << 24) | (uint32(str[1]) << 16) | (uint32(str[2]) << 8) | uint32(str[3])), nil
}
// MustPraseTag converts a string to Tag.
// If parsing fails, MustParseTag panics.
func MustParseTag(str string) Tag {
t, err := ParseTag(str)
if err != nil {
panic(err)
}
return t
}
// Metrics implements Face.
func (g *GoTextFace) Metrics() Metrics {
return g.Source.Metrics()
}
// UnsafeInternal implements Face.
func (g *GoTextFace) UnsafeInternal() any {
return g.Source.f
}
func (g *GoTextFace) ensureVariationsString() string {
if g.variationsString != "" {
return g.variationsString
}
if len(g.variations) == 0 {
return ""
}
var buf bytes.Buffer
for _, t := range g.variations {
_ = binary.Write(&buf, binary.LittleEndian, t.Tag)
_ = binary.Write(&buf, binary.LittleEndian, t.Value)
}
g.variationsString = buf.String()
return g.variationsString
}
func (g *GoTextFace) ensureFeaturesString() string {
if g.featuresString != "" {
return g.featuresString
}
if len(g.features) == 0 {
return ""
}
var buf bytes.Buffer
for _, t := range g.features {
_ = binary.Write(&buf, binary.LittleEndian, t.Tag)
_ = binary.Write(&buf, binary.LittleEndian, t.Value)
}
g.featuresString = buf.String()
return g.featuresString
}
// faceCacheKey implements Face.
func (g *GoTextFace) faceCacheKey() faceCacheKey {
return faceCacheKey{
goTextFaceSourceID: g.Source.id,
goTextFaceDirection: g.Direction,
goTextFaceSizeInPoints: g.SizeInPoints,
goTextFaceLanguage: g.Language.String(),
goTextFaceScript: g.Script.String(),
goTextFaceVariations: g.ensureVariationsString(),
goTextFaceFeatures: g.ensureFeaturesString(),
}
}
func (g *GoTextFace) outputCacheKey(text string) goTextOutputCacheKey {
return goTextOutputCacheKey{
text: text,
direction: g.Direction,
sizeInPoints: g.SizeInPoints,
language: g.Language.String(),
script: g.Script.String(),
variations: g.ensureVariationsString(),
features: g.ensureFeaturesString(),
}
}
func (g *GoTextFace) diDirection() di.Direction {
switch g.Direction {
case DirectionLeftToRight:
return di.DirectionLTR
case DirectionRightToLeft:
return di.DirectionRTL
default:
return di.DirectionTTB
}
}
func (g *GoTextFace) gScript() glanguage.Script {
var str string
if g.Script != (language.Script{}) {
str = g.Script.String()
} else {
s, _ := g.Language.Script()
str = s.String()
}
// ISO 15924 itself is case-insensitive [1], but go-text uses small-caps.
// [1] https://www.unicode.org/iso15924/faq.html#17
str = strings.ToLower(str)
s, err := glanguage.ParseScript(str)
if err != nil {
panic(err)
}
return s
}
// advance implements Face.
func (g *GoTextFace) advance(text string) float64 {
output, _ := g.Source.shape(text, g)
return fixed26_6ToFloat64(output.Advance)
}
// appendGlyphs implements Face.
func (g *GoTextFace) appendGlyphs(glyphs []Glyph, text string, indexOffset int, originX, originY float64) []Glyph {
_, gs := g.Source.shape(text, g)
origin := fixed.Point26_6{
X: float64ToFixed26_6(originX),
Y: float64ToFixed26_6(originY),
}
for _, glyph := range gs {
img, imgX, imgY := g.glyphImage(glyph, origin)
if img != nil {
glyphs = append(glyphs, Glyph{
StartIndexInBytes: indexOffset + glyph.startIndex,
EndIndexInBytes: indexOffset + glyph.endIndex,
GID: uint32(glyph.shapingGlyph.GlyphID),
Image: img,
X: float64(imgX),
Y: float64(imgY),
})
}
origin = origin.Add(fixed.Point26_6{
X: glyph.shapingGlyph.XAdvance,
Y: -glyph.shapingGlyph.YAdvance,
})
}
return glyphs
}
func (g *GoTextFace) glyphImage(glyph glyph, origin fixed.Point26_6) (*ebiten.Image, int, int) {
if g.direction().isHorizontal() {
origin.X = adjustGranularity(origin.X)
origin.Y &^= ((1 << 6) - 1)
} else {
origin.X &^= ((1 << 6) - 1)
origin.Y = adjustGranularity(origin.Y)
}
b := segmentsToBounds(glyph.scaledSegments)
subpixelOffset := fixed.Point26_6{
X: (origin.X + b.Min.X) & ((1 << 6) - 1),
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
}
key := glyphImageCacheKey{
id: uint32(glyph.shapingGlyph.GlyphID),
xoffset: subpixelOffset.X,
yoffset: subpixelOffset.Y,
}
img := theGlyphImageCache.getOrCreate(g, key, func() *ebiten.Image {
return segmentsToImage(glyph.scaledSegments, subpixelOffset, b)
})
imgX := (origin.X + b.Min.X).Floor()
imgY := (origin.Y + b.Min.Y).Floor()
return img, imgX, imgY
}
// direction implements Face.
func (g *GoTextFace) direction() Direction {
return g.Direction
}
// private implements Face.
func (g *GoTextFace) private() {
}

235
text/v2/gotextfacesource.go Normal file
View File

@ -0,0 +1,235 @@
// 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"
"runtime"
"sync"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/shaping"
)
type goTextOutputCacheKey struct {
text string
direction Direction
sizeInPoints float64
language string
script string
variations string
features string
}
type glyph struct {
shapingGlyph *shaping.Glyph
startIndex int
endIndex int
scaledSegments []api.Segment
}
type goTextOutputCacheValue struct {
output shaping.Output
glyphs []glyph
atime int64
}
// GoTextFaceSource is a source of a GoTextFace. This can be shared by multiple GoTextFace objects.
type GoTextFaceSource struct {
f font.Face
id uint64
outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue
m sync.Mutex
}
func toFontResource(source io.ReadSeeker) (font.Resource, error) {
// font.Resource has io.ReaderAt in addition to io.ReadSeeker.
// 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.
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.ReadSeeker) (*GoTextFaceSource, error) {
src, err := toFontResource(source)
if err != nil {
return nil, err
}
f, err := font.ParseTTF(src)
if err != nil {
return nil, err
}
s := &GoTextFaceSource{
f: f,
id: nextUniqueID(),
}
runtime.SetFinalizer(s, finalizeGoTextFaceSource)
return s, nil
}
// NewGoTextFaceSourcesFromCollection parses an OpenType or TrueType font collection and returns a slice of GoTextFaceSource objects.
func NewGoTextFaceSourcesFromCollection(source io.ReadSeeker) ([]*GoTextFaceSource, error) {
src, err := toFontResource(source)
if err != nil {
return nil, err
}
fs, err := font.ParseTTC(src)
if err != nil {
return nil, err
}
sources := make([]*GoTextFaceSource, len(fs))
for i, f := range fs {
s := &GoTextFaceSource{
f: f,
id: nextUniqueID(),
}
runtime.SetFinalizer(s, finalizeGoTextFaceSource)
sources[i] = s
}
return sources, nil
}
func finalizeGoTextFaceSource(source *GoTextFaceSource) {
runtime.SetFinalizer(source, nil)
theGlyphImageCache.clear(func(key faceCacheKey) bool {
return key.goTextFaceSourceID == source.id
})
}
// Metrics returns the font's metrics.
func (g *GoTextFaceSource) Metrics() Metrics {
upem := float64(g.f.Font.Upem())
var m Metrics
if h, ok := g.f.FontHExtents(); ok {
m.Height = float64(h.Ascender-h.Descender+h.LineGap) / upem
m.HAscent = float64(h.Ascender) / upem
m.HDescent = float64(-h.Descender) / upem
}
if v, ok := g.f.FontVExtents(); ok {
m.Width = float64(v.Ascender-v.Descender+v.LineGap) / upem
m.VAscent = float64(v.Ascender) / upem
m.VDescent = float64(-v.Descender) / upem
}
return m
}
func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output, []glyph) {
g.m.Lock()
defer g.m.Unlock()
key := face.outputCacheKey(text)
if out, ok := g.outputCache[key]; ok {
out.atime = now()
return out.output, out.glyphs
}
g.f.SetVariations(face.variations)
runes := []rune(text)
input := shaping.Input{
Text: runes,
RunStart: 0,
RunEnd: len(runes),
Direction: face.diDirection(),
Face: face.Source.f,
FontFeatures: face.features,
Size: float64ToFixed26_6(face.SizeInPoints),
Script: face.gScript(),
Language: language.Language(face.Language.String()),
}
out := (&shaping.HarfbuzzShaper{}).Shape(input)
if g.outputCache == nil {
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{}
}
var indices []int
for i := range text {
indices = append(indices, i)
}
indices = append(indices, len(text))
gs := make([]glyph, len(out.Glyphs))
for i, gl := range out.Glyphs {
gl := gl
var segs []api.Segment
switch data := g.f.GlyphData(gl.GlyphID).(type) {
case api.GlyphOutline:
segs = data.Segments
case api.GlyphSVG:
segs = data.Outline.Segments
case api.GlyphBitmap:
if data.Outline != nil {
segs = data.Outline.Segments
}
}
scaledSegs := make([]api.Segment, len(segs))
scale := fixed26_6ToFloat32(out.Size) / float32(out.Face.Font.Upem())
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
scaledSegs[i].Args[j].Y *= -1
}
}
gs[i] = glyph{
shapingGlyph: &gl,
startIndex: indices[gl.ClusterIndex],
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
scaledSegments: scaledSegs,
}
}
g.outputCache[key] = &goTextOutputCacheValue{
output: out,
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 out, gs
}

122
text/v2/gotextseg.go Normal file
View File

@ -0,0 +1,122 @@
// 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 (
"image"
"image/draw"
"math"
"github.com/go-text/typesetting/opentype/api"
"golang.org/x/image/math/fixed"
"golang.org/x/image/vector"
"github.com/hajimehoshi/ebiten/v2"
)
func segmentsToBounds(segs []api.Segment) fixed.Rectangle26_6 {
if len(segs) == 0 {
return fixed.Rectangle26_6{}
}
minX := float32(math.Inf(1))
minY := float32(math.Inf(1))
maxX := float32(math.Inf(-1))
maxY := float32(math.Inf(-1))
for _, seg := range segs {
n := 1
switch seg.Op {
case api.SegmentOpQuadTo:
n = 2
case api.SegmentOpCubeTo:
n = 3
}
for i := 0; i < n; i++ {
x := seg.Args[i].X
y := seg.Args[i].Y
if minX > x {
minX = x
}
if minY > y {
minY = y
}
if maxX < x {
maxX = x
}
if maxY < y {
maxY = y
}
}
}
return fixed.Rectangle26_6{
Min: fixed.Point26_6{
X: float32ToFixed26_6(minX),
Y: float32ToFixed26_6(minY),
},
Max: fixed.Point26_6{
X: float32ToFixed26_6(maxX),
Y: float32ToFixed26_6(maxY),
},
}
}
func segmentsToImage(segs []api.Segment, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image {
if len(segs) == 0 {
return nil
}
w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil()
if w == 0 || h == 0 {
return nil
}
if glyphBounds.Min.X&((1<<6)-1) != 0 {
w++
}
if glyphBounds.Min.Y&((1<<6)-1) != 0 {
h++
}
biasX := fixed26_6ToFloat32(-glyphBounds.Min.X + subpixelOffset.X)
biasY := fixed26_6ToFloat32(-glyphBounds.Min.Y + subpixelOffset.Y)
rast := vector.NewRasterizer(w, h)
rast.DrawOp = draw.Src
for _, seg := range segs {
switch seg.Op {
case api.SegmentOpMoveTo:
rast.MoveTo(seg.Args[0].X+biasX, seg.Args[0].Y+biasY)
case api.SegmentOpLineTo:
rast.LineTo(seg.Args[0].X+biasX, seg.Args[0].Y+biasY)
case api.SegmentOpQuadTo:
rast.QuadTo(
seg.Args[0].X+biasX, seg.Args[0].Y+biasY,
seg.Args[1].X+biasX, seg.Args[1].Y+biasY,
)
case api.SegmentOpCubeTo:
rast.CubeTo(
seg.Args[0].X+biasX, seg.Args[0].Y+biasY,
seg.Args[1].X+biasX, seg.Args[1].Y+biasY,
seg.Args[2].X+biasX, seg.Args[2].Y+biasY,
)
}
}
dst := image.NewAlpha(image.Rect(0, 0, w, h))
rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{})
return ebiten.NewImageFromImage(dst)
}

View File

@ -17,7 +17,6 @@ package text
import (
"image"
"runtime"
"sync/atomic"
"unicode/utf8"
"golang.org/x/image/font"
@ -26,12 +25,6 @@ import (
"github.com/hajimehoshi/ebiten/v2"
)
var currentStdFaceID uint64
func nextStdFaceID() uint64 {
return atomic.AddUint64(&currentStdFaceID, 1)
}
var _ Face = (*StdFace)(nil)
// StdFace is a Face implementation for a semi-standard font.Face (golang.org/x/image/font).
@ -51,13 +44,20 @@ func NewStdFace(face font.Face) *StdFace {
f: &faceWithCache{
f: face,
},
id: nextStdFaceID(),
id: nextUniqueID(),
}
s.addr = s
runtime.SetFinalizer(s, theGlyphImageCache.clear)
runtime.SetFinalizer(s, finalizeStdFace)
return s
}
func finalizeStdFace(face *StdFace) {
runtime.SetFinalizer(face, nil)
theGlyphImageCache.clear(func(key faceCacheKey) bool {
return key.stdFaceID == face.id
})
}
func (s *StdFace) copyCheck() {
if s.addr != s {
panic("text: illegal use of non-zero StdFace copied by value")
@ -84,7 +84,9 @@ func (s *StdFace) UnsafeInternal() any {
// faceCacheKey implements Face.
func (s *StdFace) faceCacheKey() faceCacheKey {
return faceCacheKey(s.id)
return faceCacheKey{
stdFaceID: s.id,
}
}
// advance implements Face.
@ -137,7 +139,7 @@ func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
}
key := glyphImageCacheKey{
rune: r,
id: uint32(r),
xoffset: subpixelOffset.X,
// yoffset is always the same if the rune is the same, so this doesn't have to be a key.
}

View File

@ -20,14 +20,13 @@ package text
import (
"math"
"strings"
"sync/atomic"
"golang.org/x/image/math/fixed"
"github.com/hajimehoshi/ebiten/v2"
)
type faceCacheKey uint64
// Face is an interface representing a font face. The implementations are only GoTextFace and StdFace.
type Face interface {
// Metrics returns the metrics for this Face.
@ -63,6 +62,10 @@ type Metrics struct {
// The value is typically positive, even though a descender goes below the baseline.
HDescent float64
// Width is the recommended amount of horizontal space between two lines of text in pixels.
// If the face is StdFace or the font dosen't support a vertical direction, Width is 0.
Width float64
// VAscent is the distance in pixels from the top of a line to its baseline for vertical lines.
// If the face is StdFace or the font dosen't support a vertical direction, VAscent is 0.
VAscent float64
@ -72,10 +75,20 @@ type Metrics struct {
VDescent float64
}
func fixed26_6ToFloat32(x fixed.Int26_6) float32 {
return float32(x>>6) + float32(x&((1<<6)-1))/float32(1<<6)
}
func fixed26_6ToFloat64(x fixed.Int26_6) float64 {
return float64(x>>6) + float64(x&((1<<6)-1))/float64(1<<6)
}
func float32ToFixed26_6(x float32) fixed.Int26_6 {
i := float32(math.Floor(float64(x)))
frac := x - i
return fixed.Int26_6(i)<<6 + fixed.Int26_6(frac*(1<<6))
}
func float64ToFixed26_6(x float64) fixed.Int26_6 {
i := math.Floor(x)
frac := x - i
@ -227,3 +240,9 @@ func CacheGlyphs(text string, face Face) {
}
}
}
var currentUniqueID uint64
func nextUniqueID() uint64 {
return atomic.AddUint64(&currentUniqueID, 1)
}