Files
gio/font/opentype/opentype.go
T
Chris Waldon ccf24c0bd2 font/opentype: make reusing font.Face efficient and safe
This commit updates the internal representation of a font to separate the
threadsafe and non-threadsafe operations in a way that enures font.Faces can
be shared by all text shapers in an application. This should ensure that applications
only need to parse fonts a single time, saving a great deal of memory for
applications that open many windows (which each need a different text shaper).

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-21 13:21:37 -06:00

153 lines
4.1 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
// Package opentype implements text layout and shaping for OpenType
// files.
//
// NOTE: the OpenType specification allows for fonts to include bitmap images
// in a variety of formats. In the interest of small binary sizes, the opentype
// package only automatically imports the PNG image decoder. If you have a font
// with glyphs in JPEG or TIFF formats, register those decoders with the image
// package in order to ensure those glyphs are visible in text.
package opentype
import (
"bytes"
"fmt"
_ "image/png"
giofont "gioui.org/font"
"github.com/go-text/typesetting/font"
fontapi "github.com/go-text/typesetting/opentype/api/font"
"github.com/go-text/typesetting/opentype/api/metadata"
"github.com/go-text/typesetting/opentype/loader"
)
// Face is a thread-safe representation of a loaded font. For efficiency, applications
// should construct a face for any given font file once, reusing it across different
// text shapers.
type Face struct {
face font.Font
aspect metadata.Aspect
family string
variant string
}
// Parse constructs a Face from source bytes.
func Parse(src []byte) (Face, error) {
ld, err := loader.NewLoader(bytes.NewReader(src))
if err != nil {
return Face{}, err
}
font, aspect, family, variant, err := parseLoader(ld)
if err != nil {
return Face{}, fmt.Errorf("failed parsing truetype font: %w", err)
}
return Face{
face: font,
aspect: aspect,
family: family,
variant: variant,
}, nil
}
// ParseCollection parse an Opentype font file, with support for collections.
// Single font files are supported, returning a slice with length 1.
// The returned fonts are automatically wrapped in a text.FontFace with
// inferred font metadata.
// BUG(whereswaldon): the only Variant that can be detected automatically is
// "Mono".
func ParseCollection(src []byte) ([]giofont.FontFace, error) {
lds, err := loader.NewLoaders(bytes.NewReader(src))
if err != nil {
return nil, err
}
out := make([]giofont.FontFace, len(lds))
for i, ld := range lds {
face, aspect, family, variant, err := parseLoader(ld)
if err != nil {
return nil, fmt.Errorf("reading font %d of collection: %s", i, err)
}
ff := Face{
face: face,
aspect: aspect,
family: family,
variant: variant,
}
out[i] = giofont.FontFace{
Face: ff,
Font: ff.Font(),
}
}
return out, nil
}
// parseLoader parses the contents of the loader into a face and its metadata.
func parseLoader(ld *loader.Loader) (_ font.Font, _ metadata.Aspect, family, variant string, _ error) {
ft, err := fontapi.NewFont(ld)
if err != nil {
return nil, metadata.Aspect{}, "", "", err
}
data := metadata.Metadata(ld)
if data.IsMonospace {
variant = "Mono"
}
return ft, data.Aspect, data.Family, variant, nil
}
// Face returns a thread-unsafe wrapper for this Face suitable for use by a single shaper.
// Face many be invoked any number of times and is safe so long as each return value is
// only used by one goroutine.
func (f Face) Face() font.Face {
return &fontapi.Face{Font: f.face}
}
// FontFace returns a text.Font with populated font metadata for the
// font.
// BUG(whereswaldon): the only Variant that can be detected automatically is
// "Mono".
func (f Face) Font() giofont.Font {
return giofont.Font{
Typeface: giofont.Typeface(f.family),
Style: f.style(),
Weight: f.weight(),
Variant: giofont.Variant(f.variant),
}
}
func (f Face) style() giofont.Style {
switch f.aspect.Style {
case metadata.StyleItalic:
return giofont.Italic
case metadata.StyleNormal:
fallthrough
default:
return giofont.Regular
}
}
func (f Face) weight() giofont.Weight {
switch f.aspect.Weight {
case metadata.WeightThin:
return giofont.Thin
case metadata.WeightExtraLight:
return giofont.ExtraLight
case metadata.WeightLight:
return giofont.Light
case metadata.WeightNormal:
return giofont.Normal
case metadata.WeightMedium:
return giofont.Medium
case metadata.WeightSemibold:
return giofont.SemiBold
case metadata.WeightBold:
return giofont.Bold
case metadata.WeightExtraBold:
return giofont.ExtraBold
case metadata.WeightBlack:
return giofont.Black
default:
return giofont.Normal
}
}