-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
font{,/liberation}: introduce Font, Collection and Cache
Fixes #613.
- Loading branch information
Showing
6 changed files
with
545 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,320 @@ | ||
// Copyright ©2021 The Gonum Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package font | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"golang.org/x/image/font" | ||
"golang.org/x/image/font/opentype" | ||
"golang.org/x/image/font/sfnt" | ||
"golang.org/x/image/math/fixed" | ||
) | ||
|
||
// DefaultCache is the global cache for fonts. | ||
var DefaultCache *Cache = NewCache(nil) | ||
|
||
// Font represents a font face. | ||
type Font struct { | ||
// Typeface identifies the Font. | ||
Typeface Typeface | ||
|
||
// Variant is the variant of a font, such as "Mono" or "Smallcaps". | ||
Variant Variant | ||
|
||
// Style is the style of a font, such as Regular or Italic. | ||
Style font.Style | ||
|
||
// Weight is the weight of a font, such as Normal or Bold. | ||
Weight font.Weight | ||
|
||
// Size is the size of the font. | ||
Size Length | ||
} | ||
|
||
// Name returns a fully qualified name for the given font. | ||
func (f *Font) Name() string { | ||
v := f.Variant | ||
w := weightName(f.Weight) | ||
s := styleName(f.Style) | ||
|
||
switch f.Style { | ||
case font.StyleNormal: | ||
s = "" | ||
if f.Weight == font.WeightNormal { | ||
w = "Regular" | ||
} | ||
default: | ||
if f.Weight == font.WeightNormal { | ||
w = "" | ||
} | ||
} | ||
|
||
return fmt.Sprintf("%s%s-%s%s", f.Typeface, v, w, s) | ||
} | ||
|
||
// From returns a copy of the provided font with its size set. | ||
func From(fnt Font, size Length) Font { | ||
o := fnt | ||
o.Size = size | ||
return o | ||
} | ||
|
||
// Typeface identifies a particular typeface design. | ||
// The empty string denotes the default typeface. | ||
type Typeface string | ||
|
||
// Variant denotes a typeface variant, such as "Mono", "Smallcaps" or "Math". | ||
type Variant string | ||
|
||
// Extents contains font metric information. | ||
type Extents struct { | ||
// Ascent is the distance that the text | ||
// extends above the baseline. | ||
Ascent Length | ||
|
||
// Descent is the distance that the text | ||
// extends below the baseline. The descent | ||
// is given as a positive value. | ||
Descent Length | ||
|
||
// Height is the distance from the lowest | ||
// descending point to the highest ascending | ||
// point. | ||
Height Length | ||
} | ||
|
||
// Face holds a font descriptor and the associated font face. | ||
type Face struct { | ||
Font Font | ||
Face *opentype.Font | ||
} | ||
|
||
// Name returns a fully qualified name for the given font. | ||
func (f *Face) Name() string { | ||
return f.Font.Name() | ||
} | ||
|
||
// FontFace returns the opentype font face for the requested | ||
// dots-per-inch resolution. | ||
func (f *Face) FontFace(dpi float64) font.Face { | ||
face, err := opentype.NewFace(f.Face, &opentype.FaceOptions{ | ||
Size: f.Font.Size.Points(), | ||
DPI: dpi, | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return face | ||
} | ||
|
||
// default hinting for OpenType fonts | ||
const defaultHinting = font.HintingNone | ||
|
||
// Extents returns the FontExtents for a font. | ||
func (f *Face) Extents() Extents { | ||
var ( | ||
// TODO(sbinet): re-use a Font-level sfnt.Buffer instead? | ||
buf sfnt.Buffer | ||
ppem = fixed.Int26_6(f.Face.UnitsPerEm()) | ||
) | ||
|
||
met, err := f.Face.Metrics(&buf, ppem, defaultHinting) | ||
if err != nil { | ||
panic(fmt.Errorf("could not extract font extents: %v", err)) | ||
} | ||
scale := f.Font.Size / Points(float64(ppem)) | ||
return Extents{ | ||
Ascent: Points(float64(met.Ascent)) * scale, | ||
Descent: Points(float64(met.Descent)) * scale, | ||
Height: Points(float64(met.Height)) * scale, | ||
} | ||
} | ||
|
||
// Width returns width of a string when drawn using the font. | ||
func (f *Face) Width(s string) Length { | ||
var ( | ||
pixelsPerEm = fixed.Int26_6(f.Face.UnitsPerEm()) | ||
|
||
// scale converts sfnt.Unit to float64 | ||
scale = f.Font.Size / Points(float64(pixelsPerEm)) | ||
|
||
width = 0 | ||
hasPrev = false | ||
buf sfnt.Buffer | ||
prev, idx sfnt.GlyphIndex | ||
hinting = defaultHinting | ||
) | ||
for _, rune := range s { | ||
var err error | ||
idx, err = f.Face.GlyphIndex(&buf, rune) | ||
if err != nil { | ||
panic(fmt.Errorf("could not get glyph index: %v", err)) | ||
} | ||
if hasPrev { | ||
kern, err := f.Face.Kern(&buf, prev, idx, pixelsPerEm, hinting) | ||
switch { | ||
case err == nil: | ||
width += int(kern) | ||
case errors.Is(err, sfnt.ErrNotFound): | ||
// no-op | ||
default: | ||
panic(fmt.Errorf("could not get kerning: %v", err)) | ||
} | ||
} | ||
adv, err := f.Face.GlyphAdvance(&buf, idx, pixelsPerEm, hinting) | ||
if err != nil { | ||
panic(fmt.Errorf("could not retrieve glyph's advance: %v", err)) | ||
} | ||
width += int(adv) | ||
prev, hasPrev = idx, true | ||
} | ||
return Points(float64(width)) * scale | ||
} | ||
|
||
// Collection is a collection of fonts, regrouped under a common typeface. | ||
type Collection []Face | ||
|
||
// Cache collects font faces. | ||
type Cache struct { | ||
def Typeface | ||
faces map[Font]*opentype.Font | ||
} | ||
|
||
// We make Cache implement dummy GobDecoder and GobEncoder interfaces | ||
// to allow plot.Plot (or any other type holding a Cache) to be (de)serialized | ||
// with encoding/gob. | ||
// As Cache holds opentype.Font, the reflect-based gob (de)serialization can not | ||
// work: gob isn't happy with opentype.Font having no exported field: | ||
// | ||
// error: gob: type font.Cache has no exported fields | ||
// | ||
// FIXME(sbinet): perhaps encode/decode Cache.def typeface? | ||
|
||
func (c *Cache) GobEncode() ([]byte, error) { return nil, nil } | ||
func (c *Cache) GobDecode([]byte) error { | ||
if c.faces == nil { | ||
c.faces = make(map[Font]*opentype.Font) | ||
} | ||
return nil | ||
} | ||
|
||
// NewCache creates a new cache of fonts from the provided collection of | ||
// font Faces. | ||
// The first font Face in the collection is set to be the default one. | ||
func NewCache(coll Collection) *Cache { | ||
cache := &Cache{ | ||
faces: make(map[Font]*opentype.Font, len(coll)), | ||
} | ||
cache.Add(coll) | ||
return cache | ||
} | ||
|
||
// Add adds a whole collection of font Faces to the font cache. | ||
// If the cache is empty, the first font Face in the collection is set | ||
// to be the default one. | ||
func (c *Cache) Add(coll Collection) { | ||
if c.faces == nil { | ||
c.faces = make(map[Font]*opentype.Font, len(coll)) | ||
} | ||
for i, f := range coll { | ||
if i == 0 && c.def == "" { | ||
c.def = f.Font.Typeface | ||
} | ||
fnt := f.Font | ||
fnt.Size = 0 // store all font descriptors with the same size. | ||
c.faces[fnt] = f.Face | ||
} | ||
} | ||
|
||
// Lookup returns the font Face corresponding to the provided Font descriptor, | ||
// with the provided font size set. | ||
// | ||
// If no matching font Face could be found, the one corresponding to | ||
// the default typeface is selected and returned. | ||
func (c *Cache) Lookup(fnt Font, size Length) Face { | ||
if len(c.faces) == 0 { | ||
return Face{} | ||
} | ||
|
||
face := c.lookup(fnt) | ||
if face == nil { | ||
fnt.Typeface = c.def | ||
face = c.lookup(fnt) | ||
} | ||
|
||
ff := Face{ | ||
Font: fnt, | ||
Face: face, | ||
} | ||
ff.Font.Size = size | ||
return ff | ||
} | ||
|
||
// Has returns whether the cache contains the exact font descriptor. | ||
func (c *Cache) Has(fnt Font) bool { | ||
face := c.lookup(fnt) | ||
return face != nil | ||
} | ||
|
||
func (c *Cache) lookup(key Font) *opentype.Font { | ||
key.Size = 0 | ||
|
||
tf := c.faces[key] | ||
if tf == nil { | ||
key := key | ||
key.Weight = font.WeightNormal | ||
tf = c.faces[key] | ||
} | ||
if tf == nil { | ||
key := key | ||
key.Style = font.StyleNormal | ||
tf = c.faces[key] | ||
} | ||
if tf == nil { | ||
key := key | ||
key.Style = font.StyleNormal | ||
key.Weight = font.WeightNormal | ||
tf = c.faces[key] | ||
} | ||
|
||
return tf | ||
} | ||
func weightName(w font.Weight) string { | ||
switch w { | ||
case font.WeightThin: | ||
return "Thin" | ||
case font.WeightExtraLight: | ||
return "ExtraLight" | ||
case font.WeightLight: | ||
return "Light" | ||
case font.WeightNormal: | ||
return "Regular" | ||
case font.WeightMedium: | ||
return "Medium" | ||
case font.WeightSemiBold: | ||
return "SemiBold" | ||
case font.WeightBold: | ||
return "Bold" | ||
case font.WeightExtraBold: | ||
return "ExtraBold" | ||
case font.WeightBlack: | ||
return "Black" | ||
} | ||
return fmt.Sprintf("weight(%d)", w) | ||
} | ||
|
||
func styleName(sty font.Style) string { | ||
switch sty { | ||
case font.StyleNormal: | ||
return "Normal" | ||
case font.StyleItalic: | ||
return "Italic" | ||
case font.StyleOblique: | ||
return "Oblique" | ||
} | ||
return fmt.Sprintf("style(%d)", sty) | ||
} |
Oops, something went wrong.