// Copyright 2025-2026 肖其顿 (XIAO QI DUN) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ofdgo import ( "bytes" "io" "io/fs" "path" "sort" "strings" "sync" "time" ) // 字体文件系统及目录条目的接口检查 var ( _ fs.FS = (*FontFS)(nil) _ fs.ReadDirFS = (*FontFS)(nil) _ fs.GlobFS = (*FontFS)(nil) _ fs.File = (*fontMemFile)(nil) _ fs.ReadDirFile = (*fontDir)(nil) _ fs.DirEntry = (*fontDirEntry)(nil) ) // FontFile 内存字体文件 type FontFile struct { Name string Data []byte } // FontFS 缓存字体数据及名称索引的只读文件系统 type FontFS struct { files map[string][]byte loaders map[string]func() ([]byte, error) names []string candidates []fontFileCandidate } // fontFileCandidate 字体文件候选 type fontFileCandidate struct { name string face int base string lowerBase string filename string normalized string names []string fold string } // fontPatternMatcher 字体文件匹配器 type fontPatternMatcher struct { pattern string lowerPattern string stem string aliases []string priority int } // fontFileMatch 字体文件匹配结果 type fontFileMatch struct { name string face int priority int rank int styleRank int sortName string } // fontFileKey 字体文件与集合索引的去重键 type fontFileKey struct { name string face int } // fontMemFile 内存字体文件 type fontMemFile struct { *bytes.Reader info fontFileInfo } // fontDir 字体目录 type fontDir struct { offset int entries []fs.DirEntry } // fontDirEntry 字体目录条目 type fontDirEntry struct { info fontFileInfo } // fontFileInfo 字体文件信息 type fontFileInfo struct { name string size int64 mode fs.FileMode } // NewFontFS 创建内存字体文件系统 // 入参: fonts 字体文件列表 // 返回: *FontFS 内存字体文件系统 func NewFontFS(fonts []FontFile) *FontFS { fsys := &FontFS{ files: make(map[string][]byte), } for _, font := range fonts { name := cleanFontName(font.Name) if name == "." || len(font.Data) == 0 { continue } if _, ok := fsys.files[name]; !ok { fsys.names = append(fsys.names, name) } fsys.files[name] = append([]byte(nil), font.Data...) } sort.Strings(fsys.names) fsys.candidates = fontFileCandidates(fsys.names, path.Base) for i, file := range fsys.candidates { fsys.candidates = appendFontFileNames(fsys.candidates, i, fontFileNames(bytes.NewReader(fsys.files[file.name]))) } return fsys } // NewLazyFontFS 创建按需读取的字体文件系统,按文件名匹配,不预读字体内容 // 每个文件的读取结果仅缓存一次;目录条目大小为0,打开后可获取实际大小 // 入参: names 包含字体名称和样式的文件名, load 按原文件名读取独立字体的非nil函数 // 返回: *FontFS 字体文件系统 func NewLazyFontFS(names []string, load func(string) ([]byte, error)) *FontFS { fsys := &FontFS{files: make(map[string][]byte), loaders: make(map[string]func() ([]byte, error))} for _, original := range names { name := cleanFontName(original) if name == "." || !isFontFileName(name) { continue } if _, exists := fsys.files[name]; exists { continue } fsys.names = append(fsys.names, name) fsys.files[name] = nil fsys.loaders[name] = sync.OnceValues(func() ([]byte, error) { data, err := load(original) return bytes.Clone(data), err }) } sort.Strings(fsys.names) fsys.candidates = fontFileCandidates(fsys.names, path.Base) return fsys } // Len 获取字体文件数量 // 返回: int 字体文件数量 func (fsys *FontFS) Len() int { if fsys == nil { return 0 } return len(fsys.names) } // Open 打开字体文件 // 入参: name 字体文件名 // 返回: fs.File 字体文件, error 错误信息 func (fsys *FontFS) Open(name string) (fs.File, error) { if !fs.ValidPath(name) { return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} } if name == "." { return &fontDir{entries: fsys.entries()}, nil } data, ok := fsys.files[strings.ToLower(name)] if !ok { return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} } if load := fsys.loaders[strings.ToLower(name)]; load != nil { var err error data, err = load() if err != nil { return nil, &fs.PathError{Op: "open", Path: name, Err: err} } } return &fontMemFile{ Reader: bytes.NewReader(data), info: fontFileInfo{ name: strings.ToLower(name), size: int64(len(data)), }, }, nil } // ReadDir 读取字体目录 // 入参: name 目录名 // 返回: []fs.DirEntry 目录条目, error 错误信息 func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) { if !fs.ValidPath(name) { return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid} } if name != "." { err := fs.ErrNotExist if _, ok := fsys.files[strings.ToLower(name)]; ok { err = fs.ErrInvalid } return nil, &fs.PathError{Op: "readdir", Path: name, Err: err} } return fsys.entries(), nil } // Glob 匹配字体文件 // 入参: pattern 匹配模式 // 返回: []string 字体文件列表, error 错误信息 func (fsys *FontFS) Glob(pattern string) ([]string, error) { return fs.Glob(struct{ fs.FS }{fsys}, pattern) } // Match 匹配指定字体名称 // 入参: names 字体名称列表 // 返回: string 匹配字体文件, bool 是否为名称匹配 func (fsys *FontFS) Match(names ...string) (string, bool) { return fsys.MatchStyle(false, false, names...) } // MatchStyle 匹配指定样式的字体名称 // 入参: bold 是否粗体, italic 是否斜体, names 字体名称列表 // 返回: string 匹配字体文件, bool 是否为名称匹配 func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool) { if len(names) > 0 { if matches := fsys.matchStyle(names, bold, italic); len(matches) > 0 { return matches[0].name, true } } if matches := fsys.fallbackFonts(bold, italic); len(matches) > 0 { return matches[0], false } return "", false } // FontFileMatches 匹配指定样式的字体文件名称 // 入参: files 可用字体文件名, bold 是否粗体, italic 是否斜体, names 字体名称,留空匹配默认回退字体 // 返回: []string 按优先级排列的匹配文件名 func FontFileMatches(files []string, bold, italic bool, names ...string) []string { return fontFileMatchNames(fontFileMatches(fontFileCandidates(files, path.Base), names, bold, italic)) } // MatchFontFiles 按文档字体名称和样式选择首选文件,未声明的使用编号按默认规则回退 // 入参: files 可用字体文件名, ids 已发现的使用编号,省略时仅匹配声明字体 // 返回: []string 所需字体文件名, error 错误信息 func (r *Reader) MatchFontFiles(files []string, ids ...string) ([]string, error) { fonts, err := r.Fonts() if err != nil { return nil, err } for _, id := range ids { if r.fontCache[id] == nil { fonts = append(fonts, Font{ID: id}) } } candidates := fontFileCandidates(files, path.Base) names := make([]string, 0, len(fonts)) seen := make(map[string]bool) for _, font := range fonts { if font.FontFile != "" { continue } matches := fontFileMatches(candidates, []string{font.FontName, font.FamilyName}, font.Bold, font.Italic) if len(matches) == 0 { matches = fontFileMatches(candidates, nil, font.Bold, font.Italic) } if len(matches) > 0 { name := matches[0].name if !seen[name] { names = append(names, name) seen[name] = true } } } return names, nil } // Stat 获取字体文件信息 // 返回: fs.FileInfo 文件信息, error 错误信息 func (f *fontMemFile) Stat() (fs.FileInfo, error) { return f.info, nil } // Close 关闭字体文件 // 返回: error 错误信息 func (f *fontMemFile) Close() error { return nil } // Stat 获取字体目录信息 // 返回: fs.FileInfo 文件信息, error 错误信息 func (d *fontDir) Stat() (fs.FileInfo, error) { return fontFileInfo{name: ".", mode: fs.ModeDir}, nil } // Read 字体目录不提供字节内容,始终返回EOF // 入参: []byte 读取缓冲区,不使用 // 返回: int 始终为0, error 始终为io.EOF func (d *fontDir) Read([]byte) (int, error) { return 0, io.EOF } // Close 关闭字体目录 // 返回: error 错误信息 func (d *fontDir) Close() error { return nil } // ReadDir 读取字体目录条目 // 入参: count 读取数量 // 返回: []fs.DirEntry 目录条目, error 错误信息 func (d *fontDir) ReadDir(count int) ([]fs.DirEntry, error) { if d.offset >= len(d.entries) { if count <= 0 { return nil, nil } return nil, io.EOF } if count <= 0 || d.offset+count > len(d.entries) { count = len(d.entries) - d.offset } entries := d.entries[d.offset : d.offset+count] d.offset += count return entries, nil } // Name 获取目录条目名称 // 返回: string 目录条目名称 func (e fontDirEntry) Name() string { return e.info.Name() } // IsDir 判断是否为目录 // 返回: bool 是否为目录 func (e fontDirEntry) IsDir() bool { return false } // Type 获取目录条目类型 // 返回: fs.FileMode 文件模式 func (e fontDirEntry) Type() fs.FileMode { return e.info.Mode().Type() } // Info 获取目录条目信息 // 返回: fs.FileInfo 文件信息, error 错误信息 func (e fontDirEntry) Info() (fs.FileInfo, error) { return e.info, nil } // Name 获取文件名 // 返回: string 文件名 func (i fontFileInfo) Name() string { if i.name == "" { return "." } return i.name } // Size 获取文件大小 // 返回: int64 文件大小 func (i fontFileInfo) Size() int64 { return i.size } // Mode 获取文件模式 // 返回: fs.FileMode 文件模式 func (i fontFileInfo) Mode() fs.FileMode { if i.mode != 0 { return i.mode } return 0444 } // ModTime 获取文件修改时间 // 返回: time.Time 修改时间 func (i fontFileInfo) ModTime() time.Time { return time.Time{} } // IsDir 判断是否为目录 // 返回: bool 是否为目录 func (i fontFileInfo) IsDir() bool { return i.mode.IsDir() } // Sys 获取底层文件信息 // 返回: any 底层文件信息 func (i fontFileInfo) Sys() any { return nil } // matchStyle 匹配指定样式的字体名称 // 入参: names 字体名称列表, bold 是否粗体, italic 是否斜体 // 返回: []fontFileMatch 字体匹配列表 func (fsys *FontFS) matchStyle(names []string, bold, italic bool) []fontFileMatch { return fontFileMatches(fsys.candidates, names, bold, italic) } // fontFileMatches 匹配字体文件候选 // 入参: candidates 字体候选, names 字体名称, bold 是否粗体, italic 是否斜体 // 返回: []fontFileMatch 按优先级排列的字体匹配 func fontFileMatches(candidates []fontFileCandidate, names []string, bold, italic bool) []fontFileMatch { matches := make([]fontFileMatch, 0, len(candidates)) seen := make(map[fontFileKey]int, len(candidates)) if len(names) == 0 { for _, name := range fontDefaultSystemNames() { matches = append(matches, fontFileMatches(candidates, []string{name}, bold, italic)...) } sort.SliceStable(matches, func(i, j int) bool { return matches[i].rank < matches[j].rank }) unique := matches[:0] for _, match := range matches { key := fontFileKey{strings.ToLower(match.name), match.face} if _, ok := seen[key]; !ok { seen[key] = len(unique) unique = append(unique, match) } } return unique } for i, name := range names { if name == "" { continue } for j, alias := range append([]string{name}, fontQualifiedNames(name)...) { matcher := fontPatternMatcher{stem: fontNormalizeName(alias), priority: (i-len(names))*2 + min(j, 1)} if matcher.stem == "" { continue } for _, file := range candidates { if rank := matcher.rankCandidate(file); rank == fontMatchExact { appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic) } } } } for _, matcher := range newFontPatternMatchers(fontFilePatterns(names...)) { for _, file := range candidates { rank := matcher.rankCandidate(file) appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic) } } sortFontFileMatches(matches) return matches } // fallbackFonts 获取回退字体文件 // 入参: bold 是否粗体, italic 是否斜体 // 返回: []string 字体文件列表 func (fsys *FontFS) fallbackFonts(bold, italic bool) []string { if matches := fsys.matchStyle(nil, bold, italic); len(matches) > 0 { return fontFileMatchNames(matches) } if len(fsys.names) > 0 { return []string{fsys.names[0]} } return nil } // entries 获取字体目录条目 // 返回: []fs.DirEntry 目录条目 func (fsys *FontFS) entries() []fs.DirEntry { entries := make([]fs.DirEntry, 0, len(fsys.names)) for _, name := range fsys.names { entries = append(entries, fontDirEntry{info: fontFileInfo{ name: name, size: int64(len(fsys.files[name])), }}) } return entries } // cleanFontName 清理字体文件名 // 入参: name 字体文件名 // 返回: string 清理后的字体文件名 func cleanFontName(name string) string { name = strings.ReplaceAll(name, "\\", "/") name = strings.TrimSpace(path.Clean(name)) if name == "" || name == "/" { return "." } name = path.Base(name) if name == "." || name == "/" { return "." } return strings.ToLower(name) } // fontFileCandidates 筛选字体文件候选 // 入参: names 文件名列表, base 文件名提取函数 // 返回: []fontFileCandidate 字体文件候选列表 func fontFileCandidates(names []string, base func(string) string) []fontFileCandidate { files := make([]fontFileCandidate, 0, len(names)) for _, name := range names { if isFontFileName(name) { baseName := base(name) normalized := fontNormalizeName(strings.TrimSuffix(baseName, path.Ext(baseName))) files = append(files, fontFileCandidate{ name: name, base: baseName, lowerBase: strings.ToLower(baseName), filename: normalized, normalized: normalized, fold: strings.ToLower(name), }) } } return files } // newFontPatternMatcher 创建字体文件匹配器 // 入参: pattern 匹配模式 // 返回: fontPatternMatcher 字体文件匹配器 func newFontPatternMatcher(pattern string) fontPatternMatcher { matcher := fontPatternMatcher{ pattern: pattern, lowerPattern: strings.ToLower(pattern), stem: fontPatternStem(pattern), } if matcher.stem != "" { for _, alias := range fontExactCandidateNames(matcher.stem) { alias = fontNormalizeName(alias) if alias != "" { matcher.aliases = append(matcher.aliases, alias) } } } return matcher } // newFontPatternMatchers 创建字体文件匹配器列表 // 入参: patterns 匹配模式列表 // 返回: []fontPatternMatcher 字体文件匹配器列表 func newFontPatternMatchers(patterns []string) []fontPatternMatcher { matchers := make([]fontPatternMatcher, 0, len(patterns)) for priority, pattern := range patterns { matcher := newFontPatternMatcher(pattern) matcher.priority = priority matchers = append(matchers, matcher) } return matchers } // rank 获取字体文件匹配等级 // 入参: name 字体文件名 // 返回: int 匹配等级 func (m fontPatternMatcher) rank(name string) int { return m.rankCandidate(fontFileCandidate{ base: name, lowerBase: strings.ToLower(name), normalized: fontNormalizeName(name), }) } // rankCandidate 获取字体文件候选匹配等级 // 入参: file 字体文件候选 // 返回: int 匹配等级 func (m fontPatternMatcher) rankCandidate(file fontFileCandidate) int { exactPath, _ := path.Match(m.pattern, file.base) lowerPath, _ := path.Match(m.lowerPattern, file.lowerBase) if m.stem == "" { if exactPath || lowerPath { return fontMatchFuzzy } return fontMatchNone } name := m.candidateName(file) if name == m.stem { return fontMatchExact } for _, alias := range m.aliases { if name == alias { return fontMatchExact } } if fontFileKnownStyleSuffix(m.styleSuffixNormalized(name)) { return fontMatchExact } if strings.HasPrefix(name, m.stem) { return fontMatchPartial } for _, alias := range m.aliases { if strings.HasPrefix(name, alias) { return fontMatchPartial } } if exactPath || lowerPath || strings.Contains(name, m.stem) { return fontMatchFuzzy } for _, alias := range m.aliases { if strings.Contains(name, alias) { return fontMatchFuzzy } } return fontMatchNone } // candidateName 保留同族内部名称的区分,只有无关联时使用调用方文件别名 // 入参: file 字体文件候选 // 返回: string 用于统一匹配名称与样式的规范名称 func (m fontPatternMatcher) candidateName(file fontFileCandidate) string { if len(file.names) == 0 || file.base == "" || m.stem == "" { return file.normalized } for _, name := range file.names { if strings.HasPrefix(name, m.stem) { return file.normalized } for _, alias := range m.aliases { if strings.HasPrefix(name, alias) { return file.normalized } } } return file.filename } // styleSuffixNormalized 获取规范字体名称的样式后缀 // 入参: name 规范字体名称 // 返回: string 样式后缀 func (m fontPatternMatcher) styleSuffixNormalized(name string) string { if m.stem == "" || name == "" { return "" } if strings.HasPrefix(name, m.stem) { return strings.TrimPrefix(name, m.stem) } for _, alias := range m.aliases { if strings.HasPrefix(name, alias) { return strings.TrimPrefix(name, alias) } } return "" } // appendFontFileMatch 追加字体文件匹配结果 // 入参: matches 匹配结果, seen 已匹配文件, matcher 匹配器, file 字体文件候选, rank 匹配等级, bold 是否粗体, italic 是否斜体 func appendFontFileMatch(matches *[]fontFileMatch, seen map[fontFileKey]int, matcher fontPatternMatcher, file fontFileCandidate, rank int, bold, italic bool) { if rank == fontMatchNone { return } suffix := matcher.styleSuffixNormalized(matcher.candidateName(file)) if rank == fontMatchFuzzy { suffix = matcher.styleSuffixNormalized(fontNormalizeName(file.base)) } next := fontFileMatch{ name: file.name, face: file.face, priority: matcher.priority, rank: rank, styleRank: fontFileStyleRank(suffix, bold, italic), sortName: file.lowerBase, } key := fontFileKey{file.fold, file.face} if index, ok := seen[key]; ok { if fontFileMatchLess(next, (*matches)[index]) { (*matches)[index] = next } return } seen[key] = len(*matches) *matches = append(*matches, next) } // sortFontFileMatches 排序字体文件匹配结果 // 入参: matches 匹配结果 func sortFontFileMatches(matches []fontFileMatch) { sort.SliceStable(matches, func(i, j int) bool { return fontFileMatchLess(matches[i], matches[j]) }) } // fontFileMatchLess 判断字体文件匹配结果优先级 // 入参: left 左侧匹配结果, right 右侧匹配结果 // 返回: bool 左侧是否优先 func fontFileMatchLess(left, right fontFileMatch) bool { if left.priority != right.priority { return left.priority < right.priority } if left.rank != right.rank { return left.rank < right.rank } if left.styleRank != right.styleRank { return left.styleRank < right.styleRank } return left.sortName < right.sortName } // fontFileMatchNames 获取字体文件匹配名称 // 入参: matches 匹配结果 // 返回: []string 字体文件列表 func fontFileMatchNames(matches []fontFileMatch) []string { names := make([]string, 0, len(matches)) seen := make(map[string]bool, len(matches)) for _, match := range matches { key := strings.ToLower(match.name) if !seen[key] { seen[key] = true names = append(names, match.name) } } return names } // fontFileStyleRank 获取字体文件样式匹配等级 // 入参: suffix 样式后缀, bold 是否粗体, italic 是否斜体 // 返回: int 样式匹配等级 func fontFileStyleRank(suffix string, bold, italic bool) int { fileBold, fileItalic := fontFileStyleFromSuffix(suffix) rank := 0 if fileBold != bold { rank += 2 } if fileItalic != italic { rank += 2 } if !bold && !italic && !fileBold && !fileItalic && suffix != "" && !fontFileRegularSuffix(suffix) { rank++ } return rank } // fontFileStyleFromSuffix 获取样式后缀对应的字体样式 // 入参: suffix 样式后缀 // 返回: bool 是否粗体, bool 是否斜体 func fontFileStyleFromSuffix(suffix string) (bool, bool) { bold := suffix == "b" || suffix == "bd" || suffix == "bold" || suffix == "bi" || suffix == "bolditalic" || suffix == "boldoblique" italic := suffix == "i" || suffix == "it" || suffix == "italic" || suffix == "oblique" || suffix == "bi" || suffix == "bolditalic" || suffix == "boldoblique" return bold, italic } // fontFileRegularSuffix 判断是否为常规样式后缀 // 入参: suffix 样式后缀 // 返回: bool 是否为常规样式后缀 func fontFileRegularSuffix(suffix string) bool { switch suffix { case "r", "regular", "normal", "常规": return true default: return false } } // fontFileKnownStyleSuffix 判断是否为已知样式后缀 // 入参: suffix 样式后缀 // 返回: bool 是否为已知样式后缀 func fontFileKnownStyleSuffix(suffix string) bool { switch suffix { case "r", "regular", "normal", "常规", "b", "bd", "bold", "i", "it", "italic", "oblique", "bi", "bolditalic", "boldoblique": return true default: return false } }