font/opentype: [API] support font collection loading

This commit adds back support for loading font collections, which we
lost when switching to the harfbuzz-based shaper last January. In
addition, this commit takes advantage of our new font loading library's
metadata facilities to automatically construct text.FontFaces for all
fonts within a collection. This is significantly more ergonomic for
users, and can be used to load single fonts with automatic metadata
detection as well.

I've exposed a opentype.Face.Font() method that can be used to get the
font metadata for a given face as well, though you have to type assert to
see it:

var myFace text.Face
if asOpentype, ok := myFace.(opentype.Face); ok {
    myFont := asOpentype.Font()
}

The one problem with this approach is that the font variant field always
be automatically populated. Mono font detection is supported, but
other variants like SmallCaps are more complicated and may need to be
expressed differently in the future (smallcaps is a feature that any font
file can have, not necessarily a separate font file). See this [0] upstream
issue for details.

Additionally, in order to avoid import cycles, I've moved the declarations
of font attributes to package font. You can fix your code automatically to
refer to the new definitions by running the following:

    gofmt -w -r 'text.FontFace -> font.FontFace' .
    gofmt -w -r 'text.Variant -> font.Variant' .
    gofmt -w -r 'text.Style -> font.Style' .
    gofmt -w -r 'text.Typeface -> font.Typeface' .
    gofmt -w -r 'text.Font -> font.Font' .
    gofmt -w -r 'text.Regular -> font.Regular' .
    gofmt -w -r 'text.Italic -> font.Italic' .
    gofmt -w -r 'text.Thin -> font.Thin' .
    gofmt -w -r 'text.ExtraLight -> font.ExtraLight' .
    gofmt -w -r 'text.Light -> font.Light' .
    gofmt -w -r 'text.Normal -> font.Normal' .
    gofmt -w -r 'text.Medium -> font.Medium' .
    gofmt -w -r 'text.SemiBold -> font.SemiBold' .
    gofmt -w -r 'text.Bold -> font.Bold' .
    gofmt -w -r 'text.ExtraBold -> font.ExtraBold' .
    gofmt -w -r 'text.Black -> font.Black' .
    gofmt -w -r 'text.Hairline -> font.Thin' .
    gofmt -w -r 'text.UltraLight -> font.ExtraLight' .
    gofmt -w -r 'text.DemiBold -> font.SemiBold' .
    gofmt -w -r 'text.UltraBold -> font.ExtraBold' .
    gofmt -w -r 'text.Heavy -> font.Black' .
    gofmt -w -r 'text.ExtraBlack -> font.Black+50' .
    gofmt -w -r 'text.UltraBlack -> font.ExtraBlack' .

Make sure each affected file imports gioui.org/font.

[0] https://github.com/go-text/typesetting/issues/57

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-04-07 13:24:51 -04:00
committed by Elias Naur
parent cda73efa6a
commit f77bf9a42c
25 changed files with 366 additions and 241 deletions
+94
View File
@@ -0,0 +1,94 @@
/*
Package font provides type describing font faces attributes.
*/
package font
import "github.com/go-text/typesetting/font"
// A FontFace is a Font and a matching Face.
type FontFace struct {
Font Font
Face Face
}
// Style is the font style.
type Style int
// Weight is a font weight, in CSS units subtracted 400 so the zero value
// is normal text weight.
type Weight int
// Font specify a particular typeface variant, style and weight.
type Font struct {
Typeface Typeface
Variant Variant
Style Style
// Weight is the text weight. If zero, Normal is used instead.
Weight Weight
}
// Face is an opaque handle to a typeface. The concrete implementation depends
// upon the kind of font and shaper in use.
type Face interface {
Face() font.Face
}
// Typeface identifies a particular typeface design. The empty
// string denotes the default typeface.
type Typeface string
// Variant denotes a typeface variant such as "Mono" or "Smallcaps".
type Variant string
const (
Regular Style = iota
Italic
)
const (
Thin Weight = -300
ExtraLight Weight = -200
Light Weight = -100
Normal Weight = 0
Medium Weight = 100
SemiBold Weight = 200
Bold Weight = 300
ExtraBold Weight = 400
Black Weight = 500
)
func (s Style) String() string {
switch s {
case Regular:
return "Regular"
case Italic:
return "Italic"
default:
panic("invalid Style")
}
}
func (w Weight) String() string {
switch w {
case Thin:
return "Thin"
case ExtraLight:
return "ExtraLight"
case Light:
return "Light"
case Normal:
return "Normal"
case Medium:
return "Medium"
case SemiBold:
return "SemiBold"
case Bold:
return "Bold"
case ExtraBold:
return "ExtraBold"
case Black:
return "Black"
default:
panic("invalid Weight")
}
}
+17 -17
View File
@@ -24,29 +24,29 @@ import (
"golang.org/x/image/font/gofont/gosmallcaps"
"golang.org/x/image/font/gofont/gosmallcapsitalic"
"gioui.org/font"
"gioui.org/font/opentype"
"gioui.org/text"
)
var (
once sync.Once
collection []text.FontFace
collection []font.FontFace
)
func Collection() []text.FontFace {
func Collection() []font.FontFace {
once.Do(func() {
register(text.Font{}, goregular.TTF)
register(text.Font{Style: text.Italic}, goitalic.TTF)
register(text.Font{Weight: text.Bold}, gobold.TTF)
register(text.Font{Style: text.Italic, Weight: text.Bold}, gobolditalic.TTF)
register(text.Font{Weight: text.Medium}, gomedium.TTF)
register(text.Font{Weight: text.Medium, Style: text.Italic}, gomediumitalic.TTF)
register(text.Font{Variant: "Mono"}, gomono.TTF)
register(text.Font{Variant: "Mono", Weight: text.Bold}, gomonobold.TTF)
register(text.Font{Variant: "Mono", Weight: text.Bold, Style: text.Italic}, gomonobolditalic.TTF)
register(text.Font{Variant: "Mono", Style: text.Italic}, gomonoitalic.TTF)
register(text.Font{Variant: "Smallcaps"}, gosmallcaps.TTF)
register(text.Font{Variant: "Smallcaps", Style: text.Italic}, gosmallcapsitalic.TTF)
register(font.Font{}, goregular.TTF)
register(font.Font{Style: font.Italic}, goitalic.TTF)
register(font.Font{Weight: font.Bold}, gobold.TTF)
register(font.Font{Style: font.Italic, Weight: font.Bold}, gobolditalic.TTF)
register(font.Font{Weight: font.Medium}, gomedium.TTF)
register(font.Font{Weight: font.Medium, Style: font.Italic}, gomediumitalic.TTF)
register(font.Font{Variant: "Mono"}, gomono.TTF)
register(font.Font{Variant: "Mono", Weight: font.Bold}, gomonobold.TTF)
register(font.Font{Variant: "Mono", Weight: font.Bold, Style: font.Italic}, gomonobolditalic.TTF)
register(font.Font{Variant: "Mono", Style: font.Italic}, gomonoitalic.TTF)
register(font.Font{Variant: "Smallcaps"}, gosmallcaps.TTF)
register(font.Font{Variant: "Smallcaps", Style: font.Italic}, gosmallcapsitalic.TTF)
// Ensure that any outside appends will not reuse the backing store.
n := len(collection)
collection = collection[:n:n]
@@ -54,11 +54,11 @@ func Collection() []text.FontFace {
return collection
}
func register(fnt text.Font, ttf []byte) {
func register(fnt font.Font, ttf []byte) {
face, err := opentype.Parse(ttf)
if err != nil {
panic(fmt.Errorf("failed to parse font: %v", err))
}
fnt.Typeface = "Go"
collection = append(collection, text.FontFace{Font: fnt, Face: face})
collection = append(collection, font.FontFace{Font: fnt, Face: face})
}
+113 -3
View File
@@ -15,23 +15,133 @@ import (
"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 shapeable representation of a font.
type Face struct {
face font.Face
face font.Face
aspect metadata.Aspect
family string
variant string
}
// Parse constructs a Face from source bytes.
func Parse(src []byte) (Face, error) {
face, err := font.ParseTTF(bytes.NewReader(src))
ld, err := loader.NewLoader(bytes.NewReader(src))
if err != nil {
return Face{}, err
}
face, aspect, family, variant, err := parseLoader(ld)
if err != nil {
return Face{}, fmt.Errorf("failed parsing truetype font: %w", err)
}
return Face{face: face}, nil
return Face{
face: face,
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.Face, _ 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 &fontapi.Face{Font: ft}, data.Aspect, data.Family, variant, nil
}
func (f Face) Face() font.Face {
return 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
}
}