Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions font/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,37 @@ func (f *Face) GlyphExtents(glyph GID) (GlyphExtents, bool) {
}
return e, ok
}

// advanceCache stores one direction of advances of variable fonts
// without HVAR/VVAR tables. Computing one resolves the full glyph
// outline. The horizontal and vertical caches are separate slices,
// since most fonts are never asked for vertical advances.
// Advances are never negative, so -1 marks an empty entry.
type advanceCache []float32

func newAdvanceCache(nGlyphs int) advanceCache {
ac := make(advanceCache, nGlyphs)
ac.reset()
return ac
}

func (ac advanceCache) get(gid gID) (float32, bool) {
if int(gid) >= len(ac) {
return 0, false
}
adv := ac[gid]
return adv, adv >= 0
}

func (ac advanceCache) set(gid gID, advance float32) {
if int(gid) >= len(ac) {
return
}
ac[gid] = advance
}

func (ac advanceCache) reset() {
for i := range ac {
ac[i] = -1
}
}
20 changes: 18 additions & 2 deletions font/cff/charstring.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,31 @@ package cff
import (
"errors"
"fmt"
"sync"

ps "github.com/go-text/typesetting/font/cff/interpreter"
ot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font/opentype/tables"
)

// a Machine is about 4KB, so LoadGlyph reuses one instead of allocating it per glyph
var machinePool = sync.Pool{New: func() any { return new(ps.Machine) }}

