Skip to content

Commit

Permalink
[fontscan] expose system fonts
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitkugler committed Mar 26, 2024
1 parent c7936fe commit e556800
Show file tree
Hide file tree
Showing 13 changed files with 422 additions and 394 deletions.
31 changes: 25 additions & 6 deletions fontscan/fontmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ type Logger interface {
// The family substitution algorithm is copied from fontconfig
// and the match algorithm is inspired from Rust font-kit library

// SystemFonts loads the system fonts, using an index stored in [cacheDir].
// See [FontMap.UseSystemFonts] for more details.
//
// If [logger] is nil, log.Default() is used.
func SystemFonts(logger Logger, cacheDir string) ([]Footprint, error) {
if logger == nil {
logger = log.New(log.Writer(), "fontscan", log.Flags())
}

// safe for concurrent use; subsequent calls are no-ops
err := initSystemFonts(logger, cacheDir)
if err != nil {
return nil, err
}

// systemFonts is read-only, so may be used concurrently
return systemFonts.flatten(), nil
}

// FontMap provides a mechanism to select a [font.Face] from a font description.
// It supports system and user-provided fonts, and implements the CSS font substitutions
// rules.
Expand Down Expand Up @@ -121,13 +140,13 @@ func (fm *FontMap) UseSystemFonts(cacheDir string) error {

// appendFootprints adds the provided footprints to the database and maps their script
// coverage.
func (fm *FontMap) appendFootprints(footprints ...footprint) {
func (fm *FontMap) appendFootprints(footprints ...Footprint) {
startIdx := len(fm.database)
fm.database = append(fm.database, footprints...)
// Insert entries into scriptMap for each footprint's covered scripts.
for i, fp := range footprints {
dbIdx := startIdx + i
for _, script := range fp.scripts {
for _, script := range fp.Scripts {
fm.scriptMap[script] = append(fm.scriptMap[script], dbIdx)
}
}
Expand Down Expand Up @@ -245,7 +264,7 @@ func (fm *FontMap) AddFont(fontFile font.Resource, fileID, familyName string) er
panic("internal error: inconsistent font descriptors and loader")
}

var addedFonts []footprint
var addedFonts []Footprint
for i, fontDesc := range loaders {
fp, _, err := newFootprintFromLoader(fontDesc, true, scanBuffer{})
// the font won't be usable, just ignore it
Expand Down Expand Up @@ -293,7 +312,7 @@ func (fm *FontMap) AddFace(face font.Face, location Location, md meta.Descriptio
fm.lru.Clear()
}

func (fm *FontMap) cache(fp footprint, face font.Face) {
func (fm *FontMap) cache(fp Footprint, face font.Face) {
if fm.firstFace == nil {
fm.firstFace = face
}
Expand Down Expand Up @@ -460,7 +479,7 @@ func (fm *FontMap) resolveForRune(candidates []int, r rune) font.Face {
func (fm *FontMap) resolveForLang(candidates []int, lang LangID) font.Face {
for _, footprintIndex := range candidates {
// check the coverage
if fp := fm.database[footprintIndex]; fp.langs.contains(lang) {
if fp := fm.database[footprintIndex]; fp.Langs.Contains(lang) {
// try to use the font
face, err := fm.loadFont(fp)
if err != nil { // very unlikely; try another family
Expand Down Expand Up @@ -596,7 +615,7 @@ func (fm *FontMap) ResolveFaceForLang(lang LangID) font.Face {
return nil
}

func (fm *FontMap) loadFont(fp footprint) (font.Face, error) {
func (fm *FontMap) loadFont(fp Footprint) (font.Face, error) {
if face, hasCached := fm.faceCache[fp.Location]; hasCached {
return face, nil
}
Expand Down
11 changes: 9 additions & 2 deletions fontscan/fontmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,13 @@ func TestInitSystemFonts(t *testing.T) {
tu.AssertC(t, len(systemFonts.flatten()) != 0, "systemFonts should not be empty")
}

func TestSystemFonts(t *testing.T) {
fonts, err := SystemFonts(nil, t.TempDir())
tu.AssertNoErr(t, err)

tu.AssertC(t, len(fonts) != 0, "systemFonts should not be empty")
}

func TestFontMap_AddFont_FaceLocation(t *testing.T) {
file1, err := os.Open("../font/testdata/Amiri-Regular.ttf")
tu.AssertNoErr(t, err)
Expand Down Expand Up @@ -290,11 +297,11 @@ func TestFindSytemFont(t *testing.T) {
tu.Assert(t, !ok) // no match on an empty fontmap

// simulate system fonts
fm.appendFootprints(footprint{
fm.appendFootprints(Footprint{
Family: meta.NormalizeFamily("Nimbus"),
Location: Location{File: "nimbus.ttf"},
},
footprint{
Footprint{
Family: meta.NormalizeFamily("Noto Sans"),
Location: Location{File: "noto.ttf"},
isUserProvided: true,
Expand Down
36 changes: 18 additions & 18 deletions fontscan/footprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import (
// Location identifies where a font.Face is stored.
type Location = api.FontID

// footprint is a condensed summary of the main information
// Footprint is a condensed summary of the main information
// about a font, serving as a lightweight surrogate
// for the original font file.
type footprint struct {
type Footprint struct {
// Location stores the adress of the font resource.
Location Location

Expand All @@ -28,13 +28,13 @@ type footprint struct {
Family string

// Runes is the set of runes supported by the font.
Runes runeSet
Runes RuneSet

// set of scripts deduced from Runes
scripts scriptSet
// Scripts is the set of scripts deduced from [Runes]
Scripts ScriptSet

// set of languages deduced from Runes
langs langSet
// Langs is the set of languages deduced from [Runes]
Langs LangSet

// Aspect precises the visual characteristics
// of the font among a family, like "Bold Italic"
Expand All @@ -50,17 +50,17 @@ type footprint struct {
isUserProvided bool
}

func newFootprintFromFont(f font.Font, location Location, md meta.Description) (out footprint) {
out.Runes, out.scripts, _ = newCoveragesFromCmap(f.Cmap, nil)
out.langs = newLangsetFromCoverage(out.Runes)
func newFootprintFromFont(f font.Font, location Location, md meta.Description) (out Footprint) {
out.Runes, out.Scripts, _ = newCoveragesFromCmap(f.Cmap, nil)
out.Langs = newLangsetFromCoverage(out.Runes)
out.Family = meta.NormalizeFamily(md.Family)
out.Aspect = md.Aspect
out.Location = location
out.isUserProvided = true
return out
}

func newFootprintFromLoader(ld *loader.Loader, isUserProvided bool, buffer scanBuffer) (out footprint, _ scanBuffer, err error) {
func newFootprintFromLoader(ld *loader.Loader, isUserProvided bool, buffer scanBuffer) (out Footprint, _ scanBuffer, err error) {
raw := buffer.tableBuffer

// since raw is shared, special car must be taken in the parsing order
Expand All @@ -75,20 +75,20 @@ func newFootprintFromLoader(ld *loader.Loader, isUserProvided bool, buffer scanB
// the input slice
raw, err = ld.RawTableTo(loader.MustNewTag("cmap"), raw)
if err != nil {
return footprint{}, buffer, err
return Footprint{}, buffer, err
}
tb, _, err := tables.ParseCmap(raw)
if err != nil {
return footprint{}, buffer, err
return Footprint{}, buffer, err
}
cmap, _, err := api.ProcessCmap(tb, fp)
if err != nil {
return footprint{}, buffer, err
return Footprint{}, buffer, err
}

out.Runes, out.scripts, buffer.cmapBuffer = newCoveragesFromCmap(cmap, buffer.cmapBuffer) // ... and build the corresponding rune set
out.Runes, out.Scripts, buffer.cmapBuffer = newCoveragesFromCmap(cmap, buffer.cmapBuffer) // ... and build the corresponding rune set

out.langs = newLangsetFromCoverage(out.Runes)
out.Langs = newLangsetFromCoverage(out.Runes)

family, aspect, raw := meta.Describe(ld, raw)
out.Family = meta.NormalizeFamily(family)
Expand All @@ -101,15 +101,15 @@ func newFootprintFromLoader(ld *loader.Loader, isUserProvided bool, buffer scanB
}

// loadFromDisk assume the footprint location refers to the file system
func (fp *footprint) loadFromDisk() (font.Face, error) {
func (fp *Footprint) loadFromDisk() (font.Face, error) {
location := fp.Location

file, err := os.Open(location.File)
if err != nil {
return nil, err
}
defer file.Close()

faces, err := font.ParseTTC(file)
if err != nil {
return nil, err
Expand Down
18 changes: 9 additions & 9 deletions fontscan/langset.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,26 @@ func NewLangID(l language.Language) (LangID, bool) {
return 0, false
}

// langSet is a bit set for 512 languages
// LangSet is a bit set for 512 languages
//
// It works as a map[LangID]bool, with the limitation
// that only the 9 low bits of a LangID are used.
// More precisely, the page of a LangID l is given by its 3 "higher" bits : 8-6
// and the bit position by its 6 lower bits : 5-0
type langSet [8]uint64
type LangSet [8]uint64

// newLangsetFromCoverage compile the languages supported by the given
// rune coverage
func newLangsetFromCoverage(rs runeSet) (out langSet) {
func newLangsetFromCoverage(rs RuneSet) (out LangSet) {
for id, item := range languagesRunes {
if rs.includes(item.runes) {
out.add(LangID(id))
out.Add(LangID(id))
}
}
return out
}

func (ls langSet) String() string {
func (ls LangSet) String() string {
var chunks []string
for pageN, page := range ls {
for bit := 0; bit < 64; bit++ {
Expand All @@ -83,21 +83,21 @@ func (ls langSet) String() string {
return "{" + strings.Join(chunks, "|") + "}"
}

func (ls *langSet) add(l LangID) {
func (ls *LangSet) Add(l LangID) {
page := (l & 0b111111111 >> 6)
bit := l & 0b111111
ls[page] |= 1 << bit
}

func (ls langSet) contains(l LangID) bool {
func (ls LangSet) Contains(l LangID) bool {
page := (l & 0b111111111 >> 6)
bit := l & 0b111111
return ls[page]&(1<<bit) != 0
}

const langSetSize = 8 * 8

func (ls langSet) serialize() []byte {
func (ls LangSet) serialize() []byte {
var buffer [langSetSize]byte
for i, v := range ls {
binary.BigEndian.PutUint64(buffer[i*8:], v)
Expand All @@ -107,7 +107,7 @@ func (ls langSet) serialize() []byte {

// deserializeFrom reads the binary format produced by serializeTo
// it returns the number of bytes read from `data`
func (ls *langSet) deserializeFrom(data []byte) (int, error) {
func (ls *LangSet) deserializeFrom(data []byte) (int, error) {
if len(data) < langSetSize {
return 0, errors.New("invalid lang set (EOF)")
}
Expand Down
Loading

0 comments on commit e556800

Please sign in to comment.