Skip to content

Commit

Permalink
[font] add a cache for glyph extents
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitkugler committed Apr 5, 2024
1 parent 1019675 commit 52836ef
Show file tree
Hide file tree
Showing 16 changed files with 157 additions and 85 deletions.
38 changes: 38 additions & 0 deletions font/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package font

type glyphExtents struct {
valid bool
extents GlyphExtents
}

type extentsCache []glyphExtents

func (ec extentsCache) get(gid GID) (GlyphExtents, bool) {
if int(gid) >= len(ec) {
return GlyphExtents{}, false
}
ge := ec[gid]
return ge.extents, ge.valid
}

func (ec extentsCache) set(gid GID, extents GlyphExtents) {
ec[gid].valid = true
ec[gid].extents = extents
}

func (ec extentsCache) reset() {
for i := range ec {
ec[i] = glyphExtents{}
}
}

func (f *Face) GlyphExtents(glyph GID) (GlyphExtents, bool) {
if e, ok := f.extentsCache.get(glyph); ok {
return e, ok
}
e, ok := f.glyphExtentsRaw(glyph)
if ok {
f.extentsCache.set(glyph, e)
}
return e, ok
}
65 changes: 46 additions & 19 deletions font/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func ParseTTF(file Resource) (*Face, error) {
if err != nil {
return nil, err
}
return &Face{Font: ft}, nil
return NewFace(ft), nil
}

// ParseTTC parse an Opentype font file, with support for collections.
Expand All @@ -70,7 +70,7 @@ func ParseTTC(file Resource) ([]*Face, error) {
if err != nil {
return nil, fmt.Errorf("reading font %d of collection: %s", i, err)
}
out[i] = &Face{Font: ft}
out[i] = NewFace(ft)
}

return out, nil
Expand Down Expand Up @@ -180,7 +180,8 @@ type Font struct {
GSUB GSUB // An absent table has a nil slice of lookups
GPOS GPOS // An absent table has a nil slice of lookups

upem uint16 // cached value
upem uint16 // cached value
nGlyphs int
}

