mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
feat(扩展功能): 更好的支持WASM
This commit is contained in:
@@ -0,0 +1,406 @@
|
|||||||
|
// 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"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FontFile 内存字体文件
|
||||||
|
type FontFile struct {
|
||||||
|
Name string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontFS 内存字体文件系统
|
||||||
|
type FontFS struct {
|
||||||
|
files map[string][]byte
|
||||||
|
names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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) {
|
||||||
|
name = cleanFontName(name)
|
||||||
|
if name == "." {
|
||||||
|
return &fontDir{entries: fsys.entries()}, nil
|
||||||
|
}
|
||||||
|
data, ok := fsys.files[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
return &fontMemFile{
|
||||||
|
Reader: bytes.NewReader(data),
|
||||||
|
info: fontFileInfo{
|
||||||
|
name: name,
|
||||||
|
size: int64(len(data)),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir 读取字体目录
|
||||||
|
// 入参: name 目录名
|
||||||
|
// 返回: []fs.DirEntry 目录条目, error 错误信息
|
||||||
|
func (fsys *FontFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||||
|
name = cleanFontName(name)
|
||||||
|
if name != "." {
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
return fsys.entries(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glob 匹配字体文件
|
||||||
|
// 入参: pattern 匹配模式
|
||||||
|
// 返回: []string 字体文件列表, error 错误信息
|
||||||
|
func (fsys *FontFS) Glob(pattern string) ([]string, error) {
|
||||||
|
if _, err := path.Match(pattern, ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fsys.match(pattern), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match 匹配指定字体名称
|
||||||
|
// 入参: names 字体名称列表
|
||||||
|
// 返回: string 匹配字体文件, bool 是否为名称匹配
|
||||||
|
func (fsys *FontFS) Match(names ...string) (string, bool) {
|
||||||
|
for _, name := range names {
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matches := fsys.match(name + "*"); len(matches) > 0 {
|
||||||
|
return matches[0], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches := fsys.fallbackFonts(); len(matches) > 0 {
|
||||||
|
return matches[0], false
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// match 匹配字体文件模式
|
||||||
|
// 入参: pattern 匹配模式
|
||||||
|
// 返回: []string 字体文件列表
|
||||||
|
func (fsys *FontFS) match(pattern string) []string {
|
||||||
|
var matches []string
|
||||||
|
for _, name := range fsys.names {
|
||||||
|
if matchFontPattern(pattern, name) {
|
||||||
|
matches = append(matches, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallbackFonts 获取回退字体文件
|
||||||
|
// 返回: []string 字体文件列表
|
||||||
|
func (fsys *FontFS) fallbackFonts() []string {
|
||||||
|
preferred := []string{"simsun.ttc", "msyh.ttc", "simhei.ttf"}
|
||||||
|
for _, item := range preferred {
|
||||||
|
if _, ok := fsys.files[item]; ok {
|
||||||
|
return []string{item}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchFontPattern 匹配字体文件模式
|
||||||
|
// 入参: pattern 匹配模式, name 字体文件名
|
||||||
|
// 返回: bool 是否匹配
|
||||||
|
func matchFontPattern(pattern, name string) bool {
|
||||||
|
if ok, _ := path.Match(pattern, name); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ok, _ := path.Match(strings.ToLower(pattern), strings.ToLower(name)); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
stem := strings.TrimSuffix(pattern, "*")
|
||||||
|
stem = strings.TrimSuffix(stem, path.Ext(stem))
|
||||||
|
stem = normalizeFontFileName(stem)
|
||||||
|
if stem == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name = normalizeFontFileName(name)
|
||||||
|
if strings.HasPrefix(name, stem) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, alias := range fontNameAliases(stem) {
|
||||||
|
if strings.HasPrefix(name, alias) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeFontFileName 规范化字体文件名
|
||||||
|
// 入参: name 字体文件名
|
||||||
|
// 返回: string 规范化后的字体文件名
|
||||||
|
func normalizeFontFileName(name string) string {
|
||||||
|
name = strings.TrimSuffix(path.Base(name), path.Ext(name))
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
replacer := strings.NewReplacer(" ", "", "-", "", "_", "")
|
||||||
|
return replacer.Replace(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fontNameAliases 获取字体名称别名
|
||||||
|
// 入参: name 字体名称
|
||||||
|
// 返回: []string 字体名称别名
|
||||||
|
func fontNameAliases(name string) []string {
|
||||||
|
aliases := []struct {
|
||||||
|
Keys []string
|
||||||
|
Values []string
|
||||||
|
}{
|
||||||
|
{[]string{"宋体", "新宋体", "simsun"}, []string{"simsun"}},
|
||||||
|
{[]string{"黑体", "simhei"}, []string{"simhei"}},
|
||||||
|
{[]string{"楷体", "kaiti", "simkai"}, []string{"simkai"}},
|
||||||
|
{[]string{"仿宋", "fangsong", "simfang"}, []string{"simfang"}},
|
||||||
|
{[]string{"微软雅黑", "microsoftyahei", "yahei", "msyh"}, []string{"msyh"}},
|
||||||
|
}
|
||||||
|
var result []string
|
||||||
|
for _, alias := range aliases {
|
||||||
|
for _, key := range alias.Keys {
|
||||||
|
key = normalizeFontFileName(key)
|
||||||
|
if key != "" && (name == key || strings.Contains(name, key)) {
|
||||||
|
result = append(result, alias.Values...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(name, "宋") {
|
||||||
|
result = append(result, "simsun")
|
||||||
|
}
|
||||||
|
if strings.Contains(name, "黑") {
|
||||||
|
result = append(result, "simhei")
|
||||||
|
}
|
||||||
|
if strings.Contains(name, "楷") {
|
||||||
|
result = append(result, "simkai")
|
||||||
|
}
|
||||||
|
if strings.Contains(name, "仿") {
|
||||||
|
result = append(result, "simfang")
|
||||||
|
}
|
||||||
|
if strings.Contains(name, "雅") {
|
||||||
|
result = append(result, "msyh")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 读取字体目录数据
|
||||||
|
// 返回: int 读取字节数, error 错误信息
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FontStatusEmbedded 字体使用OFD内嵌字体
|
||||||
|
FontStatusEmbedded = "embedded"
|
||||||
|
// FontStatusMatched 字体命中外部字体
|
||||||
|
FontStatusMatched = "matched"
|
||||||
|
// FontStatusFallback 字体使用外部字体回退
|
||||||
|
FontStatusFallback = "fallback"
|
||||||
|
// FontStatusMissing 字体缺失
|
||||||
|
FontStatusMissing = "missing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FontInfo OFD字体诊断信息
|
||||||
|
type FontInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FontName string `json:"fontName"`
|
||||||
|
FamilyName string `json:"familyName"`
|
||||||
|
Charset string `json:"charset"`
|
||||||
|
FontFile string `json:"fontFile"`
|
||||||
|
Embedded bool `json:"embedded"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Matched string `json:"matched"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Used int `json:"used"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonts 获取OFD声明的字体列表
|
||||||
|
// 返回: []Font 字体列表, error 错误信息
|
||||||
|
func (r *Reader) Fonts() ([]Font, error) {
|
||||||
|
if _, err := r.Doc(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fonts := make([]Font, 0, len(r.fontCache))
|
||||||
|
for _, font := range r.fontCache {
|
||||||
|
if font != nil {
|
||||||
|
fonts = append(fonts, *font)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.SliceStable(fonts, func(i, j int) bool {
|
||||||
|
return fonts[i].ID < fonts[j].ID
|
||||||
|
})
|
||||||
|
return fonts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FontInfos 获取OFD字体诊断信息
|
||||||
|
// 返回: []FontInfo 字体诊断列表, error 错误信息
|
||||||
|
func (r *Renderer) FontInfos() ([]FontInfo, error) {
|
||||||
|
if r == nil || r.Reader == nil {
|
||||||
|
return nil, fmt.Errorf("ofd renderer is not initialized")
|
||||||
|
}
|
||||||
|
doc, err := r.Reader.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if doc == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
fonts, err := r.Reader.Fonts()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
usage := r.fontUsage(doc)
|
||||||
|
infos := make([]FontInfo, 0, len(fonts)+len(usage))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, font := range fonts {
|
||||||
|
info := r.fontInfo(font)
|
||||||
|
info.Used = usage[font.ID]
|
||||||
|
infos = append(infos, info)
|
||||||
|
seen[font.ID] = true
|
||||||
|
}
|
||||||
|
for id, used := range usage {
|
||||||
|
if seen[id] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
infos = append(infos, FontInfo{
|
||||||
|
ID: id,
|
||||||
|
FontName: id,
|
||||||
|
Status: FontStatusMissing,
|
||||||
|
Detail: "未在OFD资源中声明",
|
||||||
|
Used: used,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.SliceStable(infos, func(i, j int) bool {
|
||||||
|
if infos[i].Used == 0 && infos[j].Used > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if infos[i].Used > 0 && infos[j].Used == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return infos[i].ID < infos[j].ID
|
||||||
|
})
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fontInfo 获取单个字体诊断信息
|
||||||
|
// 入参: font 字体定义
|
||||||
|
// 返回: FontInfo 字体诊断信息
|
||||||
|
func (r *Renderer) fontInfo(font Font) FontInfo {
|
||||||
|
info := FontInfo{
|
||||||
|
ID: font.ID,
|
||||||
|
FontName: font.FontName,
|
||||||
|
FamilyName: font.FamilyName,
|
||||||
|
Charset: font.Charset,
|
||||||
|
FontFile: font.FontFile,
|
||||||
|
Embedded: font.FontFile != "",
|
||||||
|
}
|
||||||
|
if info.Embedded {
|
||||||
|
if _, err := r.Reader.ResData(font.FontFile); err == nil {
|
||||||
|
info.Status = FontStatusEmbedded
|
||||||
|
info.Matched = path.Base(font.FontFile)
|
||||||
|
info.Detail = "使用OFD内嵌字体"
|
||||||
|
} else {
|
||||||
|
info.Status = FontStatusMissing
|
||||||
|
info.Detail = "内嵌字体文件缺失"
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
if matched, exact := r.matchFont(font.FontName, font.FamilyName); matched != "" {
|
||||||
|
info.Matched = matched
|
||||||
|
if exact {
|
||||||
|
info.Status = FontStatusMatched
|
||||||
|
info.Detail = "使用外部字体文件"
|
||||||
|
} else {
|
||||||
|
info.Status = FontStatusFallback
|
||||||
|
info.Detail = "使用外部字体回退"
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
info.Status = FontStatusMissing
|
||||||
|
info.Detail = "未找到可用字体"
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchFont 匹配外部字体
|
||||||
|
// 入参: names 字体名称列表
|
||||||
|
// 返回: string 匹配字体文件, bool 是否为名称匹配
|
||||||
|
func (r *Renderer) matchFont(names ...string) (string, bool) {
|
||||||
|
for _, fsys := range r.fontFS {
|
||||||
|
if matcher, ok := fsys.(interface {
|
||||||
|
Match(...string) (string, bool)
|
||||||
|
}); ok {
|
||||||
|
if matched, exact := matcher.Match(names...); matched != "" {
|
||||||
|
return matched, exact
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matched := matchFontFS(fsys, names...); matched != "" {
|
||||||
|
return matched, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fsys := range r.fontFS {
|
||||||
|
if matched := fallbackFontFS(fsys); matched != "" {
|
||||||
|
return matched, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchFontFS 从字体文件系统匹配字体
|
||||||
|
// 入参: fsys 字体文件系统, names 字体名称列表
|
||||||
|
// 返回: string 匹配字体文件
|
||||||
|
func matchFontFS(fsys fs.FS, names ...string) string {
|
||||||
|
for _, name := range names {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matches, err := fs.Glob(fsys, name+"*")
|
||||||
|
if err == nil && len(matches) > 0 {
|
||||||
|
return matches[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallbackFontFS 从字体文件系统获取回退字体
|
||||||
|
// 入参: fsys 字体文件系统
|
||||||
|
// 返回: string 回退字体文件
|
||||||
|
func fallbackFontFS(fsys fs.FS) string {
|
||||||
|
matches, err := fs.Glob(fsys, "*")
|
||||||
|
if err == nil && len(matches) > 0 {
|
||||||
|
return matches[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// fontUsage 统计文档字体使用次数
|
||||||
|
// 入参: doc 文档结构
|
||||||
|
// 返回: map[string]int 字体使用次数
|
||||||
|
func (r *Renderer) fontUsage(doc *Document) map[string]int {
|
||||||
|
usage := make(map[string]int)
|
||||||
|
for _, pageRef := range doc.Pages.Page {
|
||||||
|
page, err := r.Reader.PageContent(pageRef)
|
||||||
|
if err == nil {
|
||||||
|
r.countPageFonts(page, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tpl := range doc.CommonData.TemplatePage {
|
||||||
|
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
|
||||||
|
if err == nil {
|
||||||
|
r.countPageFonts(page, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return usage
|
||||||
|
}
|
||||||
|
|
||||||
|
// countPageFonts 统计页面字体使用次数
|
||||||
|
// 入参: page 页面内容, usage 字体使用次数
|
||||||
|
func (r *Renderer) countPageFonts(page *PageContent, usage map[string]int) {
|
||||||
|
for _, layer := range page.Content.Layer {
|
||||||
|
r.countLayerFonts(layer, usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// countLayerFonts 统计图层字体使用次数
|
||||||
|
// 入参: layer 图层, usage 字体使用次数
|
||||||
|
func (r *Renderer) countLayerFonts(layer Layer, usage map[string]int) {
|
||||||
|
if len(layer.Objects) > 0 {
|
||||||
|
for _, obj := range layer.Objects {
|
||||||
|
r.countObjectFonts(obj, usage)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, text := range layer.TextObject {
|
||||||
|
r.countTextFont(text, usage)
|
||||||
|
}
|
||||||
|
for _, cgu := range layer.CompositeGraphicUnit {
|
||||||
|
r.countCompositeFonts(cgu, usage, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// countObjectFonts 统计图元字体使用次数
|
||||||
|
// 入参: obj 图元对象, usage 字体使用次数
|
||||||
|
func (r *Renderer) countObjectFonts(obj GraphicObject, usage map[string]int) {
|
||||||
|
switch obj.Type {
|
||||||
|
case "TextObject":
|
||||||
|
r.countTextFont(obj.TextObject, usage)
|
||||||
|
case "CompositeGraphicUnit", "CompositeObject":
|
||||||
|
r.countCompositeFonts(obj.CompositeGraphicUnit, usage, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// countCompositeFonts 统计复合图元字体使用次数
|
||||||
|
// 入参: cgu 复合图元, usage 字体使用次数, visited 已访问资源
|
||||||
|
func (r *Renderer) countCompositeFonts(cgu CompositeGraphicUnit, usage map[string]int, visited map[string]bool) {
|
||||||
|
if cgu.ResourceID != "" {
|
||||||
|
if visited == nil {
|
||||||
|
visited = make(map[string]bool)
|
||||||
|
}
|
||||||
|
if visited[cgu.ResourceID] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visited[cgu.ResourceID] = true
|
||||||
|
if ref := r.CompositeGraphicUnits[cgu.ResourceID]; ref != nil {
|
||||||
|
r.countCompositeFonts(*ref, usage, visited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cgu.Objects) > 0 {
|
||||||
|
for _, obj := range cgu.Objects {
|
||||||
|
r.countObjectFonts(obj, usage)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, text := range cgu.TextObject {
|
||||||
|
r.countTextFont(text, usage)
|
||||||
|
}
|
||||||
|
for _, sub := range cgu.CompositeGraphicUnit {
|
||||||
|
r.countCompositeFonts(sub, usage, visited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// countTextFont 统计文本字体使用次数
|
||||||
|
// 入参: text 文本对象, usage 字体使用次数
|
||||||
|
func (r *Renderer) countTextFont(text TextObject, usage map[string]int) {
|
||||||
|
fontID := text.Font
|
||||||
|
if fontID == "" && text.DrawParam != "" {
|
||||||
|
fontID = r.drawParamFont(text.DrawParam, nil)
|
||||||
|
}
|
||||||
|
if fontID != "" {
|
||||||
|
usage[fontID]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawParamFont 获取绘制参数字体
|
||||||
|
// 入参: id 绘制参数ID, visited 已访问绘制参数
|
||||||
|
// 返回: string 字体ID
|
||||||
|
func (r *Renderer) drawParamFont(id string, visited map[string]bool) string {
|
||||||
|
if id == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if visited == nil {
|
||||||
|
visited = make(map[string]bool)
|
||||||
|
}
|
||||||
|
if visited[id] {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
visited[id] = true
|
||||||
|
dp := r.DrawParams[id]
|
||||||
|
if dp == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if dp.Font != "" {
|
||||||
|
return dp.Font
|
||||||
|
}
|
||||||
|
return r.drawParamFont(dp.Relative, visited)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//go:build !js || !wasm
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import "github.com/tdewolff/canvas"
|
||||||
|
|
||||||
|
// loadDefaultFonts 加载默认字体
|
||||||
|
// 返回: bool 是否加载成功
|
||||||
|
func (r *Renderer) loadDefaultFonts() bool {
|
||||||
|
sysFonts := []string{
|
||||||
|
"SimHei", "Microsoft YaHei", "SimSun", "KaiTi", "FangSong",
|
||||||
|
"Arial", "Segoe UI", "Times New Roman",
|
||||||
|
}
|
||||||
|
for _, name := range sysFonts {
|
||||||
|
if err := r.fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// canLoadSystemFonts 判断是否可以加载系统字体
|
||||||
|
// 返回: bool 是否可以加载系统字体
|
||||||
|
func canLoadSystemFonts() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
//go:build js && wasm
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
// loadDefaultFonts 加载默认字体
|
||||||
|
// 返回: bool 是否加载成功
|
||||||
|
func (r *Renderer) loadDefaultFonts() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// canLoadSystemFonts 判断是否可以加载系统字体
|
||||||
|
// 返回: bool 是否可以加载系统字体
|
||||||
|
func canLoadSystemFonts() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
+23
-10
@@ -40,6 +40,7 @@ type Renderer struct {
|
|||||||
DPI float64
|
DPI float64
|
||||||
RenderAnnotations bool
|
RenderAnnotations bool
|
||||||
fontFamily *canvas.FontFamily
|
fontFamily *canvas.FontFamily
|
||||||
|
defaultFontLoaded bool
|
||||||
DrawParams map[string]*DrawParam
|
DrawParams map[string]*DrawParam
|
||||||
CompositeGraphicUnits map[string]*CompositeGraphicUnit
|
CompositeGraphicUnits map[string]*CompositeGraphicUnit
|
||||||
FontMap map[string]*canvas.FontFamily
|
FontMap map[string]*canvas.FontFamily
|
||||||
@@ -466,15 +467,7 @@ func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam {
|
|||||||
// initCommon 初始化公共资源
|
// initCommon 初始化公共资源
|
||||||
func (r *Renderer) initCommon() {
|
func (r *Renderer) initCommon() {
|
||||||
r.fontFamily = canvas.NewFontFamily("default")
|
r.fontFamily = canvas.NewFontFamily("default")
|
||||||
sysFonts := []string{
|
r.defaultFontLoaded = r.loadDefaultFonts()
|
||||||
"SimHei", "Microsoft YaHei", "SimSun", "KaiTi", "FangSong",
|
|
||||||
"Arial", "Segoe UI", "Times New Roman",
|
|
||||||
}
|
|
||||||
for _, name := range sysFonts {
|
|
||||||
if err := r.fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderImage 渲染图片
|
// renderImage 渲染图片
|
||||||
@@ -907,7 +900,10 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
if ff, ok := r.FontMap[fontID]; ok {
|
if ff, ok := r.FontMap[fontID]; ok {
|
||||||
return ff
|
return ff
|
||||||
}
|
}
|
||||||
defaultFont := r.fontFamily
|
var defaultFont *canvas.FontFamily
|
||||||
|
if r.defaultFontLoaded {
|
||||||
|
defaultFont = r.fontFamily
|
||||||
|
}
|
||||||
of, ok := r.Reader.fontCache[fontID]
|
of, ok := r.Reader.fontCache[fontID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return defaultFont
|
return defaultFont
|
||||||
@@ -985,6 +981,23 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !canLoadSystemFonts() {
|
||||||
|
for _, fsys := range r.fontFS {
|
||||||
|
if matches, err := fs.Glob(fsys, "*"); err == nil {
|
||||||
|
for _, m := range matches {
|
||||||
|
resData, err := fs.ReadFile(fsys, m)
|
||||||
|
if err == nil {
|
||||||
|
if err := ff.LoadFont(resData, 0, fontStyle); err == nil {
|
||||||
|
r.FontMap[fontID] = ff
|
||||||
|
return ff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.FontMap[fontID] = defaultFont
|
||||||
|
return defaultFont
|
||||||
|
}
|
||||||
names := []string{of.FamilyName, of.FontName}
|
names := []string{of.FamilyName, of.FontName}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user