From 9d6bf3e87f745f5cb70551480d8795b1bf96ec65 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 28 Oct 2026 18:50:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E4=BD=93=E9=80=89=E6=8B=A9):=20?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=AD=97=E4=BD=93=E4=BC=98=E5=85=88=E4=B8=AD?= =?UTF-8?q?=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 50 ++++++++++++++++++++++++++++---------- assets/webui/ofdgo_font.js | 19 +++++++++++++++ internal/webui/webui.go | 15 ++++++++++++ ofdgo_font.go | 35 ++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 13 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 8a75e12..5171187 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -113,6 +113,8 @@ function createDocumentState() { outlineExpanded: new Map(), textFonts: [], textFontID: null, + textFontInitialized: false, + textFontInitializing: null, textDefaults: { type: "TextObject", size: 16 * 25.4 / 72, color: "#000000", wrap: true, align: "left", paragraphHeight: 0, letterSpacing: 0 }, ofdBytes: null, sourcePDF: false, @@ -4363,13 +4365,12 @@ function editorFonts(item) { return [...embedded.values(), ...fonts]; } -async function loadEditorFonts(requestPermission = true) { - const tab = documentTab(), target = tab.state; - const tasks = []; - if (!state.fontCatalogLoading && fontManager.canReadLocal() && !fontManager.catalogLoaded && fontManager.permission !== "denied" +async function loadEditorFonts(requestPermission = true, tab = documentTab(), waitPending = false) { + const target = tab.state; + const tasks = waitPending ? [target.fontCatalogLoading, target.fontFacesLoading].filter(Boolean) : []; + if (!target.fontCatalogLoading && fontManager.canReadLocal() && !fontManager.catalogLoaded && fontManager.permission !== "denied" && (requestPermission || fontManager.permission === "granted")) { - target.fontCatalogLoading = true; - tasks.push((async () => { + target.fontCatalogLoading = (async () => { try { await fontManager.queryLocal(); refreshEditorFonts(); @@ -4378,13 +4379,13 @@ async function loadEditorFonts(requestPermission = true) { } finally { target.fontCatalogLoading = false; } - })()); + })(); + tasks.push(target.fontCatalogLoading); } - if (!state.fontFacesLoading && fontManager.userFonts.some(font => font.enabled && !font.faces)) { - target.fontFacesLoading = true; + if (!target.fontFacesLoading && fontManager.userFonts.some(font => font.enabled && !font.faces)) { const wasmSeq = target.wasmSeq; const current = () => !tab.closed && !target.wasmExited && target.wasmSeq === wasmSeq; - tasks.push((async () => { + target.fontFacesLoading = (async () => { try { await fontManager.loadFaces(data => callTabWASM(tab, "ofdgoFontFaces", data), current); refreshEditorFonts(); @@ -4392,11 +4393,33 @@ async function loadEditorFonts(requestPermission = true) { target.fontFacesLoading = false; if (!current() && state.ready) loadEditorFonts(false); } - })()); + })(); + tasks.push(target.fontFacesLoading); } await Promise.all(tasks); } +async function initializeTextFont(tab) { + const target = tab.state; + if (target.textFontInitialized) return; + if (target.textFontInitializing) return target.textFontInitializing; + const wasmSeq = target.wasmSeq; + const current = () => !tab.closed && !target.wasmExited && target.wasmSeq === wasmSeq; + target.textFontInitializing = (async () => { + await loadEditorFonts(false, tab, true); + if (!current()) return; + const font = await fontManager.defaultFont((faces, families) => callTabWASM(tab, "ofdgoFontPreferenceOrder", JSON.stringify({ + faces: faces.map(({ family, fullName, postscriptName, style, names }) => ({ family, fullName, postscriptName, style, names })), families + })), async (font, text) => callTabWASM(tab, "ofdgoCheckTextFont", await readTextFont(font, tab), text), current); + if (!current()) return; + target.textDefaults.fontChoice = font || (fontManager.editorFonts().length ? null : undefined); + target.textFontInitialized = true; + if (isForegroundDocument(tab)) refreshEditorFonts(); + })(); + try { await target.textFontInitializing; } + finally { target.textFontInitializing = null; } +} + function refreshEditorFonts() { updateTextFonts(canvasEditor.selected, true, true); } @@ -4446,9 +4469,9 @@ function updateTextFonts(item, refresh = false, preserve = false) { state.textFonts.unshift(selected); } const mixed = (item?.items || item?.richText) && !id; - fontPicker.setFonts(state.textFonts, mixed ? null : selected || state.textFonts[0], preserve); + fontPicker.setFonts(state.textFonts, mixed ? null : selected || (state.textDefaults.fontChoice === null ? null : state.textFonts[0]), preserve); el.textFont.placeholder = mixed ? "混合" : ""; - if (!id && !item?.items && !item?.richText) state.textDefaults.fontChoice = state.textFonts[Number(fontPicker.value)]; + if (!id && !item?.items && !item?.richText) state.textDefaults.fontChoice = state.textFonts[Number(fontPicker.value)] || (state.textDefaults.fontChoice === null ? null : undefined); } } @@ -6567,6 +6590,7 @@ async function openDocument(options = {}) { return; } const pageFonts = state.sourcePDF && !nativeHost && !state.editorInfo ? await fontManager.files(fontManager.records()) : fonts; + if (state.editing) await initializeTextFont(tab); if (openSeq !== state.openSeq) return; tab.worker.configurePages?.(!state.editorInfo && !doc.encrypted && state.ofdBytes ? { file: state.ofdBytes, fonts: pageFonts, pdf: state.sourcePDF, fontNames: state.sourcePDF ? [...fontManager.localEntries().keys()] : [], annotations: state.renderAnnotations, document: doc.documentIndex, version: doc.documentVersion diff --git a/assets/webui/ofdgo_font.js b/assets/webui/ofdgo_font.js index 3f847ca..9c24574 100644 --- a/assets/webui/ofdgo_font.js +++ b/assets/webui/ofdgo_font.js @@ -335,6 +335,25 @@ export class FontManager { } } + async defaultFont(order, inspect, current = () => true) { + const fonts = this.editorFonts().filter(font => !font.disabled); + if (!fonts.length) return; + const families = [["DengXian", "等线"], ["Microsoft YaHei", "微软雅黑"], ["PingFang SC", "苹方-简", "苹方 简体"], + ["Noto Sans SC", "Noto Sans CJK SC"], ["Source Han Sans SC", "Source Han Sans CN", "思源黑体", "思源黑体 CN"]]; + const indices = await order(fonts, families); + let fallback = fonts.length; + for (const index of indices) { + if (!current()) return; + try { + await inspect(fonts[index], "中文汉字编辑水印,。!?"); + if (current()) return fonts[index]; + } catch (error) { + if (error.missingGlyphs) fallback = Math.min(fallback, index); + } + } + if (current()) return fonts[fallback]; + } + async add(fonts) { let saved = true; try { diff --git a/internal/webui/webui.go b/internal/webui/webui.go index e436697..19756bb 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -3175,6 +3175,20 @@ func (e *businessEngine) fontFaces(args []platformValue) (any, error) { return (ofdgo.FontFile{Data: data}).Faces() } +// fontPreferenceOrder 按字体族偏好排列编辑字体候选索引 +// 入参: args 候选字体和字体族别名组的JSON数据 +// 返回: any 候选索引列表, error 错误信息 +func (e *businessEngine) fontPreferenceOrder(args []platformValue) (any, error) { + var request struct { + Faces []ofdgo.FontFace `json:"faces"` + Families [][]string `json:"families"` + } + if err := json.Unmarshal([]byte(args[0].String()), &request); err != nil { + return nil, err + } + return ofdgo.FontPreferenceOrder(request.Faces, request.Families), nil +} + // fontFace 提取编辑时选定的字体,供输入预览和保存共用 // 入参: args 字体数据和零起始索引 // 返回: any 独立字体数据, error 错误信息 @@ -8053,6 +8067,7 @@ func (e *businessEngine) businessCommands() map[string]func([]platformValue) (an "ofdgoSaveRichText": e.saveRichText, "ofdgoPreviewRichText": e.previewRichText, "ofdgoCheckTextFont": e.checkTextFont, + "ofdgoFontPreferenceOrder": e.fontPreferenceOrder, "ofdgoInsertImage": e.insertImage, "ofdgoInsertShape": e.insertShape, "ofdgoUpdatePathStyle": e.updatePathStyle, diff --git a/ofdgo_font.go b/ofdgo_font.go index a2e69d5..31edec7 100644 --- a/ofdgo_font.go +++ b/ofdgo_font.go @@ -17,6 +17,7 @@ package ofdgo import ( "fmt" "path" + "sort" "unicode/utf8" basefont "github.com/xiaoqidun/ofdgo/internal/base/font" @@ -25,6 +26,40 @@ import ( // FontFace 字体文件中的字体名称与零起始索引,不依赖文件名 type FontFace = basefont.Face +// FontPreferenceOrder 按字体族偏好排列候选索引,同族优先常规样式,其余保持原顺序 +// 入参: faces 候选字体, families 按优先级排列的字体族别名组 +// 返回: []int 候选字体的零起始索引,不修改输入列表 +func FontPreferenceOrder(faces []FontFace, families [][]string) []int { + priorities := make(map[string]int) + for priority, names := range families { + for _, name := range names { + key := FontNormalizeName(name) + if _, exists := priorities[key]; key != "" && !exists { + priorities[key] = priority + } + } + } + order, ranks := make([]int, len(faces)), make([]int, len(faces)) + for i, face := range faces { + order[i], ranks[i] = i, len(families)*2 + names := append([]string{face.Family, face.FullName, face.PostScriptName}, face.Names...) + for _, name := range names { + if priority, exists := priorities[FontNormalizeName(name)]; exists && priority*2 < ranks[i] { + ranks[i] = priority * 2 + } + } + if ranks[i] < len(families)*2 { + switch FontNormalizeName(face.Style) { + case "", "regular", "normal", "roman", "常规", "正常", "标准": + default: + ranks[i]++ + } + } + } + sort.SliceStable(order, func(i, j int) bool { return ranks[order[i]] < ranks[order[j]] }) + return order +} + // FixFontDataAggressive 激进修复字体数据 // 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据 // 对显式字形映射补充私有字符cmap,便于按glyph id渲染