feat(优化代码): 统一库与WebUI的字体发现和匹配逻辑
build.yaml / build (push) Successful in 39s

This commit is contained in:
xiaoqidun committed 2026-09-12 16:48:56 +08:00
1 parent d5b8343ac4
commit 3c15a18546
5 files changed
+102 -88

No files matched your search

+15 -49
View File
@@ -1,5 +1,4 @@
const MM_TO_PX = 96 / 25.4; const MM_TO_PX = 96 / 25.4;
const LOCAL_FONT_LOAD_LIMIT = 16;
const FONT_DATABASE = "ofdgo"; const FONT_DATABASE = "ofdgo";
const fontChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(FONT_DATABASE) : null; const fontChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(FONT_DATABASE) : null;
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)"); const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
@@ -765,7 +764,7 @@ async function queryLocalFonts() {
} }
async function autoLoadDocumentLocalFonts(openSeq) { async function autoLoadDocumentLocalFonts(openSeq) {
if (!externalDocumentFontNames().length || !canReadLocalFonts() || state.systemFontPermission === "denied") { if (!state.doc?.fonts?.some((font) => !font.embedded) || !canReadLocalFonts() || state.systemFontPermission === "denied") {
return false; return false;
} }
setProgress("正在匹配字体", 62, STATUS.fonts); setProgress("正在匹配字体", 62, STATUS.fonts);
@@ -787,7 +786,6 @@ async function autoLoadDocumentLocalFonts(openSeq) {
async function loadDocumentLocalFonts(available, openSeq = state.openSeq) { async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
const docFonts = state.doc?.fonts || []; const docFonts = state.doc?.fonts || [];
const docNames = externalDocumentFontNames();
if (!docFonts.length) { if (!docFonts.length) {
state.localFonts = []; state.localFonts = [];
setStatus("暂无字体"); setStatus("暂无字体");
@@ -795,18 +793,14 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
renderFontList(); renderFontList();
return false; return false;
} }
if (!docNames.length) { if (docFonts.every((font) => font.embedded)) {
state.localFonts = []; state.localFonts = [];
setStatus("文档字体均为内嵌"); setStatus("文档字体均为内嵌");
updateFontSummary(); updateFontSummary();
renderFontList(); renderFontList();
return false; return false;
} }
const docLoadLimit = Math.min(LOCAL_FONT_LOAD_LIMIT, Math.max(4, docNames.length * 3)); const selected = await selectLocalFonts(available);
let selected = await selectLocalFonts(available, docNames, docLoadLimit);
if (!selected.length) {
selected = await selectLocalFonts(available, [], 6);
}
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return false; return false;
} }
@@ -834,32 +828,22 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
return fonts.length > 0; return fonts.length > 0;
} }
function uniqueLocalFonts(fonts) { async function selectLocalFonts(fonts) {
const seen = new Set();
const selected = [];
for (const font of fonts) {
const key = normalizeFontName(localFontName(font));
if (!key || seen.has(key)) {
continue;
}
seen.add(key);
selected.push(font);
}
return selected;
}
async function selectLocalFonts(fonts, names, limit) {
const available = new Map(); const available = new Map();
for (const font of uniqueLocalFonts(fonts)) { for (const font of fonts) {
available.set(localFontName(font), font); const names = [
for (const name of [font.postscriptName, [font.family, font.style].filter(Boolean).join(" ")]) { localFontName(font),
if (name) { font.postscriptName && `${font.postscriptName}.ttf`,
available.set(`${name}.ttf`, font); font.family && `${[font.family, font.style].filter(Boolean).join(" ")}.ttf`,
];
for (const name of names) {
if (name && !available.has(name)) {
available.set(name, font);
} }
} }
} }
const matched = await callWASM("ofdgoFontFileMatches", [...available.keys()], names); const matched = await callWASM("ofdgoMatchFontFiles", [...available.keys()]);
return uniqueLocalFonts(matched.map((name) => available.get(name))).slice(0, limit); return [...new Set(matched.map((name) => available.get(name)))];
} }
async function fontData(fonts) { async function fontData(fonts) {
@@ -944,29 +928,11 @@ function fontRecords() {
return [...state.localFonts, ...state.userFonts]; return [...state.localFonts, ...state.userFonts];
} }
function externalDocumentFontNames() {
const names = [];
for (const font of state.doc?.fonts || []) {
if (font.embedded || font.status === "embedded") {
continue;
}
names.push(font.fontName, font.familyName);
}
return names;
}
function localFontName(font) { function localFontName(font) {
const name = font.fullName || font.family || font.postscriptName || "local-font"; const name = font.fullName || font.family || font.postscriptName || "local-font";
return `${name}.ttf`; return `${name}.ttf`;
} }
function normalizeFontName(name = "") {
return String(name)
.toLowerCase()
.replace(/\.[^.]+$/, "")
.replace(/[\s_\-()()]/g, "");
}
async function applyFontChange(options = {}) { async function applyFontChange(options = {}) {
updateFontSummary(); updateFontSummary();
renderFontList(); renderFontList();
+11 -32
View File
@@ -20,8 +20,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"syscall/js" "syscall/js"
"github.com/xiaoqidun/ofdgo"
) )
// currentSession 当前WebUI文档会话 // currentSession 当前WebUI文档会话
@@ -43,7 +41,7 @@ func RunWASM() {
registerCallback("ofdgoExportFormats", exportFormats) registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoExportPage", exportPage) registerCallback("ofdgoExportPage", exportPage)
registerCallback("ofdgoExportPDF", exportPDF) registerCallback("ofdgoExportPDF", exportPDF)
registerCallback("ofdgoFontFileMatches", fontFileMatches) registerCallback("ofdgoMatchFontFiles", matchFontFiles)
select {} select {}
} }
@@ -224,13 +222,14 @@ func bytesToJS(data []byte) js.Value {
return value return value
} }
// fontFileMatches 匹配浏览器字体文件名称 // matchFontFiles 匹配文档所需的字体文件
// 入参: args 浏览器参数 // 入参: args 浏览器参数
// 返回: any 匹配的字体文件名称, error 错误信息 // 返回: any 匹配的字体文件名称, error 错误信息
func fontFileMatches(args []js.Value) (any, error) { func matchFontFiles(args []js.Value) (any, error) {
files := stringsFromJS(jsArg(args, 0)) if currentSession == nil {
names := stringsFromJS(jsArg(args, 1)) return nil, fmt.Errorf("ofd document is not opened")
return ofdgo.FontFileMatches(files, false, false, names...), nil }
return currentSession.Reader.MatchFontFiles(stringsFromJS(args[0]))
} }
// bytesFromJS 从浏览器值读取二进制数据 // bytesFromJS 从浏览器值读取二进制数据
@@ -279,31 +278,11 @@ func fontsFromJS(value js.Value) ([]FontFile, error) {
// 入参: value 浏览器值 // 入参: value 浏览器值
// 返回: []string 字符串列表 // 返回: []string 字符串列表
func stringsFromJS(value js.Value) []string { func stringsFromJS(value js.Value) []string {
if value.IsUndefined() || value.IsNull() { items := make([]string, value.Length())
return nil for i := range items {
items[i] = value.Index(i).String()
} }
if js.Global().Get("Array").Call("isArray", value).Bool() { return items
length := value.Get("length").Int()
items := make([]string, 0, length)
for i := 0; i < length; i++ {
item := value.Index(i)
if !item.IsUndefined() && !item.IsNull() {
items = append(items, item.String())
}
}
return items
}
return []string{value.String()}
}
// jsArg 获取浏览器参数
// 入参: args 浏览器参数列表, index 参数索引
// 返回: js.Value 浏览器值
func jsArg(args []js.Value, index int) js.Value {
if index < 0 || index >= len(args) {
return js.Undefined()
}
return args[index]
} }
// encodeResult 编码浏览器接口返回结果 // encodeResult 编码浏览器接口返回结果
+30
View File
@@ -147,6 +147,36 @@ func FontFileMatches(files []string, bold, italic bool, names ...string) []strin
return fontFileMatchNames(fontFileMatches(fontFileCandidates(files, path.Base), names, bold, italic)) 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 匹配指定样式的字体名称 // matchStyle 匹配指定样式的字体名称
// 入参: names 字体名称列表, bold 是否粗体, italic 是否斜体 // 入参: names 字体名称列表, bold 是否粗体, italic 是否斜体
// 返回: []fontFileMatch 字体匹配列表 // 返回: []fontFileMatch 字体匹配列表
+11 -1
View File
@@ -49,9 +49,19 @@ type FontInfo struct {
// Fonts 获取OFD声明的字体列表 // Fonts 获取OFD声明的字体列表
// 返回: []Font 字体列表, error 错误信息 // 返回: []Font 字体列表, error 错误信息
func (r *Reader) Fonts() ([]Font, error) { func (r *Reader) Fonts() ([]Font, error) {
if _, err := r.Doc(); err != nil { doc, err := r.Doc()
if err != nil {
return nil, err return nil, err
} }
if !r.fontResourcesRead {
for _, page := range doc.Pages.Page {
r.loadPageResources(page)
}
for _, template := range doc.CommonData.TemplatePage {
r.loadPageResources(Page{BaseLoc: template.BaseLoc})
}
r.fontResourcesRead = true
}
fonts := make([]Font, 0, len(r.fontCache)) fonts := make([]Font, 0, len(r.fontCache))
for _, font := range r.fontCache { for _, font := range r.fontCache {
if font != nil { if font != nil {
+35 -6
View File
@@ -32,9 +32,11 @@ type Reader struct {
RootDir string RootDir string
ResMap map[string]string ResMap map[string]string
fontCache map[string]*Font fontCache map[string]*Font
fontResourcesRead bool
colorSpaceCache map[string]*ColorSpace colorSpaceCache map[string]*ColorSpace
drawParamCache map[string]*DrawParam drawParamCache map[string]*DrawParam
compositeGraphicUnitCache map[string]*CompositeGraphicUnit compositeGraphicUnitCache map[string]*CompositeGraphicUnit
pageHeaderCache map[string]PageContent
doc *Document doc *Document
Stamps map[string][]Stamp Stamps map[string][]Stamp
Annots map[string][]Annotation Annots map[string][]Annotation
@@ -78,6 +80,7 @@ func (r *Reader) initRoot() error {
r.colorSpaceCache = make(map[string]*ColorSpace) r.colorSpaceCache = make(map[string]*ColorSpace)
r.drawParamCache = make(map[string]*DrawParam) r.drawParamCache = make(map[string]*DrawParam)
r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit) r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit)
r.pageHeaderCache = make(map[string]PageContent)
return nil return nil
} }
@@ -272,21 +275,38 @@ func (r *Reader) PageArea(page Page) (PageArea, error) {
return r.resolvePageArea(content.Area, content.Template) return r.resolvePageArea(content.Area, content.Template)
} }
// readPageHeader 读取页面区域和模板引用 // loadPageResources 加载页面资源,不解析图元
// 入参: page 页面引用
func (r *Reader) loadPageResources(page Page) {
header, err := r.readPageHeader(page)
if err != nil {
return
}
for _, res := range header.PageRes {
r.loadRes(resolveResourcePath(page.BaseLoc, "", res))
}
}
// readPageHeader 读取页面区域、模板引用和资源声明
// 入参: page 页面对象 // 入参: page 页面对象
// 返回: PageContent 页面头部, error 错误信息 // 返回: PageContent 页面头部, error 错误信息
func (r *Reader) readPageHeader(page Page) (PageContent, error) { func (r *Reader) readPageHeader(page Page) (PageContent, error) {
f, err := r.openFile(r.ResPath(page.BaseLoc)) name := r.ResPath(page.BaseLoc)
if content, ok := r.pageHeaderCache[name]; ok {
return content, nil
}
f, err := r.openFile(name)
if err != nil { if err != nil {
return PageContent{}, err return PageContent{}, err
} }
defer f.Close() defer f.Close()
var content PageContent var content PageContent
d := xml.NewDecoder(f) d := xml.NewDecoder(f)
scan:
for { for {
token, err := d.Token() token, err := d.Token()
if err == io.EOF { if err == io.EOF {
return content, nil break
} }
if err != nil { if err != nil {
return PageContent{}, err return PageContent{}, err
@@ -302,10 +322,17 @@ func (r *Reader) readPageHeader(page Page) (PageContent, error) {
} }
content.Template = append(content.Template, template) content.Template = append(content.Template, template)
case "Area": case "Area":
err := d.DecodeElement(&content.Area, &start) if err := d.DecodeElement(&content.Area, &start); err != nil {
return content, err return PageContent{}, err
}
case "PageRes":
var res string
if err := d.DecodeElement(&res, &start); err != nil {
return PageContent{}, err
}
content.PageRes = append(content.PageRes, res)
case "Content": case "Content":
return content, nil break scan
default: default:
if err := d.Skip(); err != nil { if err := d.Skip(); err != nil {
return PageContent{}, err return PageContent{}, err
@@ -313,6 +340,8 @@ func (r *Reader) readPageHeader(page Page) (PageContent, error) {
} }
} }
} }
r.pageHeaderCache[name] = content
return content, nil
} }
// resolvePageArea 解析页面区域的继承关系 // resolvePageArea 解析页面区域的继承关系