font{,/{opentype,gofont}},text: [API] drop monospace font metadata

In the general case, it isn't possible for us to efficiently find system fonts that
are monospace. Fonts don't advertise being monospace frequently, so the only way to
reliably detect it is to check that all glyphs are the same width. This is expensive,
far too much so to be done on every system font when there may be thousands of them.

Other font resolution systems rely upon the user requesting fonts by their family name.
If you want a monospace font, ask for it by name or use a generic name like 'monospace'.
This will be Gio's approach from here on out.

Existing code relying upon setting Variant="Mono" should instead set Typeface="Go Mono"
(for the Go font) or specify another monospace typeface. The generic face "monospace"
will search for one of a set of known monospace fonts that may be available on the system.

Similarly, smallcaps isn't well advertised and users should rely on requesting all-smallcaps
fonts by typeface. To get the Go smallcaps font, use Typeface="Go Smallcaps".

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-06-22 15:00:22 -04:00
committed by Elias Naur
parent 5606a961f2
commit 15031d0b52
4 changed files with 92 additions and 55 deletions
+4 -6
View File
@@ -20,10 +20,11 @@ type Weight int
// Font specify a particular typeface variant, style and weight. // Font specify a particular typeface variant, style and weight.
type Font struct { type Font struct {
// Typeface specifies the name of the family of faces.
Typeface Typeface Typeface Typeface
Variant Variant // Style specifies the kind of text style.
Style Style Style Style
// Weight is the text weight. If zero, Normal is used instead. // Weight is the text weight.
Weight Weight Weight Weight
} }
@@ -37,9 +38,6 @@ type Face interface {
// string denotes the default typeface. // string denotes the default typeface.
type Typeface string type Typeface string
// Variant denotes a typeface variant such as "Mono" or "Smallcaps".
type Variant string
const ( const (
Regular Style = iota Regular Style = iota
Italic Italic
+16 -17
View File
@@ -37,11 +37,11 @@ var (
func loadRegular() { func loadRegular() {
regOnce.Do(func() { regOnce.Do(func() {
face, err := opentype.Parse(goregular.TTF) faces, err := opentype.ParseCollection(goregular.TTF)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to parse font: %v", err)) panic(fmt.Errorf("failed to parse font: %v", err))
} }
reg = []font.FontFace{{Font: font.Font{Typeface: "Go"}, Face: face}} reg = faces
collection = append(collection, reg[0]) collection = append(collection, reg[0])
}) })
} }
@@ -56,17 +56,17 @@ func Regular() []font.FontFace {
func Collection() []font.FontFace { func Collection() []font.FontFace {
loadRegular() loadRegular()
once.Do(func() { once.Do(func() {
register(font.Font{Style: font.Italic}, goitalic.TTF) register(goitalic.TTF)
register(font.Font{Weight: font.Bold}, gobold.TTF) register(gobold.TTF)
register(font.Font{Style: font.Italic, Weight: font.Bold}, gobolditalic.TTF) register(gobolditalic.TTF)
register(font.Font{Weight: font.Medium}, gomedium.TTF) register(gomedium.TTF)
register(font.Font{Weight: font.Medium, Style: font.Italic}, gomediumitalic.TTF) register(gomediumitalic.TTF)
register(font.Font{Variant: "Mono"}, gomono.TTF) register(gomono.TTF)
register(font.Font{Variant: "Mono", Weight: font.Bold}, gomonobold.TTF) register(gomonobold.TTF)
register(font.Font{Variant: "Mono", Weight: font.Bold, Style: font.Italic}, gomonobolditalic.TTF) register(gomonobolditalic.TTF)
register(font.Font{Variant: "Mono", Style: font.Italic}, gomonoitalic.TTF) register(gomonoitalic.TTF)
register(font.Font{Variant: "Smallcaps"}, gosmallcaps.TTF) register(gosmallcaps.TTF)
register(font.Font{Variant: "Smallcaps", Style: font.Italic}, gosmallcapsitalic.TTF) register(gosmallcapsitalic.TTF)
// Ensure that any outside appends will not reuse the backing store. // Ensure that any outside appends will not reuse the backing store.
n := len(collection) n := len(collection)
collection = collection[:n:n] collection = collection[:n:n]
@@ -74,11 +74,10 @@ func Collection() []font.FontFace {
return collection return collection
} }
func register(fnt font.Font, ttf []byte) { func register(ttf []byte) {
face, err := opentype.Parse(ttf) faces, err := opentype.ParseCollection(ttf)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to parse font: %v", err)) panic(fmt.Errorf("failed to parse font: %v", err))
} }
fnt.Typeface = "Go" collection = append(collection, faces[0])
collection = append(collection, font.FontFace{Font: fnt, Face: face})
} }
+71 -31
View File
@@ -26,10 +26,8 @@ import (
// should construct a face for any given font file once, reusing it across different // should construct a face for any given font file once, reusing it across different
// text shapers. // text shapers.
type Face struct { type Face struct {
face font.Font face font.Font
aspect metadata.Aspect font giofont.Font
family string
variant string
} }
// Parse constructs a Face from source bytes. // Parse constructs a Face from source bytes.
@@ -38,15 +36,13 @@ func Parse(src []byte) (Face, error) {
if err != nil { if err != nil {
return Face{}, err return Face{}, err
} }
font, aspect, family, variant, err := parseLoader(ld) font, md, err := parseLoader(ld)
if err != nil { if err != nil {
return Face{}, fmt.Errorf("failed parsing truetype font: %w", err) return Face{}, fmt.Errorf("failed parsing truetype font: %w", err)
} }
return Face{ return Face{
face: font, face: font,
aspect: aspect, font: md,
family: family,
variant: variant,
}, nil }, nil
} }
@@ -63,15 +59,13 @@ func ParseCollection(src []byte) ([]giofont.FontFace, error) {
} }
out := make([]giofont.FontFace, len(lds)) out := make([]giofont.FontFace, len(lds))
for i, ld := range lds { for i, ld := range lds {
face, aspect, family, variant, err := parseLoader(ld) face, md, err := parseLoader(ld)
if err != nil { if err != nil {
return nil, fmt.Errorf("reading font %d of collection: %s", i, err) return nil, fmt.Errorf("reading font %d of collection: %s", i, err)
} }
ff := Face{ ff := Face{
face: face, face: face,
aspect: aspect, font: md,
family: family,
variant: variant,
} }
out[i] = giofont.FontFace{ out[i] = giofont.FontFace{
Face: ff, Face: ff,
@@ -82,17 +76,32 @@ func ParseCollection(src []byte) ([]giofont.FontFace, error) {
return out, nil return out, nil
} }
func DescriptionToFont(md metadata.Description) giofont.Font {
return giofont.Font{
Typeface: giofont.Typeface(md.Family),
Style: gioStyle(md.Aspect.Style),
Weight: gioWeight(md.Aspect.Weight),
}
}
func FontToDescription(font giofont.Font) metadata.Description {
return metadata.Description{
Family: string(font.Typeface),
Aspect: metadata.Aspect{
Style: mdStyle(font.Style),
Weight: mdWeight(font.Weight),
},
}
}
// parseLoader parses the contents of the loader into a face and its metadata. // 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) { func parseLoader(ld *loader.Loader) (font.Font, giofont.Font, error) {
ft, err := fontapi.NewFont(ld) ft, err := fontapi.NewFont(ld)
if err != nil { if err != nil {
return nil, metadata.Aspect{}, "", "", err return nil, giofont.Font{}, err
} }
data := metadata.Metadata(ld) data := DescriptionToFont(metadata.Metadata(ld))
if data.IsMonospace { return ft, data, nil
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 returns a thread-unsafe wrapper for this Face suitable for use by a single shaper.
@@ -107,16 +116,11 @@ func (f Face) Face() font.Face {
// BUG(whereswaldon): the only Variant that can be detected automatically is // BUG(whereswaldon): the only Variant that can be detected automatically is
// "Mono". // "Mono".
func (f Face) Font() giofont.Font { func (f Face) Font() giofont.Font {
return giofont.Font{ return f.font
Typeface: giofont.Typeface(f.family),
Style: f.style(),
Weight: f.weight(),
Variant: giofont.Variant(f.variant),
}
} }
func (f Face) style() giofont.Style { func gioStyle(s metadata.Style) giofont.Style {
switch f.aspect.Style { switch s {
case metadata.StyleItalic: case metadata.StyleItalic:
return giofont.Italic return giofont.Italic
case metadata.StyleNormal: case metadata.StyleNormal:
@@ -126,8 +130,19 @@ func (f Face) style() giofont.Style {
} }
} }
func (f Face) weight() giofont.Weight { func mdStyle(g giofont.Style) metadata.Style {
switch f.aspect.Weight { switch g {
case giofont.Italic:
return metadata.StyleItalic
case giofont.Regular:
fallthrough
default:
return metadata.StyleNormal
}
}
func gioWeight(w metadata.Weight) giofont.Weight {
switch w {
case metadata.WeightThin: case metadata.WeightThin:
return giofont.Thin return giofont.Thin
case metadata.WeightExtraLight: case metadata.WeightExtraLight:
@@ -150,3 +165,28 @@ func (f Face) weight() giofont.Weight {
return giofont.Normal return giofont.Normal
} }
} }
func mdWeight(g giofont.Weight) metadata.Weight {
switch g {
case giofont.Thin:
return metadata.WeightThin
case giofont.ExtraLight:
return metadata.WeightExtraLight
case giofont.Light:
return metadata.WeightLight
case giofont.Normal:
return metadata.WeightNormal
case giofont.Medium:
return metadata.WeightMedium
case giofont.SemiBold:
return metadata.WeightSemibold
case giofont.Bold:
return metadata.WeightBold
case giofont.ExtraBold:
return metadata.WeightExtraBold
case giofont.Black:
return metadata.WeightBlack
default:
return metadata.WeightNormal
}
}
+1 -1
View File
@@ -895,7 +895,7 @@ func closestFont(lookup giofont.Font, available []giofont.Font) (giofont.Font, b
if cf == lookup { if cf == lookup {
return lookup, true return lookup, true
} }
if cf.Typeface != lookup.Typeface || cf.Variant != lookup.Variant || cf.Style != lookup.Style { if cf.Typeface != lookup.Typeface || cf.Style != lookup.Style {
continue continue
} }
if !found { if !found {