diff --git a/kernel/api/system.go b/kernel/api/system.go index cb0a4b5d0..3fb6c16dc 100644 --- a/kernel/api/system.go +++ b/kernel/api/system.go @@ -668,7 +668,15 @@ func setFollowSystemLockScreen(c *gin.Context) { func getSysFonts(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) - ret.Data = util.LoadSysFonts() + fonts := util.LoadSysFonts() + + // TODO: 字重 https://github.com/siyuan-note/siyuan/issues/10313 + var families []string + for _, font := range fonts { + families = append(families, font.Family) + } + families = gulu.Str.RemoveDuplicatedElem(families) + ret.Data = families } func version(c *gin.Context) { diff --git a/kernel/go.mod b/kernel/go.mod index 8c181473e..e104975f7 100644 --- a/kernel/go.mod +++ b/kernel/go.mod @@ -11,7 +11,7 @@ require ( github.com/88250/lute v1.7.7-0.20260419134724-bb68012f231d github.com/88250/vitess-sqlparser v0.0.0-20210205111146-56a2ded2aba1 github.com/ClarkThan/ahocorasick v0.0.0-20231011042242-30d1ef1347f4 - github.com/ConradIrwin/font v0.2.1 + github.com/ConradIrwin/font v0.2.2-0.20260202161408-44ae4cf5fb22 github.com/Masterminds/sprig/v3 v3.3.0 github.com/PuerkitoBio/goquery v1.11.0 github.com/Xuanwo/go-locale v1.1.3 @@ -78,7 +78,6 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 github.com/xuri/excelize/v2 v2.10.1 - golang.org/x/image v0.38.0 golang.org/x/mobile v0.0.0-20251209145715-2553ed8ce294 golang.org/x/mod v0.34.0 golang.org/x/net v0.53.0 @@ -138,6 +137,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.28.0 // indirect github.com/go-resty/resty/v2 v2.16.5 // indirect + github.com/go-sw/text-codec v0.0.1 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect @@ -194,6 +194,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/crypto v0.50.0 // indirect + golang.org/x/image v0.38.0 // indirect golang.org/x/tools v0.43.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/kernel/go.sum b/kernel/go.sum index 875c99e61..bc67e47aa 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -24,8 +24,8 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/ClarkThan/ahocorasick v0.0.0-20231011042242-30d1ef1347f4 h1:r10k4+Lu1mDpiCKa1liAdGJUhB4BJHHJnMvtoli6Hts= github.com/ClarkThan/ahocorasick v0.0.0-20231011042242-30d1ef1347f4/go.mod h1:a3CzWIqeRxiODAscAIfZ4wbFRXxywBrdCwTENVAWB2g= -github.com/ConradIrwin/font v0.2.1 h1:D4tWi7zyRAdVKOtOys5960HnAAfUSRx/syaf+J9JqlI= -github.com/ConradIrwin/font v0.2.1/go.mod h1:krTLO7JWu6g8RMxG8sl+T1Hf8W93XQacBKJmqFZ2MFY= +github.com/ConradIrwin/font v0.2.2-0.20260202161408-44ae4cf5fb22 h1:xEDrMXxOJsMByKW9Uw2WvwuVhfRd0SN5sOWVR8rYSjc= +github.com/ConradIrwin/font v0.2.2-0.20260202161408-44ae4cf5fb22/go.mod h1:5iRYC36M+hBFrRcE25N9/kioASZaqIkAXbgOyfVDCXg= github.com/JalfResi/justext v0.0.0-20221106200834-be571e3e3052 h1:8T2zMbhLBbH9514PIQVHdsGhypMrsB4CxwbldKA9sBA= github.com/JalfResi/justext v0.0.0-20221106200834-be571e3e3052/go.mod h1:0SURuH1rsE8aVWvutuMZghRNrNrYEUzibzJfhEYR8L0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -186,6 +186,8 @@ github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3 github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-sw/text-codec v0.0.1 h1:H+wnKj5TuvqYlfhMtbc52m1Oe0YnxD7QPzZPBE8QAn8= +github.com/go-sw/text-codec v0.0.1/go.mod h1:RTuIwijiCSy7wYLZd3h4RmWHBCD1bERsEST7vqnOzmo= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= @@ -529,7 +531,6 @@ golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20180302201248-b7ef84aaf62a/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= diff --git a/kernel/util/font.go b/kernel/util/font.go index 9c8cdaa70..de208fd64 100644 --- a/kernel/util/font.go +++ b/kernel/util/font.go @@ -19,25 +19,24 @@ package util import ( "os" "sort" + "strconv" "strings" "sync" "time" - "github.com/88250/gulu" "github.com/ConradIrwin/font/sfnt" "github.com/flopp/go-findfont" "github.com/siyuan-note/logging" - ttc "golang.org/x/image/font/sfnt" textUnicode "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) var ( - sysFonts []string + sysFonts []*Font sysFontsLock = sync.Mutex{} ) -func LoadSysFonts() (ret []string) { +func LoadSysFonts() []*Font { sysFontsLock.Lock() defer sysFontsLock.Unlock() @@ -46,21 +45,20 @@ func LoadSysFonts() (ret []string) { } start := time.Now() - fonts := loadFonts() - ret = []string{} - for _, font := range fonts { - ret = append(ret, font.Family) - } - ret = gulu.Str.RemoveDuplicatedElem(ret) - sort.Strings(ret) - sysFonts = ret + sysFonts = loadFonts() + + sort.Slice(sysFonts, func(i, j int) bool { + return sysFonts[i].DisplayName < sysFonts[j].DisplayName + }) + logging.LogInfof("loaded system fonts [%d] in [%dms]", len(sysFonts), time.Since(start).Milliseconds()) - return + return sysFonts } type Font struct { - Path string - Family string + Family string `json:"family"` // 对应 CSS font-family + Weight int `json:"weight"` // 对应 CSS font-weight + DisplayName string `json:"displayName"` // 给人看的名称 (Family + Subfamily) } func loadFonts() (ret []*Font) { @@ -68,22 +66,22 @@ func loadFonts() (ret []*Font) { for _, fontPath := range findfont.List() { if strings.HasSuffix(strings.ToLower(fontPath), ".ttc") { families := parseTTCFontFamily(fontPath) - for _, family := range families { - if existFont(family, ret) { + for _, f := range families { + if existFont(f, ret) { continue } - ret = append(ret, &Font{fontPath, family}) + ret = append(ret, f) //LogInfof("[%s] [%s]", fontPath, family) } } else if strings.HasSuffix(strings.ToLower(fontPath), ".otf") || strings.HasSuffix(strings.ToLower(fontPath), ".ttf") { - family := parseTTFFontFamily(fontPath) - if "" != family { - if existFont(family, ret) { + f := parseTTFFontFamily(fontPath) + if nil != f { + if existFont(f, ret) { continue } - ret = append(ret, &Font{fontPath, family}) + ret = append(ret, f) //logging.LogInfof("[%s] [%s]", fontPath, family) } } @@ -91,77 +89,63 @@ func loadFonts() (ret []*Font) { return } -func existFont(family string, fonts []*Font) bool { +func existFont(f *Font, fonts []*Font) bool { for _, font := range fonts { - if strings.EqualFold(family, font.Family) { + if strings.EqualFold(f.Family, font.Family) && f.Weight == font.Weight { return true } } return false } -func parseTTCFontFamily(fontPath string) (ret []string) { +func parseTTCFontFamily(fontPath string) (ret []*Font) { defer logging.Recover() - data, err := os.ReadFile(fontPath) + fontFile, err := os.Open(fontPath) if err != nil { //logging.LogErrorf("read font file [%s] failed: %s", fontPath, err) return } - collection, err := ttc.ParseCollection(data) + defer fontFile.Close() + + fonts, err := sfnt.ParseCollection(fontFile) if err != nil { //LogErrorf("parse font collection [%s] failed: %s", fontPath, err) return } - for i := 0; i < collection.NumFonts(); i++ { - font, err := collection.Font(i) - if err != nil { - //LogErrorf("get font [%s] failed: %s", fontPath, err) - continue - } - - family, _ := font.Name(nil, ttc.NameIDFull) - family = strings.TrimSpace(family) - if "" != family && !strings.HasPrefix(family, ".") { - ret = append(ret, family) - } - - family, _ = font.Name(nil, ttc.NameIDFamily) - family = strings.TrimSpace(family) - if "" != family && !strings.HasPrefix(family, ".") { - ret = append(ret, family) - } - - family, _ = font.Name(nil, ttc.NameIDTypographicFamily) - family = strings.TrimSpace(family) - if "" != family && !strings.HasPrefix(family, ".") { - ret = append(ret, family) + for _, f := range fonts { + font := parseFont(f) + if nil != font { + ret = append(ret, font) } } - ret = gulu.Str.RemoveDuplicatedElem(ret) return } -func parseTTFFontFamily(fontPath string) (ret string) { +func parseTTFFontFamily(fontPath string) *Font { defer logging.Recover() fontFile, err := os.Open(fontPath) - defer fontFile.Close() if err != nil { //LogErrorf("open font file [%s] failed: %s", fontPath, err) - return + return nil } + defer fontFile.Close() + font, err := sfnt.Parse(fontFile) if err != nil { - //LogErrorf("parse font [%s] failed: %s", fontPath, err) - return + //logging.LogErrorf("parse font [%s] failed: %s", fontFile.Name(), err) + return nil } + return parseFont(font) +} +func parseFont(font *sfnt.Font) *Font { t, err := font.NameTable() if err != nil { - logging.LogErrorf("get font [%s] name table failed: %s", fontPath, err) - return + //logging.LogErrorf("parse font name table failed: %s", err) + return nil } var family, subfamily string @@ -192,15 +176,53 @@ func parseTTFFontFamily(fontPath string) (ret string) { } } - //if family != "" && !strings.HasPrefix(family, ".") { - // if subfamily != "" && !strings.Contains(subfamily, "<") && !strings.EqualFold(subfamily, "Regular") { - // ret = family + "(" + subfamily + ")" - // } else { - // ret = family - // } - //} - // TODO: 字重加载方案 - _ = subfamily - ret = family - return + weight := 400 + os2, err := font.OS2Table() + if nil == err { + weight = int(os2.USWeightClass) + } + + if weight == 400 && subfamily != "" { + s := strings.ToLower(subfamily) + // 自动匹配 W01-W09 + for i := 1; i <= 9; i++ { + wStr := "w0" + strconv.Itoa(i) + if strings.Contains(s, wStr) { + weight = i * 100 + break + } + } + + // 自动匹配标准关键词 + if weight == 400 { // 如果 W 系列没匹配到 + switch { + case strings.Contains(s, "thin"): + weight = 100 + case strings.Contains(s, "light"): + weight = 300 + case strings.Contains(s, "medium"): + weight = 500 + case strings.Contains(s, "semibold") || strings.Contains(s, "demi"): + weight = 600 + case strings.Contains(s, "bold"): + weight = 700 + case strings.Contains(s, "black") || strings.Contains(s, "heavy"): + weight = 900 + } + } + } + + if family != "" && !strings.HasPrefix(family, ".") { + displayName := family + if subfamily != "" && !strings.EqualFold(subfamily, "Regular") { + displayName = family + " " + subfamily + } + + return &Font{ + Family: family, + Weight: weight, + DisplayName: displayName, + } + } + return nil }