// LoadGlyph parses the glyph charstring to compute segments and path bounds.
// It returns an error if the glyph is invalid or if decoding the charstring fails.
func (f *CFF) LoadGlyph(glyph tables.GlyphID) ([]ot.Segment, ps.PathBounds, error) {
if int(glyph) >= len(f.Charstrings) {
return nil, ps.PathBounds{}, errGlyph
}

psi := machinePool.Get().(*ps.Machine)
defer func() {
// Do not keep font data alive through the subroutine and call stacks.
*psi = ps.Machine{}
machinePool.Put(psi)
}()

var (
psi ps.Machine
loader type2CharstringHandler
index uint16
err error
Expand Down Expand Up @@ -128,8 +138,14 @@ func (f *CFF2) LoadGlyph(glyph tables.GlyphID, coords []tables.Coord) ([]ot.Segm
return nil, ps.PathBounds{}, errGlyph
}

psi := machinePool.Get().(*ps.Machine)
defer func() {
// Do not keep font data alive through the subroutine and call stacks.
*psi = ps.Machine{}
machinePool.Put(psi)
}()

var (
psi ps.Machine
loader cff2CharstringHandler
index uint16
err error
Expand Down
45 changes: 45 additions & 0 deletions font/cff/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/binary"
"encoding/json"
"reflect"
"sync"
"testing"

td "github.com/go-text/typesetting-utils/opentype"
Expand Down Expand Up @@ -508,3 +509,47 @@ func TestCFF1SubroutineRequiresReturn(t *testing.T) {
}
}
}

func TestLoadGlyphConcurrent(t *testing.T) {
charstrings := [][]byte{
{139, 139, 21, 140, 141, 5}, // move to (0, 0), line to (1, 2)
{21}, // missing rmoveto arguments
{149, 149, 21, 143, 144, 5}, // a different outline
}
cff := &CFF{Charstrings: charstrings, localSubrs: [][][]byte{nil}}
cff2 := &CFF2{Charstrings: charstrings, fonts: []privateFonts{{}}}
for _, load := range []func(tables.GlyphID) ([]ot.Segment, psinterpreter.PathBounds, error){
cff.LoadGlyph,
func(gid tables.GlyphID) ([]ot.Segment, psinterpreter.PathBounds, error) {
return cff2.LoadGlyph(gid, nil)
},
} {
want, wantBounds, err := load(0)
tu.AssertNoErr(t, err)
tu.Assert(t, len(want) != 0)
want = append([]ot.Segment(nil), want...)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for repeat := 0; repeat < 20; repeat++ {
got, bounds, err := load(0)
if err != nil || bounds != wantBounds || !reflect.DeepEqual(got, want) {
t.Errorf("reused interpreter changed glyph: %v, %v, %v", got, bounds, err)
}
if _, _, err := load(1); err == nil {
t.Error("invalid glyph accepted")
}
if _, _, err := load(2); err != nil {
t.Error(err)
}
if !reflect.DeepEqual(got, want) {
t.Error("later glyph load overwrote returned outline")
}
}
}()
}
wg.Wait()
}
}
8 changes: 6 additions & 2 deletions font/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,13 +617,15 @@ func loadGDEF(ld *ot.Loader, axisCount int, gsub, gpos []byte) (tables.GDEF, err

// Face is a font with user-provided settings.
// Contrary to the [*Font] objects, Faces are NOT safe for concurrent use.
// A Face caches glyph extents and rune to glyph mapping, and should be reused when possible.
// A Face caches glyph extents, advances and rune to glyph mapping, and should be reused when possible.
//
// Also note that an empty [Face] is invalid : the [NewFace] constructor is required to properly init caches.
type Face struct {
*Font

extentsCache extentsCache
hAdvanceCache advanceCache // advances of variable fonts without HVAR, allocated on first use
vAdvanceCache advanceCache // advances of variable fonts without VVAR, allocated on first use
cmapCache cache21_19_8 // supported runes, mapping to GID
cmapNotSupportedCache cache21_0_13 // not supported runes

Expand Down Expand Up @@ -677,6 +679,8 @@ func (f *Face) Coords() []tables.Coord { return f.coords }
// Use [NormalizeVariations] to convert from design (user) space units.
func (f *Face) SetCoords(coords []tables.Coord) {
f.coords = coords
// invalid the cache
// invalid the caches
f.extentsCache.reset()
f.hAdvanceCache.reset()
f.vAdvanceCache.reset()
}
18 changes: 16 additions & 2 deletions font/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,25 @@ func clamp(v float32) float32 {
}

func (f *Face) getGlyphAdvanceVar(gid gID, isVertical bool) float32 {
cache := &f.hAdvanceCache
if isVertical {
cache = &f.vAdvanceCache
}
if *cache == nil {
*cache = newAdvanceCache(f.nGlyphs)
}
if adv, ok := cache.get(gid); ok {
return adv
}
_, phantoms := f.getGlyfPoints(gid, false)
var adv float32
if isVertical {
return clamp(phantoms[phantomTop].Y - phantoms[phantomBottom].Y)
adv = clamp(phantoms[phantomTop].Y - phantoms[phantomBottom].Y)
} else {
adv = clamp(phantoms[phantomRight].X - phantoms[phantomLeft].X)
}
return clamp(phantoms[phantomRight].X - phantoms[phantomLeft].X)
cache.set(gid, adv)
return adv
}

func (f *Face) HorizontalAdvance(gid GID) float32 {
Expand Down
48 changes: 38 additions & 10 deletions font/renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func TestGlyfSegments1(t *testing.T) {

tu.Assert(t, len(f.glyf) == len(expecteds))

face := Face{Font: f}
face := NewFace(f)
for i, expected := range expecteds {
points := face.getPointsForGlyph(gID(i))

Expand All @@ -228,7 +228,7 @@ func TestGlyfSegments1(t *testing.T) {

func BenchmarkBuildSegments(b *testing.B) {
font := loadFont(b, "common/Roboto-BoldItalic.ttf")
face := Face{Font: font}
face := NewFace(font)
gid, ok := face.NominalGlyph('&')
if !ok {
b.Fatal("did not find & in the font")
Expand Down Expand Up @@ -380,7 +380,7 @@ func TestGlyfSegments2(t *testing.T) {
},
}

face := Face{Font: font}
face := NewFace(font)
for i, expected := range expecteds {
points := face.getPointsForGlyph(gID(i))
got := buildSegments(points[:len(points)-phantomCount])
Expand Down Expand Up @@ -478,7 +478,7 @@ func TestGlyphDataCrash(t *testing.T) {
"common/Roboto-BoldItalic.ttf",
} {
font := loadFont(t, filename)
face := Face{Font: font}
face := NewFace(font)
iter := font.Cmap.Iter()
for iter.Next() {
_, g := iter.Char()
Expand All @@ -491,7 +491,7 @@ func TestGlyphDataCrash(t *testing.T) {
"toys/chromacheck-svg.ttf",
} {
font := loadFont(t, filename)
face := Face{Font: font}
face := NewFace(font)
iter := font.Cmap.Iter()
for iter.Next() {
_, g := iter.Char()
Expand All @@ -502,14 +502,16 @@ func TestGlyphDataCrash(t *testing.T) {

func TestSbixGlyph(t *testing.T) {
ft := loadFont(t, "toys/Feat.ttf")
face := Face{Font: ft, xPpem: 100, yPpem: 100}
face := NewFace(ft)
face.SetPpem(100, 100)
data := face.GlyphData(1)
asBitmap, ok := data.(GlyphBitmap)
tu.Assert(t, ok)
tu.Assert(t, asBitmap.Format == PNG)

ft = loadFont(t, "toys/Sbix3.ttf")
face = Face{Font: ft, xPpem: 100, yPpem: 100}
face = NewFace(ft)
face.SetPpem(100, 100)
data = face.GlyphData(4)
asBitmap, ok = data.(GlyphBitmap)
tu.Assert(t, ok)
Expand All @@ -519,7 +521,8 @@ func TestSbixGlyph(t *testing.T) {
func TestCblcGlyph(t *testing.T) {
for _, filename := range td.WithCBLC {
font := loadFont(t, filename.Path)
face := Face{Font: font, xPpem: 94, yPpem: 94}
face := NewFace(font)
face.SetPpem(94, 94)

for gid := filename.GlyphRange[0]; gid <= filename.GlyphRange[1]; gid++ {
data := face.GlyphData(GID(gid))
Expand Down Expand Up @@ -617,7 +620,8 @@ func TestMixedGlyphs(t *testing.T) {
font := loadFont(t, filename)
space, ok := font.NominalGlyph(' ')
tu.Assert(t, ok)
face := Face{Font: font, xPpem: 94, yPpem: 94}
face := NewFace(font)
face.SetPpem(94, 94)

gd := face.GlyphData(space)
tu.Assert(t, gd != nil)
Expand Down Expand Up @@ -655,7 +659,7 @@ func TestGlyphDataMalformedComposite(t *testing.T) {
{Data: tables.CompositeGlyph{Glyphs: []tables.CompositeGlyphPart{{GlyphIndex: 5}}}},
},
}
face := Face{Font: f}
face := NewFace(f)

// must not panic, and yields an empty outline
out, _ := face.GlyphDataOutline(0)
Expand All @@ -665,3 +669,27 @@ func TestGlyphDataMalformedComposite(t *testing.T) {
tu.Assert(t, len(gd.Segments) == 0)
}
}

func BenchmarkGlyphData(b *testing.B) {
for _, filename := range []string{
"toys/CFFTest.otf",
"common/Roboto-BoldItalic.ttf",
"common/NotoSansCJKjp-VF.otf",
} {
font := loadFont(b, filename)
face := NewFace(font)
var gids []GID
iter := font.Cmap.Iter()
for iter.Next() && len(gids) < 200 {
_, g := iter.Char()
gids = append(gids, g)
}
b.Run(filename, func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, g := range gids {
_ = face.GlyphData(g)
}
}
})
}
}
2 changes: 1 addition & 1 deletion font/svg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func TestGlyphDataSVGViewBox(t *testing.T) {
// this font has no viewBox attribute in its SVG document:
// the viewport defaults to the em square
font := loadFont(t, "toys/chromacheck-svg.ttf")
face := Face{Font: font}
face := NewFace(font)

glyph, ok := face.GlyphDataSVG(1)
tu.Assert(t, ok)
Expand Down
14 changes: 11 additions & 3 deletions font/variations.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,12 +342,20 @@ func (gvar gvar) applyDeltasToPoints(glyph gID, coords []VarCoord, points []cont
return
}

// save original points for inferred delta calculation
origPoints := append([]contourPoint(nil), points...)
varData := gvar.variations[glyph]

// keep a copy of the original points for the inferred deltas.
// Only tuples that reference a subset of the points need it.
var origPoints []contourPoint
for _, tuple := range varData {
if tuple.pointNumbers != nil {
origPoints = append([]contourPoint(nil), points...)
break
}
}
// flag is used to indicate referenced point
deltas := make([]contourPoint, len(points))

varData := gvar.variations[glyph]
for _, tuple := range varData {
scalar := tuple.calculateScalar(coords, gvar.sharedTuples, gvar.sharedTupleActiveIdx)
if scalar == 0 {
Expand Down
Loading
Loading