// NewFont loads all the font tables, sanitizing them.
Expand Down Expand Up @@ -224,6 +225,7 @@ func NewFont(ld *ot.Loader) (*Font, error) {
if err != nil {
return nil, err
}
out.nGlyphs = int(maxp.NumGlyphs)

// We considerer all the following tables as optional,
// since, in practice, users won't have much control on the
Expand All @@ -243,19 +245,19 @@ func NewFont(ld *ot.Loader) (*Font, error) {

raw, _ = ld.RawTable(ot.MustNewTag("glyf"))
locaRaw, _ := ld.RawTable(ot.MustNewTag("loca"))
loca, err := tables.ParseLoca(locaRaw, int(maxp.NumGlyphs), out.head.IndexToLocFormat == 1)
loca, err := tables.ParseLoca(locaRaw, out.nGlyphs, out.head.IndexToLocFormat == 1)
if err == nil { // ParseGlyf panics if len(loca) == 0
out.glyf, _ = tables.ParseGlyf(raw, loca)
}

out.bitmap = selectBitmapTable(ld)

raw, _ = ld.RawTable(ot.MustNewTag("sbix"))
sbix, _, _ := tables.ParseSbix(raw, int(maxp.NumGlyphs))
sbix, _, _ := tables.ParseSbix(raw, out.nGlyphs)
out.sbix = newSbix(sbix)

out.cff, _ = loadCff(ld, int(maxp.NumGlyphs))
out.cff2, _ = loadCff2(ld, int(maxp.NumGlyphs), len(out.fvar))
out.cff, _ = loadCff(ld, out.nGlyphs)
out.cff2, _ = loadCff2(ld, out.nGlyphs, len(out.fvar))

raw, _ = ld.RawTable(ot.MustNewTag("post"))
post, _, _ := tables.ParsePost(raw)
Expand All @@ -265,8 +267,8 @@ func NewFont(ld *ot.Loader) (*Font, error) {
svg, _, _ := tables.ParseSVG(raw)
out.svg, _ = newSvg(svg)

out.hhea, out.hmtx, _ = loadHmtx(ld, int(maxp.NumGlyphs))
out.vhea, out.vmtx, _ = loadVmtx(ld, int(maxp.NumGlyphs))
out.hhea, out.hmtx, _ = loadHmtx(ld, out.nGlyphs)
out.vhea, out.vmtx, _ = loadVmtx(ld, out.nGlyphs)

if axisCount := len(out.fvar); axisCount != 0 {
raw, _ = ld.RawTable(ot.MustNewTag("MVAR"))
Expand Down Expand Up @@ -317,19 +319,19 @@ func NewFont(ld *ot.Loader) (*Font, error) {
}

raw, _ = ld.RawTable(ot.MustNewTag("morx"))
morx, _, _ := tables.ParseMorx(raw, int(maxp.NumGlyphs))
morx, _, _ := tables.ParseMorx(raw, out.nGlyphs)
out.Morx = newMorx(morx)

raw, _ = ld.RawTable(ot.MustNewTag("kerx"))
kerx, _, _ := tables.ParseKerx(raw, int(maxp.NumGlyphs))
kerx, _, _ := tables.ParseKerx(raw, out.nGlyphs)
out.Kerx = newKernxFromKerx(kerx)

raw, _ = ld.RawTable(ot.MustNewTag("kern"))
kern, _, _ := tables.ParseKern(raw)
out.Kern = newKernxFromKern(kern)

raw, _ = ld.RawTable(ot.MustNewTag("ankr"))
out.Ankr, _, _ = tables.ParseAnkr(raw, int(maxp.NumGlyphs))
out.Ankr, _, _ = tables.ParseAnkr(raw, out.nGlyphs)

raw, _ = ld.RawTable(ot.MustNewTag("trak"))
out.Trak, _, _ = tables.ParseTrak(raw)
Expand Down Expand Up @@ -483,15 +485,40 @@ func loadGDEF(ld *ot.Loader, axisCount int) (tables.GDEF, error) {
}

// Face is a font with user-provided settings.
// It is a lightweight wrapper around [*Font], NOT safe for concurrent use.
// Contrary to the [*Font] objects, Faces are NOT safe for concurrent use.
// A Face caches glyph extents and should be reused when possible.
type Face struct {
*Font

// Coords are the current variable coordinates, expressed in normalized units.
// It is empty for non variable fonts.
// Use `SetVariations` to convert from design (user) space units.
Coords []tables.Coord
extentsCache extentsCache

// Horizontal and vertical pixels-per-em (ppem), used to select bitmap sizes.
XPpem, YPpem uint16
coords []tables.Coord
xPpem, yPpem uint16
}

// NewFace wraps [font] and initializes glyph caches.
func NewFace(font *Font) *Face {
return &Face{Font: font, extentsCache: make(extentsCache, font.nGlyphs)}
}

// Ppem returns the horizontal and vertical pixels-per-em (ppem), used to select bitmap sizes.
func (f *Face) Ppem() (x, y uint16) { return f.xPpem, f.yPpem }

// SetPpem applies horizontal and vertical pixels-per-em (ppem).
func (f *Face) SetPpem(x, y uint16) {
f.xPpem, f.yPpem = x, y
// invalid the cache
f.extentsCache.reset()
}

// Coords return a read-only slice of the current variable coordinates, expressed in normalized units.
// It is empty for non variable fonts.
func (f *Face) Coords() []tables.Coord { return f.coords }

// SetCoords applies a list of variation coordinates, expressed in normalized units.
// Use [NormalizeVariations] to convert from design (user) space units.
func (f *Face) SetCoords(coords []tables.Coord) {
f.coords = coords
// invalid the cache
f.extentsCache.reset()
}
2 changes: 1 addition & 1 deletion font/glyphs.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (f *Face) getPointsForGlyph(gid tables.GlyphID, currentDepth int, allPoints
phantoms[phantomBottom].Y = vOrig - vAdv

if f.isVar() {
f.gvar.applyDeltasToPoints(gid, f.Coords, points)
f.gvar.applyDeltasToPoints(gid, f.coords, points)
}

switch data := g.Data.(type) {
Expand Down
50 changes: 25 additions & 25 deletions font/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ func (f *Face) FontHExtents() (FontExtents, bool) {
out FontExtents
ok1, ok2, ok3 bool
)
out.Ascender, ok1 = f.Font.getPositionCommon(metricsTagHorizontalAscender, f.Coords)
out.Descender, ok2 = f.Font.getPositionCommon(metricsTagHorizontalDescender, f.Coords)
out.LineGap, ok3 = f.Font.getPositionCommon(metricsTagHorizontalLineGap, f.Coords)
out.Ascender, ok1 = f.Font.getPositionCommon(metricsTagHorizontalAscender, f.coords)
out.Descender, ok2 = f.Font.getPositionCommon(metricsTagHorizontalDescender, f.coords)
out.LineGap, ok3 = f.Font.getPositionCommon(metricsTagHorizontalLineGap, f.coords)
return out, ok1 && ok2 && ok3
}

Expand All @@ -110,9 +110,9 @@ func (f *Face) FontVExtents() (FontExtents, bool) {
out FontExtents
ok1, ok2, ok3 bool
)
out.Ascender, ok1 = f.Font.getPositionCommon(metricsTagVerticalAscender, f.Coords)
out.Descender, ok2 = f.Font.getPositionCommon(metricsTagVerticalDescender, f.Coords)
out.LineGap, ok3 = f.Font.getPositionCommon(metricsTagVerticalLineGap, f.Coords)
out.Ascender, ok1 = f.Font.getPositionCommon(metricsTagVerticalAscender, f.coords)
out.Descender, ok2 = f.Font.getPositionCommon(metricsTagVerticalDescender, f.coords)
out.LineGap, ok3 = f.Font.getPositionCommon(metricsTagVerticalLineGap, f.coords)
return out, ok1 && ok2 && ok3
}

Expand All @@ -134,27 +134,27 @@ var (
func (f *Face) LineMetric(metric LineMetric) float32 {
switch metric {
case UnderlinePosition:
return f.post.underlinePosition + f.mvar.getVar(tagUnderlineOffset, f.Coords)
return f.post.underlinePosition + f.mvar.getVar(tagUnderlineOffset, f.coords)
case UnderlineThickness:
return f.post.underlineThickness + f.mvar.getVar(tagUnderlineSize, f.Coords)
return f.post.underlineThickness + f.mvar.getVar(tagUnderlineSize, f.coords)
case StrikethroughPosition:
return float32(f.os2.yStrikeoutPosition) + f.mvar.getVar(tagStrikeoutOffset, f.Coords)
return float32(f.os2.yStrikeoutPosition) + f.mvar.getVar(tagStrikeoutOffset, f.coords)
case StrikethroughThickness:
return float32(f.os2.yStrikeoutSize) + f.mvar.getVar(tagStrikeoutSize, f.Coords)
return float32(f.os2.yStrikeoutSize) + f.mvar.getVar(tagStrikeoutSize, f.coords)
case SuperscriptEmYSize:
return float32(f.os2.ySuperscriptYSize) + f.mvar.getVar(tagSuperscriptYSize, f.Coords)
return float32(f.os2.ySuperscriptYSize) + f.mvar.getVar(tagSuperscriptYSize, f.coords)
case SuperscriptEmXOffset:
return float32(f.os2.ySuperscriptXOffset) + f.mvar.getVar(tagSuperscriptXOffset, f.Coords)
return float32(f.os2.ySuperscriptXOffset) + f.mvar.getVar(tagSuperscriptXOffset, f.coords)
case SubscriptEmYSize:
return float32(f.os2.ySubscriptYSize) + f.mvar.getVar(tagSubscriptYSize, f.Coords)
return float32(f.os2.ySubscriptYSize) + f.mvar.getVar(tagSubscriptYSize, f.coords)
case SubscriptEmYOffset:
return float32(f.os2.ySubscriptYOffset) + f.mvar.getVar(tagSubscriptYOffset, f.Coords)
return float32(f.os2.ySubscriptYOffset) + f.mvar.getVar(tagSubscriptYOffset, f.coords)
case SubscriptEmXOffset:
return float32(f.os2.ySubscriptXOffset) + f.mvar.getVar(tagSubscriptXOffset, f.Coords)
return float32(f.os2.ySubscriptXOffset) + f.mvar.getVar(tagSubscriptXOffset, f.coords)
case CapHeight:
return float32(f.os2.sCapHeight) + f.mvar.getVar(tagCapHeight, f.Coords)
return float32(f.os2.sCapHeight) + f.mvar.getVar(tagCapHeight, f.coords)
case XHeight:
return float32(f.os2.sxHeigh) + f.mvar.getVar(tagXHeight, f.Coords)
return float32(f.os2.sxHeigh) + f.mvar.getVar(tagXHeight, f.coords)
default:
return 0
}
Expand Down Expand Up @@ -233,14 +233,14 @@ func (f *Face) HorizontalAdvance(gid GID) float32 {
return float32(advance)
}
if f.hvar != nil {
return float32(advance) + getAdvanceDeltaUnscaled(f.hvar, gID(gid), f.Coords)
return float32(advance) + getAdvanceDeltaUnscaled(f.hvar, gID(gid), f.coords)
}
return f.getGlyphAdvanceVar(gID(gid), false)
}

// return `true` is the font is variable and `Coords` is valid
func (f *Face) isVar() bool {
return len(f.Coords) != 0 && len(f.Coords) == len(f.Font.fvar)
return len(f.coords) != 0 && len(f.coords) == len(f.Font.fvar)
}

// HasVerticalMetrics returns true if a the 'vmtx' table is present.
Expand All @@ -255,7 +255,7 @@ func (f *Face) VerticalAdvance(gid GID) float32 {
return -float32(advance)
}
if f.vvar != nil {
return -float32(advance) - getAdvanceDeltaUnscaled(f.vvar, gID(gid), f.Coords)
return -float32(advance) - getAdvanceDeltaUnscaled(f.vvar, gID(gid), f.coords)
}
return -f.getGlyphAdvanceVar(gID(gid), true)
}
Expand All @@ -276,7 +276,7 @@ func (f *Face) getVerticalSideBearing(glyph gID) int16 {
return sideBearing
}
if f.vvar != nil {
return sideBearing + int16(getLsbDeltaUnscaled(f.vvar, glyph, f.Coords))
return sideBearing + int16(getLsbDeltaUnscaled(f.vvar, glyph, f.coords))
}
return f.getGlyphSideBearingVar(glyph, true)
}
Expand Down Expand Up @@ -390,15 +390,15 @@ func (f *Face) getExtentsFromCff2(glyph gID) (GlyphExtents, bool) {
if f.cff2 == nil {
return GlyphExtents{}, false
}
_, bounds, err := f.cff2.LoadGlyph(glyph, f.Coords)
_, bounds, err := f.cff2.LoadGlyph(glyph, f.coords)
if err != nil {
return GlyphExtents{}, false
}
return bounds.ToExtents(), true
}

func (f *Face) GlyphExtents(glyph GID) (GlyphExtents, bool) {
out, ok := f.getExtentsFromSbix(gID(glyph), f.XPpem, f.YPpem)
func (f *Face) glyphExtentsRaw(glyph GID) (GlyphExtents, bool) {
out, ok := f.getExtentsFromSbix(gID(glyph), f.xPpem, f.yPpem)
if ok {
return out, ok
}
Expand All @@ -414,6 +414,6 @@ func (f *Face) GlyphExtents(glyph GID) (GlyphExtents, bool) {
if ok {
return out, ok
}
out, ok = f.getExtentsFromBitmap(gID(glyph), f.XPpem, f.YPpem)
out, ok = f.getExtentsFromBitmap(gID(glyph), f.xPpem, f.yPpem)
return out, ok
}
6 changes: 3 additions & 3 deletions font/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ type BitmapSize struct {
// not found.
func (f *Face) GlyphData(gid GID) GlyphData {
// since outline may be specified for SVG and bitmaps, check it at the end
outB, err := f.sbix.glyphData(gID(gid), f.XPpem, f.YPpem)
outB, err := f.sbix.glyphData(gID(gid), f.xPpem, f.yPpem)
if err == nil {
outline, ok := f.outlineGlyphData(gID(gid))
if ok {
Expand All @@ -118,7 +118,7 @@ func (f *Face) GlyphData(gid GID) GlyphData {
return outB
}

outB, err = f.bitmap.glyphData(gID(gid), f.XPpem, f.YPpem)
outB, err = f.bitmap.glyphData(gID(gid), f.xPpem, f.yPpem)
if err == nil {
outline, ok := f.outlineGlyphData(gID(gid))
if ok {
Expand Down Expand Up @@ -401,7 +401,7 @@ func (f *Face) glyphDataFromCFF2(glyph gID) (GlyphOutline, error) {
if f.cff2 == nil {
return GlyphOutline{}, errNoCFF2Table
}
segments, _, err := f.cff2.LoadGlyph(glyph, f.Coords)
segments, _, err := f.cff2.LoadGlyph(glyph, f.coords)
if err != nil {
return GlyphOutline{}, err
}
Expand Down
10 changes: 5 additions & 5 deletions font/renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,14 +505,14 @@ 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 := Face{Font: ft, xPpem: 100, yPpem: 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 = Face{Font: ft, xPpem: 100, yPpem: 100}
data = face.GlyphData(4)
asBitmap, ok = data.(GlyphBitmap)
tu.Assert(t, ok)
Expand All @@ -522,7 +522,7 @@ 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 := Face{Font: font, xPpem: 94, yPpem: 94}

for gid := filename.GlyphRange[0]; gid <= filename.GlyphRange[1]; gid++ {
data := face.GlyphData(GID(gid))
Expand Down Expand Up @@ -579,7 +579,7 @@ func TestAppleBitmapGlyph(t *testing.T) {
ft, err := NewFont(fonts[0])
tu.AssertNoErr(t, err)

face := Face{Font: ft, XPpem: 94, YPpem: 94}
face := Face{Font: ft, xPpem: 94, yPpem: 94}

runes := "The quick brown fox jumps over the lazy dog"
for _, r := range runes {
Expand All @@ -601,7 +601,7 @@ 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 := Face{Font: font, xPpem: 94, yPpem: 94}

gd := face.GlyphData(space)
tu.Assert(t, gd != nil)
Expand Down
Loading

0 comments on commit 52836ef

Please sign in to comment.