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
+101 -87

No files matched your search

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