2023-11-12 09:47:31 +01:00
|
|
|
// 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"
|
2024-04-20 10:45:08 +02:00
|
|
|
"image"
|
|
|
|
"image/jpeg"
|
|
|
|
"image/png" // As typesettings/font already imports image/png, it is fine to ignore side effects (#2336).
|
2023-11-12 09:47:31 +01:00
|
|
|
"io"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/go-text/typesetting/font"
|
|
|
|
"github.com/go-text/typesetting/language"
|
|
|
|
"github.com/go-text/typesetting/opentype/api"
|
2023-11-25 13:53:46 +01:00
|
|
|
ofont "github.com/go-text/typesetting/opentype/api/font"
|
|
|
|
"github.com/go-text/typesetting/opentype/loader"
|
2023-11-12 09:47:31 +01:00
|
|
|
"github.com/go-text/typesetting/shaping"
|
2023-11-17 05:07:04 +01:00
|
|
|
"golang.org/x/image/math/fixed"
|
2024-04-20 10:45:08 +02:00
|
|
|
"golang.org/x/image/tiff"
|
2023-11-19 15:19:24 +01:00
|
|
|
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
2023-11-12 09:47:31 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type goTextOutputCacheKey struct {
|
2023-11-15 13:52:25 +01:00
|
|
|
text string
|
|
|
|
direction Direction
|
|
|
|
size float64
|
|
|
|
language string
|
|
|
|
script string
|
|
|
|
variations string
|
|
|
|
features string
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type glyph struct {
|
|
|
|
shapingGlyph *shaping.Glyph
|
|
|
|
startIndex int
|
|
|
|
endIndex int
|
|
|
|
scaledSegments []api.Segment
|
2023-11-17 05:07:04 +01:00
|
|
|
bounds fixed.Rectangle26_6
|
2024-04-20 10:45:08 +02:00
|
|
|
bitmap image.Image
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type goTextOutputCacheValue struct {
|
2023-12-09 09:30:08 +01:00
|
|
|
outputs []shaping.Output
|
|
|
|
glyphs []glyph
|
|
|
|
atime int64
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
|
|
|
|
2023-11-19 15:40:47 +01:00
|
|
|
type goTextGlyphImageCacheKey struct {
|
|
|
|
gid api.GID
|
|
|
|
xoffset fixed.Int26_6
|
|
|
|
yoffset fixed.Int26_6
|
|
|
|
variations string
|
|
|
|
}
|
|
|
|
|
2023-11-12 09:47:31 +01:00
|
|
|
// GoTextFaceSource is a source of a GoTextFace. This can be shared by multiple GoTextFace objects.
|
|
|
|
type GoTextFaceSource struct {
|
2023-11-25 13:53:46 +01:00
|
|
|
f font.Face
|
|
|
|
metadata Metadata
|
2023-11-12 09:47:31 +01:00
|
|
|
|
2023-11-19 15:19:24 +01:00
|
|
|
outputCache map[goTextOutputCacheKey]*goTextOutputCacheValue
|
2023-11-19 15:40:47 +01:00
|
|
|
glyphImageCache map[float64]*glyphImageCache[goTextGlyphImageCacheKey]
|
2023-11-12 09:47:31 +01:00
|
|
|
|
2023-11-19 15:15:18 +01:00
|
|
|
addr *GoTextFaceSource
|
|
|
|
|
2024-04-17 08:58:42 +02:00
|
|
|
shaper shaping.HarfbuzzShaper
|
|
|
|
|
2024-04-20 10:45:08 +02:00
|
|
|
bitmapSizesResult []api.BitmapSize
|
|
|
|
bitmapSizesOnce sync.Once
|
|
|
|
|
2023-11-12 09:47:31 +01:00
|
|
|
m sync.Mutex
|
|
|
|
}
|
|
|
|
|
2023-11-26 09:21:31 +01:00
|
|
|
func toFontResource(source io.Reader) (font.Resource, error) {
|
|
|
|
// font.Resource has io.Seeker and io.ReaderAt in addition to io.Reader.
|
2023-11-12 09:47:31 +01:00
|
|
|
// 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.
|
2023-11-26 09:21:31 +01:00
|
|
|
// TODO: Implement io.ReaderAt in a more efficient way when source is io.Seeker.
|
2023-11-12 09:47:31 +01:00
|
|
|
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.
|
2023-11-26 09:21:31 +01:00
|
|
|
func NewGoTextFaceSource(source io.Reader) (*GoTextFaceSource, error) {
|
2023-11-12 09:47:31 +01:00
|
|
|
src, err := toFontResource(source)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-25 13:53:46 +01:00
|
|
|
l, err := loader.NewLoader(src)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-25 14:59:01 +01:00
|
|
|
f, err := ofont.NewFont(l)
|
2023-11-12 09:47:31 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
s := &GoTextFaceSource{
|
2023-11-25 14:59:01 +01:00
|
|
|
f: &ofont.Face{Font: f},
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
2023-11-19 15:15:18 +01:00
|
|
|
s.addr = s
|
2023-11-25 13:53:46 +01:00
|
|
|
s.metadata = metadataFromLoader(l)
|
|
|
|
|
2023-11-12 09:47:31 +01:00
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewGoTextFaceSourcesFromCollection parses an OpenType or TrueType font collection and returns a slice of GoTextFaceSource objects.
|
2023-11-26 09:21:31 +01:00
|
|
|
func NewGoTextFaceSourcesFromCollection(source io.Reader) ([]*GoTextFaceSource, error) {
|
2023-11-12 09:47:31 +01:00
|
|
|
src, err := toFontResource(source)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-25 13:53:46 +01:00
|
|
|
ls, err := loader.NewLoaders(src)
|
2023-11-12 09:47:31 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-11-25 13:53:46 +01:00
|
|
|
sources := make([]*GoTextFaceSource, len(ls))
|
|
|
|
for i, l := range ls {
|
2023-11-25 14:59:01 +01:00
|
|
|
f, err := ofont.NewFont(l)
|
2023-11-25 13:53:46 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-11-12 09:47:31 +01:00
|
|
|
s := &GoTextFaceSource{
|
2023-11-25 14:59:01 +01:00
|
|
|
f: &ofont.Face{Font: f},
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
2023-11-19 15:15:18 +01:00
|
|
|
s.addr = s
|
2023-11-25 13:53:46 +01:00
|
|
|
s.metadata = metadataFromLoader(l)
|
2023-11-12 09:47:31 +01:00
|
|
|
sources[i] = s
|
|
|
|
}
|
|
|
|
return sources, nil
|
|
|
|
}
|
|
|
|
|
2023-11-19 15:15:18 +01:00
|
|
|
func (g *GoTextFaceSource) copyCheck() {
|
|
|
|
if g.addr != g {
|
|
|
|
panic("text: illegal use of non-zero GoTextFaceSource copied by value")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-25 13:53:46 +01:00
|
|
|
// Metadata returns its metadata.
|
|
|
|
func (g *GoTextFaceSource) Metadata() Metadata {
|
|
|
|
return g.metadata
|
|
|
|
}
|
|
|
|
|
2023-11-25 14:55:53 +01:00
|
|
|
// UnsafeInternal returns its font.Face.
|
|
|
|
//
|
|
|
|
// This is unsafe since this might make internal cache states out of sync.
|
|
|
|
func (g *GoTextFaceSource) UnsafeInternal() font.Face {
|
|
|
|
return g.f
|
|
|
|
}
|
|
|
|
|
2023-12-09 09:30:08 +01:00
|
|
|
func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Output, []glyph) {
|
2023-11-19 15:15:18 +01:00
|
|
|
g.copyCheck()
|
|
|
|
|
2023-11-12 09:47:31 +01:00
|
|
|
g.m.Lock()
|
|
|
|
defer g.m.Unlock()
|
|
|
|
|
|
|
|
key := face.outputCacheKey(text)
|
|
|
|
if out, ok := g.outputCache[key]; ok {
|
|
|
|
out.atime = now()
|
2023-12-09 09:30:08 +01:00
|
|
|
return out.outputs, out.glyphs
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
|
|
|
|
2024-04-21 15:46:51 +02:00
|
|
|
f := face.Source.f
|
|
|
|
f.SetVariations(face.variations)
|
2024-04-20 10:45:08 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2024-04-21 15:46:51 +02:00
|
|
|
|
2023-11-12 09:47:31 +01:00
|
|
|
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,
|
2023-11-12 09:47:31 +01:00
|
|
|
FontFeatures: face.features,
|
2023-11-15 13:52:25 +01:00
|
|
|
Size: float64ToFixed26_6(face.Size),
|
2023-11-12 09:47:31 +01:00
|
|
|
Script: face.gScript(),
|
|
|
|
Language: language.Language(face.Language.String()),
|
|
|
|
}
|
2023-12-09 09:30:08 +01:00
|
|
|
|
2023-12-23 19:30:20 +01:00
|
|
|
var seg shaping.Segmenter
|
2024-04-21 15:46:51 +02:00
|
|
|
inputs := seg.Split(input, &singleFontmap{face: f})
|
2023-11-12 09:47:31 +01:00
|
|
|
|
2023-12-09 09:30:08 +01:00
|
|
|
if face.Direction == DirectionRightToLeft {
|
|
|
|
// Reverse the input for RTL texts.
|
|
|
|
for i, j := 0, len(inputs)-1; i < j; i, j = i+1, j-1 {
|
|
|
|
inputs[i], inputs[j] = inputs[j], inputs[i]
|
|
|
|
}
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
2023-12-09 09:30:08 +01:00
|
|
|
|
|
|
|
outputs := make([]shaping.Output, len(inputs))
|
|
|
|
var gs []glyph
|
|
|
|
for i, input := range inputs {
|
2024-04-17 08:58:42 +02:00
|
|
|
out := g.shaper.Shape(input)
|
2023-12-09 09:30:08 +01:00
|
|
|
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 {
|
2024-04-20 10:45:08 +02:00
|
|
|
shapingGlyph := gl
|
2023-12-09 09:30:08 +01:00
|
|
|
var segs []api.Segment
|
2024-04-20 10:45:08 +02:00
|
|
|
var bitmap image.Image
|
|
|
|
switch data := g.f.GlyphData(shapingGlyph.GlyphID).(type) {
|
2023-12-09 09:30:08 +01:00
|
|
|
case api.GlyphOutline:
|
|
|
|
if out.Direction.IsSideways() {
|
2024-04-20 10:45:08 +02:00
|
|
|
data.Sideways(fixed26_6ToFloat32(-shapingGlyph.YOffset) / fixed26_6ToFloat32(out.Size) * float32(f.Upem()))
|
2023-12-09 09:30:08 +01:00
|
|
|
}
|
|
|
|
segs = data.Segments
|
|
|
|
case api.GlyphSVG:
|
2023-11-12 09:47:31 +01:00
|
|
|
segs = data.Outline.Segments
|
2023-12-09 09:30:08 +01:00
|
|
|
case api.GlyphBitmap:
|
2024-04-20 10:45:08 +02:00
|
|
|
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.
|
2023-12-09 09:30:08 +01:00
|
|
|
if data.Outline != nil {
|
|
|
|
segs = data.Outline.Segments
|
|
|
|
}
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
|
|
|
|
2024-04-20 10:45:08 +02:00
|
|
|
gl := glyph{
|
|
|
|
startIndex: indices[shapingGlyph.ClusterIndex],
|
|
|
|
endIndex: indices[shapingGlyph.ClusterIndex+shapingGlyph.RuneCount],
|
|
|
|
}
|
|
|
|
|
2023-12-09 09:30:08 +01:00
|
|
|
scaledSegs := make([]api.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
|
|
|
|
}
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
|
|
|
|
2024-04-20 10:45:08 +02:00
|
|
|
gl.shapingGlyph = &shapingGlyph
|
|
|
|
gl.scaledSegments = scaledSegs
|
|
|
|
gl.bounds = segmentsToBounds(scaledSegs)
|
|
|
|
|
|
|
|
if bitmap != nil {
|
|
|
|
gl.bitmap = bitmap
|
|
|
|
}
|
|
|
|
|
|
|
|
gs = append(gs, gl)
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
|
|
|
}
|
2023-12-09 09:30:08 +01:00
|
|
|
|
|
|
|
if g.outputCache == nil {
|
|
|
|
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{}
|
|
|
|
}
|
2023-11-12 09:47:31 +01:00
|
|
|
g.outputCache[key] = &goTextOutputCacheValue{
|
2023-12-09 09:30:08 +01:00
|
|
|
outputs: outputs,
|
|
|
|
glyphs: gs,
|
|
|
|
atime: now(),
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-09 09:30:08 +01:00
|
|
|
return outputs, gs
|
2023-11-12 09:47:31 +01:00
|
|
|
}
|
2023-11-15 04:34:02 +01:00
|
|
|
|
2023-11-15 13:52:25 +01:00
|
|
|
func (g *GoTextFaceSource) scale(size float64) float64 {
|
|
|
|
return size / float64(g.f.Upem())
|
2023-11-15 04:34:02 +01:00
|
|
|
}
|
2023-11-19 15:19:24 +01:00
|
|
|
|
2023-11-19 15:40:47 +01:00
|
|
|
func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goTextGlyphImageCacheKey, create func() *ebiten.Image) *ebiten.Image {
|
2023-11-19 15:19:24 +01:00
|
|
|
if g.glyphImageCache == nil {
|
2023-11-19 15:40:47 +01:00
|
|
|
g.glyphImageCache = map[float64]*glyphImageCache[goTextGlyphImageCacheKey]{}
|
2023-11-19 15:19:24 +01:00
|
|
|
}
|
|
|
|
if _, ok := g.glyphImageCache[goTextFace.Size]; !ok {
|
2023-11-19 15:40:47 +01:00
|
|
|
g.glyphImageCache[goTextFace.Size] = &glyphImageCache[goTextGlyphImageCacheKey]{}
|
2023-11-19 15:19:24 +01:00
|
|
|
}
|
|
|
|
return g.glyphImageCache[goTextFace.Size].getOrCreate(goTextFace, key, create)
|
|
|
|
}
|
2023-12-09 09:30:08 +01:00
|
|
|
|
2024-04-20 10:45:08 +02:00
|
|
|
func (g *GoTextFaceSource) bitmapSizes() []api.BitmapSize {
|
|
|
|
g.bitmapSizesOnce.Do(func() {
|
|
|
|
g.bitmapSizesResult = g.f.BitmapSizes()
|
|
|
|
})
|
|
|
|
return g.bitmapSizesResult
|
|
|
|
}
|
|
|
|
|
2023-12-09 09:30:08 +01:00
|
|
|
type singleFontmap struct {
|
|
|
|
face font.Face
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *singleFontmap) ResolveFace(r rune) font.Face {
|
|
|
|
return s.face
|
|
|
|
}
|