package d2fonts
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"unicode/utf8"
gotextfont "github.com/go-text/typesetting/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/font/sfnt"
"github.com/pancsta/d2/d2renderers/internal/fontface"
)
type FallbackFont struct {
Name string
MIMEType string
Data []byte
FaceIndex uint16
}
type FallbackRequest struct {
Runes []rune
Family string
Style string
Weight int
}
type FallbackResolver interface {
ResolveFallbacks (context .Context , FallbackRequest ) ([]FallbackFont , error )
}
type SystemFallbackLimits struct {
MaxDirectoryEntries int
MaxFiles int
MaxFaces int
MaxRequestedRunes int
MaxCoverageChecks int64
MaxFileBytes int64
MaxScannedBytes int64
MaxResolvedBytes int64
}
func (l SystemFallbackLimits ) validate () error {
if l .MaxDirectoryEntries <= 0 || l .MaxFiles <= 0 || l .MaxFaces <= 0 || l .MaxRequestedRunes <= 0 || l .MaxCoverageChecks <= 0 || l .MaxFileBytes <= 0 || l .MaxScannedBytes <= 0 || l .MaxResolvedBytes <= 0 {
return fmt .Errorf ("d2fonts: every system fallback limit must be positive" )
}
return nil
}
type SystemFallbackResolver struct {
limits SystemFallbackLimits
roots []string
resolveOnce sync .Once
resolveGate chan struct {}
pathsMu sync .Mutex
pathsIndexed bool
indexedPaths []string
pathFlight chan struct {}
work systemFallbackWork
}
type systemFallbackWork struct {
directoryEntries int
files int
requestedRunes int64
scannedBytes int64
faces int
coverageChecks int64
resolvedBytes int64
}
type fallbackFaceProfile struct {
family string
italic bool
weight int
mono bool
}
func NewSystemFallbackResolver (limits SystemFallbackLimits ) (*SystemFallbackResolver , error ) {
if err := limits .validate (); err != nil {
return nil , err
}
return &SystemFallbackResolver {limits : limits }, nil
}
func (r *SystemFallbackResolver ) ResolveFallbacks (ctx context .Context , request FallbackRequest ) ([]FallbackFont , error ) {
if r == nil {
return nil , fmt .Errorf ("d2fonts: nil system fallback resolver" )
}
if ctx == nil {
ctx = context .Background ()
}
if err := ctx .Err (); err != nil {
return nil , err
}
release , err := r .acquireResolve (ctx )
if err != nil {
return nil , err
}
defer release ()
if err := ctx .Err (); err != nil {
return nil , err
}
if len (request .Family )+len (request .Style ) > 1_024 {
return nil , fmt .Errorf ("d2fonts: fallback request font metadata exceeds 1024 bytes" )
}
if len (request .Runes ) > r .limits .MaxRequestedRunes {
return nil , fmt .Errorf ("d2fonts: requested fallback rune count %d exceeds limit %d" , len (request .Runes ), r .limits .MaxRequestedRunes )
}
if int64 (len (request .Runes )) > int64 (r .limits .MaxRequestedRunes )-r .work .requestedRunes {
return nil , fmt .Errorf ("d2fonts: requested fallback rune count exceeds cumulative resolver limit %d" , r .limits .MaxRequestedRunes )
}
r .work .requestedRunes += int64 (len (request .Runes ))
remaining , err := uniqueFallbackRunes (ctx , request .Runes )
if err != nil {
return nil , err
}
if len (remaining ) == 0 {
return nil , nil
}
requestProfile := newFallbackRequestProfile (remaining , request )
roots := r .roots
if roots == nil {
roots = systemFontRoots ()
}
if len (roots ) == 0 {
return nil , nil
}
paths , err := r .cachedFontPaths (ctx , roots , requestProfile )
if err != nil {
return nil , err
}
var resolved []FallbackFont
for _ , path := range paths {
if err := ctx .Err (); err != nil {
return nil , err
}
probe , err := probeFontFileCoverage (
ctx , path , r .limits .MaxFileBytes , r .limits .MaxScannedBytes -r .work .scannedBytes ,
r .limits .MaxFaces -r .work .faces , remaining ,
)
if err != nil {
r .work .scannedBytes += probe .bytes
return nil , fmt .Errorf ("d2fonts: system font %q: %w" , filepath .Base (path ), err )
}
if probe .faces == 0 {
r .work .scannedBytes += probe .bytes
continue
}
if probe .faces > r .limits .MaxFaces -r .work .faces {
r .work .scannedBytes += probe .bytes
return nil , fmt .Errorf ("d2fonts: system font face count exceeds cumulative resolver limit %d while resolving %U" , r .limits .MaxFaces , firstRune (remaining ))
}
if !probe .covers {
r .work .faces += probe .faces
r .work .scannedBytes += probe .bytes
continue
}
data , err := readBoundedFontFile (path , r .limits .MaxFileBytes , r .limits .MaxScannedBytes -r .work .scannedBytes )
if err != nil {
r .work .scannedBytes += probe .bytes
return nil , fmt .Errorf ("d2fonts: system font %q: %w" , filepath .Base (path ), err )
}
if len (data ) == 0 {
r .work .scannedBytes += probe .bytes
continue
}
r .work .scannedBytes += int64 (len (data ))
if err := ctx .Err (); err != nil {
return nil , err
}
remainingFaces := r .limits .MaxFaces - r .work .faces
if remainingFaces <= 0 {
return nil , fmt .Errorf ("d2fonts: system font face count exceeds cumulative resolver limit %d while resolving %U" , r .limits .MaxFaces , firstRune (remaining ))
}
collection , err := fontface .ParseFaceCollectionWithLimit (data , remainingFaces )
if err != nil {
var limitError *fontface .FaceCountLimitError
if errors .As (err , &limitError ) {
return nil , fmt .Errorf ("d2fonts: system font %q: %w" , filepath .Base (path ), err )
}
continue
}
if collection .NumFaces () > r .limits .MaxFaces -r .work .faces {
return nil , fmt .Errorf ("d2fonts: system font face count exceeds cumulative resolver limit %d while resolving %U" , r .limits .MaxFaces , firstRune (remaining ))
}
r .work .faces += collection .NumFaces ()
selectedFaces := make (map [int ]bool )
faceProfiles := make ([]fallbackFaceProfile , collection .NumFaces ())
profiledFaces := make ([]bool , collection .NumFaces ())
for len (remaining ) != 0 {
bestIndex , bestPriority , bestCoverage := -1 , 0 , []rune (nil )
for index := 0 ; index < collection .NumFaces (); index ++ {
if index > int (^uint16 (0 )) {
break
}
if selectedFaces [index ] {
continue
}
face , err := collection .Face (index )
if err != nil {
continue
}
covered , err := r .coveredRunes (ctx , face , remaining )
if err != nil {
return nil , err
}
if len (covered ) == 0 {
continue
}
if !profiledFaces [index ] {
faceProfiles [index ] = newFallbackFaceProfile (face )
profiledFaces [index ] = true
}
priority := fallbackFacePriority (faceProfiles [index ], requestProfile )
if bestIndex < 0 || priority < bestPriority || priority == bestPriority && len (covered ) > len (bestCoverage ) {
bestIndex , bestPriority , bestCoverage = index , priority , covered
}
}
if bestIndex < 0 {
break
}
if int64 (len (data )) > r .limits .MaxResolvedBytes -r .work .resolvedBytes {
return nil , fmt .Errorf ("d2fonts: resolved fallback font bytes exceed limit %d while retaining face %d from %q" , r .limits .MaxResolvedBytes , bestIndex , filepath .Base (path ))
}
selectedFaces [bestIndex ] = true
resolved = append (resolved , FallbackFont {
Name : filepath .Base (path ), MIMEType : fontMIMEType (path ),
Data : append ([]byte (nil ), data ...), FaceIndex : uint16 (bestIndex ),
})
r .work .resolvedBytes += int64 (len (data ))
for _ , value := range bestCoverage {
delete (remaining , value )
}
if len (remaining ) == 0 {
return resolved , nil
}
}
}
return resolved , nil
}
func (r *SystemFallbackResolver ) acquireResolve (ctx context .Context ) (func (), error ) {
r .resolveOnce .Do (func () { r .resolveGate = make (chan struct {}, 1 ) })
select {
case r .resolveGate <- struct {}{}:
return func () { <-r .resolveGate }, nil
case <- ctx .Done ():
return nil , ctx .Err ()
}
}
func (r *SystemFallbackResolver ) cachedFontPaths (ctx context .Context , roots []string , profile fallbackRequestProfile ) ([]string , error ) {
for {
if err := ctx .Err (); err != nil {
return nil , err
}
r .pathsMu .Lock ()
if r .pathsIndexed {
paths := append ([]string (nil ), r .indexedPaths ...)
r .pathsMu .Unlock ()
sortFontPaths (paths , profile )
return paths , nil
}
if flight := r .pathFlight ; flight != nil {
r .pathsMu .Unlock ()
select {
case <- ctx .Done ():
return nil , ctx .Err ()
case <- flight :
continue
}
}
flight := make (chan struct {})
r .pathFlight = flight
r .pathsMu .Unlock ()
paths , err := r .fontPaths (ctx , roots , profile )
r .pathsMu .Lock ()
if err == nil {
r .indexedPaths = append ([]string (nil ), paths ...)
r .pathsIndexed = true
}
r .pathFlight = nil
close (flight )
r .pathsMu .Unlock ()
return paths , err
}
}
func (r *SystemFallbackResolver ) fontPaths (ctx context .Context , roots []string , profile fallbackRequestProfile ) ([]string , error ) {
paths := make ([]string , 0 , min (r .limits .MaxFiles , 256 ))
for _ , root := range roots {
if err := ctx .Err (); err != nil {
return nil , err
}
info , err := os .Lstat (root )
if err != nil || !info .IsDir () || info .Mode ()&os .ModeSymlink != 0 {
continue
}
if r .work .directoryEntries >= r .limits .MaxDirectoryEntries {
return nil , fmt .Errorf ("d2fonts: system font directory entries exceed limit %d" , r .limits .MaxDirectoryEntries )
}
r .work .directoryEntries ++
stack := []string {root }
for len (stack ) != 0 {
directory := stack [len (stack )-1 ]
stack = stack [:len (stack )-1 ]
handle , err := openVerifiedDirectory (directory )
if err != nil {
continue
}
for {
if err := ctx .Err (); err != nil {
_ = handle .Close ()
return nil , err
}
children , readErr := handle .ReadDir (directoryReadBatch )
for _ , child := range children {
if r .work .directoryEntries >= r .limits .MaxDirectoryEntries {
_ = handle .Close ()
return nil , fmt .Errorf ("d2fonts: system font directory entries exceed limit %d" , r .limits .MaxDirectoryEntries )
}
r .work .directoryEntries ++
if r .work .directoryEntries &255 == 0 {
if err := ctx .Err (); err != nil {
_ = handle .Close ()
return nil , err
}
}
if child .Type ()&os .ModeSymlink != 0 {
continue
}
path := filepath .Join (directory , child .Name ())
if child .IsDir () {
stack = append (stack , path )
continue
}
if !isFontPath (path ) {
continue
}
if r .work .files >= r .limits .MaxFiles {
_ = handle .Close ()
return nil , fmt .Errorf ("d2fonts: system font candidate count exceeds limit %d; raise the bounded fallback search limit or configure a custom font" , r .limits .MaxFiles )
}
r .work .files ++
paths = append (paths , path )
}
if readErr == io .EOF {
break
}
if readErr != nil {
break
}
}
_ = handle .Close ()
}
}
sortFontPaths (paths , profile )
return paths , nil
}
func sortFontPaths(paths []string , profile fallbackRequestProfile ) {
sort .Slice (paths , func (i , j int ) bool {
left , right := fallbackPathPriority (paths [i ], profile ), fallbackPathPriority (paths [j ], profile )
if left != right {
return left < right
}
return paths [i ] < paths [j ]
})
}
const directoryReadBatch = 256
func openVerifiedDirectory(path string ) (*os .File , error ) {
listed , err := os .Lstat (path )
if err != nil {
return nil , err
}
if !listed .IsDir () || listed .Mode ()&os .ModeSymlink != 0 {
return nil , fmt .Errorf ("path is not a non-symlink directory" )
}
directory , err := os .Open (path )
if err != nil {
return nil , err
}
opened , err := directory .Stat ()
if err != nil {
_ = directory .Close ()
return nil , err
}
if !opened .IsDir () || !os .SameFile (listed , opened ) {
_ = directory .Close ()
return nil , fmt .Errorf ("directory identity changed while opening" )
}
return directory , nil
}
type fontCoverageProbe struct {
faces int
bytes int64
covers bool
}
var errFontProbeBytes = errors .New ("font cmap probe bytes exceed remaining scan limit" )
type boundedFontReaderAt struct {
ctx context .Context
reader io .ReaderAt
remaining int64
read int64
limitHit bool
}
func (r *boundedFontReaderAt ) ReadAt (buffer []byte , offset int64 ) (int , error ) {
if err := r .ctx .Err (); err != nil {
return 0 , err
}
if int64 (len (buffer )) > r .remaining -r .read {
r .limitHit = true
return 0 , errFontProbeBytes
}
read , err := r .reader .ReadAt (buffer , offset )
r .read += int64 (read )
return read , err
}
func probeFontFileCoverage(ctx context .Context , path string , maxFileBytes , remainingScanBytes int64 , maxFaces int , values map [rune ]struct {}) (fontCoverageProbe , error ) {
if remainingScanBytes <= 0 {
return fontCoverageProbe {}, fmt .Errorf ("system font scan bytes exceed limit" )
}
listed , err := os .Lstat (path )
if err != nil || !listed .Mode ().IsRegular () || listed .Mode ()&os .ModeSymlink != 0 || listed .Size () <= 0 || listed .Size () > maxFileBytes {
return fontCoverageProbe {}, nil
}
file , err := os .Open (path )
if err != nil {
return fontCoverageProbe {}, nil
}
defer file .Close ()
opened , err := file .Stat ()
if err != nil || !opened .Mode ().IsRegular () || !os .SameFile (listed , opened ) || opened .Size () != listed .Size () {
return fontCoverageProbe {}, nil
}
reader := &boundedFontReaderAt {ctx : ctx , reader : file , remaining : remainingScanBytes }
collection , err := opentype .ParseCollectionReaderAt (reader )
if err != nil {
if ctxErr := ctx .Err (); ctxErr != nil {
return fontCoverageProbe {bytes : reader .read }, ctxErr
}
if reader .limitHit || errors .Is (err , errFontProbeBytes ) {
return fontCoverageProbe {bytes : reader .read }, errFontProbeBytes
}
return fontCoverageProbe {bytes : reader .read }, nil
}
probe := fontCoverageProbe {faces : collection .NumFonts (), bytes : reader .read }
if probe .faces > maxFaces {
return probe , nil
}
var buffer sfnt .Buffer
for index := 0 ; index < collection .NumFonts (); index ++ {
if err := ctx .Err (); err != nil {
return probe , err
}
face , err := collection .Font (index )
if err != nil {
if ctxErr := ctx .Err (); ctxErr != nil {
return fontCoverageProbe {faces : probe .faces , bytes : reader .read }, ctxErr
}
if reader .limitHit || errors .Is (err , errFontProbeBytes ) {
return fontCoverageProbe {faces : probe .faces , bytes : reader .read }, errFontProbeBytes
}
continue
}
for value := range values {
glyph , err := face .GlyphIndex (&buffer , value )
if err != nil {
if reader .limitHit || errors .Is (err , errFontProbeBytes ) {
return fontCoverageProbe {faces : probe .faces , bytes : reader .read }, errFontProbeBytes
}
continue
}
if glyph != 0 {
probe .bytes = reader .read
probe .covers = true
return probe , nil
}
}
}
probe .bytes = reader .read
return probe , nil
}
func readBoundedFontFile(path string , maxFileBytes , remainingScanBytes int64 ) ([]byte , error ) {
if remainingScanBytes <= 0 {
return nil , fmt .Errorf ("system font scan bytes exceed limit" )
}
listed , err := os .Lstat (path )
if err != nil || !listed .Mode ().IsRegular () || listed .Mode ()&os .ModeSymlink != 0 || listed .Size () <= 0 {
return nil , nil
}
if listed .Size () > maxFileBytes {
return nil , nil
}
if listed .Size () > remainingScanBytes {
return nil , fmt .Errorf ("system font scan bytes exceed remaining limit %d" , remainingScanBytes )
}
file , err := os .Open (path )
if err != nil {
return nil , nil
}
defer file .Close ()
opened , err := file .Stat ()
if err != nil || !opened .Mode ().IsRegular () || !os .SameFile (listed , opened ) || opened .Size () <= 0 {
return nil , nil
}
readLimit := min (maxFileBytes , remainingScanBytes )
if opened .Size () > readLimit {
return nil , fmt .Errorf ("system font file exceeds remaining byte limit %d" , readLimit )
}
data , err := io .ReadAll (io .LimitReader (file , readLimit +1 ))
if int64 (len (data )) > readLimit {
return nil , fmt .Errorf ("system font file grew beyond byte limit %d while reading" , readLimit )
}
if err != nil {
return data , nil
}
return data , nil
}
func uniqueFallbackRunes(ctx context .Context , values []rune ) (map [rune ]struct {}, error ) {
result := make (map [rune ]struct {}, len (values ))
for index , value := range values {
if index &1023 == 0 {
if err := ctx .Err (); err != nil {
return nil , err
}
}
if fallbackRuneMayHaveGlyph (value ) {
result [value ] = struct {}{}
}
}
if err := ctx .Err (); err != nil {
return nil , err
}
return result , nil
}
func fallbackRuneMayHaveGlyph(value rune ) bool {
if !utf8 .ValidRune (value ) {
return false
}
if value >= 0xfdd0 && value <= 0xfdef {
return false
}
tail := value & 0xffff
return tail != 0xfffe && tail != 0xffff
}
func (r *SystemFallbackResolver ) coveredRunes (ctx context .Context , face *fontface .ParsedFace , values map [rune ]struct {}) ([]rune , error ) {
covered := make ([]rune , 0 )
index := 0
for value := range values {
if r .work .coverageChecks >= r .limits .MaxCoverageChecks {
return nil , fmt .Errorf ("d2fonts: system font coverage checks exceed limit %d across this resolver while resolving U+%04X" , r .limits .MaxCoverageChecks , firstRune (values ))
}
r .work .coverageChecks ++
if index &255 == 0 {
if err := ctx .Err (); err != nil {
return nil , err
}
}
supported , err := face .SupportsRenderableRune (value )
if err != nil {
return nil , err
}
if supported {
covered = append (covered , value )
}
index ++
}
sort .Slice (covered , func (i , j int ) bool { return covered [i ] < covered [j ] })
if err := ctx .Err (); err != nil {
return nil , err
}
return covered , nil
}
func firstRune(values map [rune ]struct {}) rune {
var first rune
found := false
for value := range values {
if !found || value < first {
first = value
found = true
}
}
return first
}
func isFontPath(path string ) bool {
switch strings .ToLower (filepath .Ext (path )) {
case ".ttf" , ".otf" , ".ttc" , ".otc" :
return true
default :
return false
}
}
func fontMIMEType(path string ) string {
switch strings .ToLower (filepath .Ext (path )) {
case ".ttc" , ".otc" :
return "font/collection"
case ".otf" :
return "font/otf"
default :
return "font/ttf"
}
}
const genericFallbackPriority = 20
type fallbackRequestProfile struct {
cjk, arabic, hebrew, symbol bool
italic, bold, semibold, mono bool
family string
weight int
}
func newFallbackRequestProfile(values map [rune ]struct {}, request FallbackRequest ) fallbackRequestProfile {
family := normalizeFallbackFamily (request .Family )
style := strings .ToLower (request .Style )
styleIsSemibold := strings .Contains (style , "semibold" ) || strings .Contains (style , "semi-bold" )
weight := request .Weight
if weight <= 0 {
switch {
case styleIsSemibold :
weight = 600
case strings .Contains (style , "bold" ):
weight = 700
default :
weight = 400
}
}
weight = min (900 , max (100 , weight ))
profile := fallbackRequestProfile {
italic : strings .Contains (style , "italic" ) || strings .Contains (style , "oblique" ),
bold : weight >= 700 ,
semibold : weight >= 600 && weight < 700 ,
mono : strings .Contains (family , "mono" ) || strings .Contains (family , "code" ) || strings .Contains (family , "console" ),
family : family ,
weight : weight ,
}
for value := range values {
profile .cjk = profile .cjk || isCJK (value )
profile .arabic = profile .arabic || isArabic (value )
profile .hebrew = profile .hebrew || isHebrew (value )
profile .symbol = profile .symbol || isEmojiOrSymbol (value )
}
return profile
}
func fallbackPathPriority(path string , profile fallbackRequestProfile ) int {
name := strings .ToLower (filepath .Base (path ))
semanticPriority := 80
contains := func (parts ...string ) bool {
for _ , part := range parts {
if strings .Contains (name , part ) {
return true
}
}
return false
}
if contains ("arial unicode" , "unifont" ) {
semanticPriority = -10
}
switch {
case profile .cjk && contains ("cjk" , "hiragino" , "pingfang" , "heiti" , "gothic" , "songti" , "mingliu" , "simsun" , "malgun" ):
semanticPriority = min (semanticPriority , 0 )
case profile .arabic && contains ("arab" , "amiri" , "naskh" , "kufi" ):
semanticPriority = min (semanticPriority , 0 )
case profile .hebrew && contains ("hebrew" , "david" , "frank" ):
semanticPriority = min (semanticPriority , 0 )
case profile .symbol && contains ("symbol" , "emoji" , "dingbat" , "dejavu" , "unifont" , "unicode" ):
semanticPriority = min (semanticPriority , 0 )
}
if contains ("noto" , "dejavu" , "arial unicode" , "unifont" ) {
semanticPriority = min (semanticPriority , genericFallbackPriority )
}
if contains ("emoji" ) {
semanticPriority += 10
}
if profile .mono && contains ("mono" , "code" , "console" ) {
semanticPriority -= 6
}
return fallbackPathStylePenalty (name , profile ) + semanticPriority
}
func fallbackPathStylePenalty(name string , request fallbackRequestProfile ) int {
candidate := fallbackFaceProfile {
family : normalizeFallbackFamily (name ),
italic : strings .Contains (name , "italic" ) || strings .Contains (name , "oblique" ),
weight : fallbackFilenameWeight (name ),
mono : strings .Contains (name , "mono" ) || strings .Contains (name , "code" ) || strings .Contains (name , "console" ),
}
return fallbackStylePenalty (candidate , request )
}
func fallbackFilenameWeight(name string ) int {
switch {
case strings .Contains (name , "extrablack" ), strings .Contains (name , "ultrablack" ), strings .Contains (name , "superblack" ):
return 900
case strings .Contains (name , "black" ), strings .Contains (name , "heavy" ):
return 900
case strings .Contains (name , "extrabold" ), strings .Contains (name , "extra-bold" ), strings .Contains (name , "ultrabold" ), strings .Contains (name , "ultra-bold" ):
return 800
case strings .Contains (name , "semibold" ), strings .Contains (name , "semi-bold" ), strings .Contains (name , "demibold" ), strings .Contains (name , "demi-bold" ):
return 600
case strings .Contains (name , "bold" ):
return 700
case strings .Contains (name , "medium" ):
return 500
case strings .Contains (name , "extralight" ), strings .Contains (name , "extra-light" ), strings .Contains (name , "ultralight" ), strings .Contains (name , "ultra-light" ):
return 200
case strings .Contains (name , "semilight" ), strings .Contains (name , "semi-light" ), strings .Contains (name , "demilight" ), strings .Contains (name , "demi-light" ):
return 350
case strings .Contains (name , "thin" ):
return 100
case strings .Contains (name , "light" ):
return 300
default :
return 400
}
}
func newFallbackFaceProfile(face *fontface .ParsedFace ) fallbackFaceProfile {
description := face .Shaping .Describe ()
return fallbackFaceProfile {
family : normalizeFallbackFamily (description .Family ),
italic : description .Aspect .Style == gotextfont .StyleItalic ,
weight : min (900 , max (100 , int (description .Aspect .Weight +0.5 ))),
mono : face .Shaping .IsMonospace (),
}
}
func fallbackFacePriority(face fallbackFaceProfile , request fallbackRequestProfile ) int {
return fallbackStylePenalty (face , request )
}
func fallbackStylePenalty(candidate fallbackFaceProfile , request fallbackRequestProfile ) int {
penalty := absInt (candidate .weight - request .weight )
if candidate .italic != request .italic {
penalty += 1_000
}
if candidate .mono != request .mono {
if request .mono {
penalty += 1_000
} else {
penalty += 100
}
}
if request .family != "" && candidate .family != "" {
switch {
case candidate .family == request .family :
penalty -= 40
case sameFallbackFamilyClass (candidate .family , request .family ):
penalty -= 10
}
}
return penalty
}
func sameFallbackFamilyClass(left , right string ) bool {
for _ , class := range []string {"sans" , "serif" , "mono" , "code" , "console" } {
if strings .Contains (left , class ) && strings .Contains (right , class ) {
return true
}
}
return false
}
func normalizeFallbackFamily(value string ) string {
value = strings .ToLower (value )
return strings .Map (func (value rune ) rune {
if value >= 'a' && value <= 'z' || value >= '0' && value <= '9' {
return value
}
return -1
}, value )
}
func absInt(value int ) int {
if value < 0 {
return -value
}
return value
}
func isCJK(value rune ) bool {
return value >= 0x2e80 && value <= 0x9fff || value >= 0xf900 && value <= 0xfaff || value >= 0x20000 && value <= 0x323af
}
func isArabic(value rune ) bool {
return value >= 0x0600 && value <= 0x08ff || value >= 0xfb50 && value <= 0xfdff || value >= 0xfe70 && value <= 0xfeff
}
func isHebrew(value rune ) bool {
return value >= 0x0590 && value <= 0x05ff || value >= 0xfb1d && value <= 0xfb4f
}
func isEmojiOrSymbol(value rune ) bool {
return value >= 0x2190 && value <= 0x2bff || value >= 0x1f000 && value <= 0x1faff
}
func systemFontRoots() []string {
switch runtime .GOOS {
case "darwin" :
return []string {"/System/Library/Fonts" , "/Library/Fonts" }
case "linux" , "freebsd" , "openbsd" , "netbsd" , "dragonfly" :
return []string {"/usr/share/fonts" , "/usr/local/share/fonts" }
case "windows" :
root := os .Getenv ("WINDIR" )
if root == "" {
root = `C:\Windows`
}
return []string {filepath .Join (root , "Fonts" )}
default :
return nil
}
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .