Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fontscan] expose system fonts #149

Merged
merged 2 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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