From 20c863afa524446982ea820b57869a05f85b262a Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Tue, 30 Jun 2026 20:19:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=89=A9=E5=B1=95=E5=8A=9F=E8=83=BD):=20?= =?UTF-8?q?=E6=9B=B4=E5=A5=BD=E7=9A=84=E6=94=AF=E6=8C=81WASM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_font_fs.go | 406 +++++++++++++++++++++++++++++++++++++++++++ ofdgo_font_info.go | 329 +++++++++++++++++++++++++++++++++++ ofdgo_font_native.go | 40 +++++ ofdgo_font_wasm.go | 29 ++++ ofdgo_renderer.go | 33 ++-- 5 files changed, 827 insertions(+), 10 deletions(-) create mode 100644 ofdgo_font_fs.go create mode 100644 ofdgo_font_info.go create mode 100644 ofdgo_font_native.go create mode 100644 ofdgo_font_wasm.go diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go new file mode 100644 index 0000000..f75d59b --- /dev/null +++ b/ofdgo_font_fs.go @@ -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) diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go new file mode 100644 index 0000000..01d6217 --- /dev/null +++ b/ofdgo_font_info.go @@ -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) +} diff --git a/ofdgo_font_native.go b/ofdgo_font_native.go new file mode 100644 index 0000000..3994178 --- /dev/null +++ b/ofdgo_font_native.go @@ -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 +} diff --git a/ofdgo_font_wasm.go b/ofdgo_font_wasm.go new file mode 100644 index 0000000..b16ce42 --- /dev/null +++ b/ofdgo_font_wasm.go @@ -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 +} diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 8c845a5..433bf49 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -40,6 +40,7 @@ type Renderer struct { DPI float64 RenderAnnotations bool fontFamily *canvas.FontFamily + defaultFontLoaded bool DrawParams map[string]*DrawParam CompositeGraphicUnits map[string]*CompositeGraphicUnit FontMap map[string]*canvas.FontFamily @@ -466,15 +467,7 @@ func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam { // initCommon 初始化公共资源 func (r *Renderer) initCommon() { r.fontFamily = canvas.NewFontFamily("default") - 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 { - break - } - } + r.defaultFontLoaded = r.loadDefaultFonts() } // renderImage 渲染图片 @@ -907,7 +900,10 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { if ff, ok := r.FontMap[fontID]; ok { return ff } - defaultFont := r.fontFamily + var defaultFont *canvas.FontFamily + if r.defaultFontLoaded { + defaultFont = r.fontFamily + } of, ok := r.Reader.fontCache[fontID] if !ok { 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} for _, name := range names { if name == "" {