Files
ofdgo/ofdgo_font_fs.go
T
2026-09-27 08:03:10 +08:00

776 lines
21 KiB
Go

// 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"
)
// 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
}
// 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 可用字体文件名
// 返回: []string 所需字体文件名, error 错误信息
func (r *Reader) MatchFontFiles(files []string) ([]string, error) {
fonts, err := r.Fonts()
if err != nil {
return nil, err
}
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
}
// 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)
}
// fontFileCandidate 字体文件候选
type fontFileCandidate struct {
name string
face int
base string
lowerBase string
filename string
normalized string
names []string
fold string
}
// 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
}
// fontPatternMatcher 字体文件匹配器
type fontPatternMatcher struct {
pattern string
lowerPattern string
stem string
aliases []string
priority int
}
// 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 ""
}
// fontFileMatch 字体文件匹配结果
type fontFileMatch struct {
name string
face int
priority int
rank int
styleRank int
sortName string
}
// fontFileKey 字体文件与集合索引的去重键
type fontFileKey struct {
name string
face int
}
// 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
}
}
// fontMemFile 内存字体文件
type fontMemFile struct {
*bytes.Reader
info fontFileInfo
}
// Stat 获取字体文件信息
// 返回: fs.FileInfo 文件信息, error 错误信息
func (f *fontMemFile) Stat() (fs.FileInfo, error) {
return f.info, nil
}
// Close 关闭字体文件
// 返回: error 错误信息
func (f *fontMemFile) Close() error {
return nil
}
// fontDir 字体目录
type fontDir struct {
offset int
entries []fs.DirEntry
}
// 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
}
// fontDirEntry 字体目录条目
type fontDirEntry struct {
info fontFileInfo
}
// 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
}
// fontFileInfo 字体文件信息
type fontFileInfo struct {
name string
size int64
mode fs.FileMode
}
// 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
}
var _ fs.FS = (*FontFS)(nil)
var _ fs.ReadDirFS = (*FontFS)(nil)
var _ fs.GlobFS = (*FontFS)(nil)
var _ fs.File = (*fontMemFile)(nil)
var _ fs.ReadDirFile = (*fontDir)(nil)
var _ fs.DirEntry = (*fontDirEntry)(nil)