mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
396 lines
13 KiB
Go
396 lines
13 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"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"sync"
|
|
|
|
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
|
|
)
|
|
|
|
// 字体来源:文件、文件系统和系统字体
|
|
const (
|
|
fontSourceFile fontSourceKind = iota
|
|
fontSourceFS
|
|
fontSourceSystem
|
|
)
|
|
|
|
// systemFontIndex 按需建立只读系统字体索引
|
|
var systemFontIndex = sync.OnceValue(indexSystemFonts)
|
|
|
|
// fontSourceKind 字体来源类型
|
|
type fontSourceKind uint8
|
|
|
|
// fontSource 字体来源
|
|
type fontSource struct {
|
|
kind fontSourceKind
|
|
index int
|
|
face int
|
|
name string
|
|
exact bool
|
|
priority int
|
|
}
|
|
|
|
// fontSourceKey 字体来源去重键
|
|
type fontSourceKey struct {
|
|
kind fontSourceKind
|
|
index int
|
|
face int
|
|
name string
|
|
}
|
|
|
|
// fontSourceCache 保存字体文件候选与原始数据,不包含绘图库对象
|
|
type fontSourceCache struct {
|
|
candidates map[string][]fontSource
|
|
directories map[string][]fontFileCandidate
|
|
filesystems map[int][]fontFileCandidate
|
|
data map[fontSourceKey]fontSourceData
|
|
fallback ResolvedFont
|
|
fallbackRead bool
|
|
}
|
|
|
|
// fontSourceData 保存读取结果,避免重复打开同一字体文件
|
|
type fontSourceData struct {
|
|
data []byte
|
|
err error
|
|
}
|
|
|
|
// newFontSourceCache 创建独立字体来源缓存
|
|
// 返回: *fontSourceCache 字体来源缓存
|
|
func newFontSourceCache() *fontSourceCache {
|
|
return &fontSourceCache{candidates: make(map[string][]fontSource), directories: make(map[string][]fontFileCandidate), filesystems: make(map[int][]fontFileCandidate), data: make(map[fontSourceKey]fontSourceData)}
|
|
}
|
|
|
|
// child 复用相同来源的文件索引与数据,匹配和回退状态保持独立
|
|
// 返回: *fontSourceCache 独立文档缓存
|
|
func (c *fontSourceCache) child() *fontSourceCache {
|
|
cache := newFontSourceCache()
|
|
cache.directories, cache.filesystems, cache.data = c.directories, c.filesystems, c.data
|
|
return cache
|
|
}
|
|
|
|
// readFontSource 读取独立字体并规范符号映射,保留集合中的名称和样式选择
|
|
// 入参: source 字体来源, definition 字体定义
|
|
// 返回: []byte 字体数据, error 读取或集合错误
|
|
func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) {
|
|
key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name}
|
|
if source.kind == fontSourceSystem {
|
|
matches := fontFileMatches(systemFontIndex(), []string{source.name}, definition.Bold, definition.Italic)
|
|
if len(matches) == 0 {
|
|
return nil, fmt.Errorf("font %q not found", source.name)
|
|
}
|
|
key = fontSourceKey{kind: fontSourceFile, name: matches[0].name, face: matches[0].face}
|
|
}
|
|
if cached, ok := r.fontSourcesCache.data[key]; ok {
|
|
return cached.data, cached.err
|
|
}
|
|
var data []byte
|
|
var err error
|
|
if key.kind == fontSourceFS {
|
|
data, err = fs.ReadFile(r.fontFS[key.index], key.name)
|
|
} else {
|
|
data, err = os.ReadFile(key.name)
|
|
}
|
|
if err == nil && bytes.HasPrefix(data, []byte("ttcf")) {
|
|
data, err = basefont.ExtractCollectionFont(data, key.face)
|
|
}
|
|
if err == nil {
|
|
data, _, err = basefont.SymbolProgram(context.Background(), data, "")
|
|
}
|
|
r.fontSourcesCache.data[key] = fontSourceData{data, err}
|
|
return data, err
|
|
}
|
|
|
|
// fontSourceMatch 匹配可被当前字体后端解析的来源
|
|
// 入参: backend 字体后端, fontID 字体ID, definition 字体定义, exact 是否禁止无关回退
|
|
// 返回: fontSource 字体来源, FontMetrics 独立字体度量
|
|
func (r *Renderer) fontSourceMatch(backend FontBackend, fontID string, definition *Font, exact bool) (fontSource, FontMetrics) {
|
|
for _, source := range r.fontSources(fontID, definition) {
|
|
if exact && !source.exact {
|
|
continue
|
|
}
|
|
data, err := r.readFontSource(source, definition)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if metrics, err := backend.OpenFont(data); err == nil {
|
|
return source, metrics
|
|
}
|
|
}
|
|
return fontSource{}, nil
|
|
}
|
|
|
|
// resolveFontSource 按公共匹配规则读取字体,由指定后端验证外部字体
|
|
// 入参: backend 字体后端, id 字体ID, exact 是否禁止无关回退
|
|
// 返回: ResolvedFont 字体及来源, error 解析错误
|
|
func (r *Renderer) resolveFontSource(backend FontBackend, id string, exact bool) (ResolvedFont, error) {
|
|
definition := r.Reader.fontDefinition(id)
|
|
if definition == nil && !r.Reader.fontResourcesRead && r.Reader.OFD != nil {
|
|
if _, err := r.Reader.Fonts(); err != nil {
|
|
return ResolvedFont{}, err
|
|
}
|
|
definition = r.Reader.fontDefinition(id)
|
|
}
|
|
if definition == nil {
|
|
definition = &Font{ID: id}
|
|
}
|
|
if definition.FontFile != "" {
|
|
data, err := r.Reader.fontData(id, true)
|
|
if err != nil {
|
|
return ResolvedFont{}, err
|
|
}
|
|
face, err := r.Reader.embeddedFontFace(*definition)
|
|
if err != nil {
|
|
return ResolvedFont{}, err
|
|
}
|
|
return ResolvedFont{Data: data, Source: path.Base(definition.FontFile), Face: face, Exact: true}, nil
|
|
}
|
|
if source, metrics := r.fontSourceMatch(backend, id, definition, exact); metrics != nil {
|
|
data := metrics.Write()
|
|
faces, err := (FontFile{Data: data}).Faces()
|
|
if err != nil {
|
|
return ResolvedFont{}, err
|
|
}
|
|
face := faces[0]
|
|
face.Index = source.face
|
|
return ResolvedFont{Data: data, Source: source.name, Face: &face, Exact: source.exact}, nil
|
|
}
|
|
if exact {
|
|
return ResolvedFont{}, fmt.Errorf("font %q is unavailable", id)
|
|
}
|
|
if !r.fontSourcesCache.fallbackRead {
|
|
r.fontSourcesCache.fallbackRead = true
|
|
if canLoadSystemFonts() {
|
|
for _, name := range fontDefaultSystemNames() {
|
|
data, err := r.readFontSource(fontSource{kind: fontSourceSystem, name: name}, &Font{})
|
|
if err != nil {
|
|
continue
|
|
}
|
|
metrics, err := backend.OpenFont(data)
|
|
if err == nil {
|
|
r.fontSourcesCache.fallback = ResolvedFont{Data: metrics.Write()}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return r.fontSourcesCache.fallback, nil
|
|
}
|
|
|
|
// fontSources 获取字体来源列表
|
|
// 入参: fontID 字体ID, font OFD字体定义
|
|
// 返回: []fontSource 字体来源列表
|
|
func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
|
|
if sources, ok := r.fontSourcesCache.candidates[fontID]; ok {
|
|
return sources
|
|
}
|
|
bold, italic := font.Bold, font.Italic
|
|
names := []string{font.FontName, font.FamilyName}
|
|
sources := make([]fontSource, 0)
|
|
seen := make(map[fontSourceKey]bool)
|
|
for _, dir := range r.fontDirs {
|
|
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
|
}
|
|
}
|
|
for index := range r.fontFS {
|
|
for _, match := range r.matchFontFS(index, names, bold, italic) {
|
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
|
}
|
|
}
|
|
sortFontSources(sources)
|
|
systemStart := len(sources)
|
|
if canLoadSystemFonts() {
|
|
for _, dir := range systemFontDirs() {
|
|
for _, match := range r.matchFontFiles(dir, names, bold, italic) {
|
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face, exact: match.rank == fontMatchExact, priority: min(match.priority, 0)})
|
|
}
|
|
}
|
|
for _, name := range fontSystemNames(names...) {
|
|
source := fontSource{kind: fontSourceSystem, name: name, exact: true}
|
|
for i, requested := range names {
|
|
if basefont.NormalizeName(name) == basefont.NormalizeName(requested) {
|
|
source.priority = (i - len(names)) * 2
|
|
break
|
|
}
|
|
}
|
|
sources = appendFontSource(sources, seen, source)
|
|
}
|
|
}
|
|
sortFontSources(sources[systemStart:])
|
|
for _, dir := range r.fontDirs {
|
|
for _, match := range r.matchFontFiles(dir, nil, bold, italic) {
|
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face})
|
|
}
|
|
}
|
|
for index := range r.fontFS {
|
|
for _, match := range r.matchFontFS(index, nil, bold, italic) {
|
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face})
|
|
}
|
|
}
|
|
for index, fsys := range r.fontFS {
|
|
names, _ := fs.Glob(fsys, "*")
|
|
for _, name := range names {
|
|
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
|
|
}
|
|
}
|
|
r.fontSourcesCache.candidates[fontID] = sources
|
|
return sources
|
|
}
|
|
|
|
// sortFontSources 将完整名称匹配排在回退候选之前
|
|
// 入参: sources 字体来源列表
|
|
func sortFontSources(sources []fontSource) {
|
|
sort.SliceStable(sources, func(i, j int) bool {
|
|
return sources[i].priority < sources[j].priority
|
|
})
|
|
}
|
|
|
|
// appendFontSource 追加字体来源
|
|
// 入参: sources 字体来源列表, seen 去重映射, source 字体来源
|
|
// 返回: []fontSource 字体来源列表
|
|
func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source fontSource) []fontSource {
|
|
if source.name == "" {
|
|
return sources
|
|
}
|
|
key := fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}
|
|
if seen[key] {
|
|
return sources
|
|
}
|
|
seen[key] = true
|
|
return append(sources, source)
|
|
}
|
|
|
|
// matchFontFiles 查找字体文件
|
|
// 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体
|
|
// 返回: []fontFileMatch 字体匹配列表
|
|
func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []fontFileMatch {
|
|
candidates, ok := r.fontSourcesCache.directories[dir]
|
|
if !ok {
|
|
files, _ := filepath.Glob(filepath.Join(dir, "*"))
|
|
candidates = fontFileCandidates(files, filepath.Base)
|
|
for i, candidate := range candidates {
|
|
file, err := os.Open(candidate.name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
candidates = appendFontFileNames(candidates, i, basefont.FileNames(file))
|
|
file.Close()
|
|
}
|
|
r.fontSourcesCache.directories[dir] = candidates
|
|
}
|
|
return fontFileMatches(candidates, names, bold, italic)
|
|
}
|
|
|
|
// matchFontFS 匹配字体文件系统中的字体名称
|
|
// 入参: index 字体文件系统索引, names 字体名称, bold 是否粗体, italic 是否斜体
|
|
// 返回: []fontFileMatch 字体匹配列表
|
|
func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []fontFileMatch {
|
|
fsys := r.fontFS[index]
|
|
if fonts, ok := fsys.(*FontFS); ok {
|
|
return fonts.matchStyle(names, bold, italic)
|
|
}
|
|
candidates, ok := r.fontSourcesCache.filesystems[index]
|
|
if !ok {
|
|
files, _ := fs.Glob(fsys, "*")
|
|
candidates = fontFileCandidates(files, path.Base)
|
|
for i, candidate := range candidates {
|
|
file, err := fsys.Open(candidate.name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
reader, ok := file.(io.ReaderAt)
|
|
if !ok {
|
|
data, _ := io.ReadAll(file)
|
|
reader = bytes.NewReader(data)
|
|
}
|
|
candidates = appendFontFileNames(candidates, i, basefont.FileNames(reader))
|
|
file.Close()
|
|
}
|
|
r.fontSourcesCache.filesystems[index] = candidates
|
|
}
|
|
return fontFileMatches(candidates, names, bold, italic)
|
|
}
|
|
|
|
// systemFontDirs 获取系统字体目录
|
|
// 返回: []string 字体目录列表
|
|
func systemFontDirs() []string {
|
|
home, _ := os.UserHomeDir()
|
|
switch runtime.GOOS {
|
|
case "android":
|
|
return []string{"/product/fonts", "/system/fonts"}
|
|
case "darwin":
|
|
return []string{"/System/Library/Fonts", "/Library/Fonts", filepath.Join(home, "Library", "Fonts")}
|
|
case "linux":
|
|
dirs := []string{"/usr/share/fonts", "/usr/local/share/fonts", filepath.Join(home, ".fonts")}
|
|
data := os.Getenv("XDG_DATA_HOME")
|
|
if data == "" {
|
|
data = filepath.Join(home, ".local", "share")
|
|
}
|
|
dirs = append(dirs, filepath.Join(data, "fonts"))
|
|
for _, dir := range filepath.SplitList(os.Getenv("XDG_DATA_DIRS")) {
|
|
dirs = append(dirs, filepath.Join(dir, "fonts"))
|
|
}
|
|
return dirs
|
|
case "windows":
|
|
windows := os.Getenv("WINDIR")
|
|
if windows == "" {
|
|
windows = `C:\Windows`
|
|
}
|
|
dirs := []string{filepath.Join(windows, "Fonts")}
|
|
if local := os.Getenv("LOCALAPPDATA"); local != "" {
|
|
dirs = append(dirs, filepath.Join(local, "Microsoft", "Windows", "Fonts"))
|
|
}
|
|
return dirs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// indexSystemFonts 递归读取系统字体名称,保留集合索引
|
|
// 返回: []fontFileCandidate 字体候选
|
|
func indexSystemFonts() []fontFileCandidate {
|
|
var files []string
|
|
seen := make(map[string]bool)
|
|
for _, dir := range systemFontDirs() {
|
|
_ = filepath.WalkDir(dir, func(name string, entry fs.DirEntry, err error) error {
|
|
if err == nil && !entry.IsDir() && basefont.IsFontFileName(name) && !seen[name] {
|
|
seen[name] = true
|
|
files = append(files, name)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
candidates := fontFileCandidates(files, filepath.Base)
|
|
for i, candidate := range candidates {
|
|
file, err := os.Open(candidate.name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
candidates = appendFontFileNames(candidates, i, basefont.FileNames(file))
|
|
file.Close()
|
|
}
|
|
return candidates
|
|
}
|