From 853ffc63273e312b1447d9c96558afff6ab9036c Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 9 Sep 2026 20:46:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B8=B2=E6=9F=93=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=AD=97=E4=BD=93=E5=9B=9E=E9=80=80=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=98=85=E8=AF=BB=E4=B8=8E?= =?UTF-8?q?PDF=E5=AF=BC=E5=87=BA=E5=AD=97=E4=BD=93=E4=B8=8D=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 115 +++-------------------------------------- go.mod | 2 +- go.sum | 4 +- internal/webui/wasm.go | 13 ++--- ofdgo_font_fs.go | 44 ++++++++-------- ofdgo_font_info.go | 12 +---- ofdgo_font_loader.go | 48 ++++++++--------- ofdgo_font_match.go | 7 --- ofdgo_renderer_text.go | 8 +++ 9 files changed, 70 insertions(+), 183 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 4354cc0..31af447 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -1,21 +1,4 @@ const MM_TO_PX = 96 / 25.4; -const COMMON_FONT_NAMES = [ - "SimSun", - "NSimSun", - "SimHei", - "KaiTi", - "FangSong", - "Microsoft YaHei", - "PingFang SC", - "Noto Sans CJK SC", - "Source Han Sans SC", - "FZXiaoBiaoSong-B05", - "Arial", - "Arial Black", - "Helvetica", - "Courier New", - "Times New Roman", -]; const LOCAL_FONT_LOAD_LIMIT = 16; const FONT_DATABASE = "ofdgo"; const fontChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(FONT_DATABASE) : null; @@ -38,7 +21,7 @@ const WASM_CALLBACKS = [ "ofdgoExportFormats", "ofdgoExportPage", "ofdgoExportPDF", - "ofdgoFontSystemNames", + "ofdgoFontFileMatches", ]; let wasmPromise = null; @@ -807,12 +790,10 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) { renderFontList(); return false; } - const docWanted = fontNameKeys(docNames); const docLoadLimit = Math.min(LOCAL_FONT_LOAD_LIMIT, Math.max(4, docNames.length * 3)); - let selected = selectLocalFonts(available, docWanted, docLoadLimit); + let selected = selectLocalFonts(available, docNames, docLoadLimit); if (!selected.length) { - const fallbackWanted = fontNameKeys(COMMON_FONT_NAMES); - selected = selectLocalFonts(available, fallbackWanted, 6); + selected = selectLocalFonts(available, [], 6); } const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到文档所需字体"; const fonts = []; @@ -852,26 +833,10 @@ function uniqueLocalFonts(fonts) { return selected; } -function selectLocalFonts(fonts, wanted, limit) { - const ranked = []; - for (const font of fonts) { - const rank = localFontMatchRank(font, wanted); - if (!rank) { - continue; - } - ranked.push({ - font, - rank, - styleRank: localFontStyleRank(font), - name: normalizeFontName(localFontName(font)), - }); - } - ranked.sort((a, b) => ( - a.rank - b.rank || - a.styleRank - b.styleRank || - a.name.localeCompare(b.name) - )); - return uniqueLocalFonts(ranked.map((item) => item.font)).slice(0, limit); +function selectLocalFonts(fonts, names, limit) { + const available = new Map(uniqueLocalFonts(fonts).map((font) => [localFontName(font), font])); + const matched = callWASM("ofdgoFontFileMatches", [...available.keys()], names); + return matched.slice(0, limit).map((name) => available.get(name)); } async function fontData(fonts) { @@ -967,72 +932,6 @@ function externalDocumentFontNames() { return names; } -function fontNameKeys(names) { - const keys = new Set(); - for (const name of fontSystemNames(names)) { - const key = normalizeFontName(name); - if (key) { - keys.add(key); - } - } - return keys; -} - -function fontSystemNames(names) { - const source = (Array.isArray(names) ? names : []) - .map((name) => String(name || "").trim()) - .filter((name) => name !== ""); - if (!source.length) { - return []; - } - if (state.ready && !state.wasmExited && typeof globalThis.ofdgoFontSystemNames === "function") { - try { - const names = callWASM("ofdgoFontSystemNames", source); - if (Array.isArray(names)) { - return names; - } - } catch { - return source; - } - } - return source; -} - -function localFontMatchRank(font, wanted) { - let best = 0; - for (const name of [font.family, font.fullName, font.postscriptName, font.style]) { - const key = normalizeFontName(name); - if (!key) { - continue; - } - for (const wantedKey of wanted) { - let rank = 0; - if (key === wantedKey) { - rank = 1; - } else if (key.startsWith(wantedKey) || wantedKey.startsWith(key)) { - rank = 2; - } else if (key.includes(wantedKey) || wantedKey.includes(key)) { - rank = 3; - } - if (rank && (!best || rank < best)) { - best = rank; - } - } - } - return best; -} - -function localFontStyleRank(font) { - const key = normalizeFontName(font.style); - if (!key || key === "regular" || key === "normal") { - return 0; - } - if (key.includes("bold") || key.includes("italic")) { - return 2; - } - return 1; -} - function localFontName(font) { const name = font.fullName || font.family || font.postscriptName || "local-font"; return `${name}.ttf`; diff --git a/go.mod b/go.mod index a605fbf..0ae77d6 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.0 require ( github.com/tdewolff/canvas v0.0.0-20260901160717-dae8cd8e19a7 github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 - github.com/xiaoqidun/jbig2 v0.0.0-20260909060133-14ddd3628846 + github.com/xiaoqidun/jbig2 v0.0.0-20260909105725-e4eceed095da golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 ) diff --git a/go.sum b/go.sum index c675480..3b8e814 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E= github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ= -github.com/xiaoqidun/jbig2 v0.0.0-20260909060133-14ddd3628846 h1:xbfsQuNR/xaZiopLQE6EQ98hvkISg6OknH4CkeGwh7I= -github.com/xiaoqidun/jbig2 v0.0.0-20260909060133-14ddd3628846/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= +github.com/xiaoqidun/jbig2 v0.0.0-20260909105725-e4eceed095da h1:qvwltcRzcntuKMFH/qlABdat9PsCsAjq7dcwKINVijA= +github.com/xiaoqidun/jbig2 v0.0.0-20260909105725-e4eceed095da/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 68d5c7b..c7efeac 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -43,7 +43,7 @@ func RunWASM() { registerCallback("ofdgoExportFormats", exportFormats) registerCallback("ofdgoExportPage", exportPage) registerCallback("ofdgoExportPDF", exportPDF) - registerCallback("ofdgoFontSystemNames", fontSystemNames) + registerCallback("ofdgoFontFileMatches", fontFileMatches) select {} } @@ -224,12 +224,13 @@ func bytesToJS(data []byte) js.Value { return value } -// fontSystemNames 获取系统字体名称 +// fontFileMatches 匹配浏览器字体文件名称 // 入参: args 浏览器参数 -// 返回: any 系统字体名称, error 错误信息 -func fontSystemNames(args []js.Value) (any, error) { - names := stringsFromJS(jsArg(args, 0)) - return ofdgo.FontSystemNames(names...), nil +// 返回: 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 } // bytesFromJS 从浏览器值读取二进制数据 diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index 70b0c95..8cf7c69 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -129,34 +129,37 @@ func (fsys *FontFS) MatchStyle(bold, italic bool, names ...string) (string, bool if matches := fsys.matchPatternsStyle(fontFilePatterns(names...), bold, italic); len(matches) > 0 { return matches[0], true } - if matches := fsys.fallbackFonts(); len(matches) > 0 { + if matches := fsys.fallbackFonts(bold, italic); len(matches) > 0 { return matches[0], false } return "", false } -// match 匹配字体文件模式 -// 入参: pattern 匹配模式 -// 返回: []string 字体文件列表 -func (fsys *FontFS) match(pattern string) []string { - return fsys.matchStyle(pattern, false, false) -} - -// matchStyle 匹配指定样式的字体文件模式 -// 入参: pattern 匹配模式, bold 是否粗体, italic 是否斜体 -// 返回: []string 字体文件列表 -func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string { - return fsys.matchPatternsStyle([]string{pattern}, bold, italic) +// FontFileMatches 匹配指定样式的字体文件名称 +// 入参: files 可用字体文件名, bold 是否粗体, italic 是否斜体, names 字体名称,留空匹配默认回退字体 +// 返回: []string 按优先级排列的匹配文件名 +func FontFileMatches(files []string, bold, italic bool, names ...string) []string { + if len(names) == 0 { + names = fontDefaultSystemNames() + } + return fontFileMatches(fontFileCandidates(files, path.Base), fontFilePatterns(names...), bold, italic) } // matchPatternsStyle 匹配指定样式的字体文件模式 // 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体 // 返回: []string 字体文件列表 func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string { - matches := make([]fontFileMatch, 0, len(fsys.candidates)) - seen := make(map[string]int, len(fsys.candidates)) + return fontFileMatches(fsys.candidates, patterns, bold, italic) +} + +// fontFileMatches 匹配字体文件候选 +// 入参: candidates 字体候选, patterns 匹配模式, bold 是否粗体, italic 是否斜体 +// 返回: []string 按优先级排列的匹配文件名 +func fontFileMatches(candidates []fontFileCandidate, patterns []string, bold, italic bool) []string { + matches := make([]fontFileMatch, 0, len(candidates)) + seen := make(map[string]int, len(candidates)) for _, matcher := range newFontPatternMatchers(patterns) { - for _, file := range fsys.candidates { + for _, file := range candidates { rank := matcher.rankCandidate(file) appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic) } @@ -166,12 +169,11 @@ func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []s } // fallbackFonts 获取回退字体文件 +// 入参: bold 是否粗体, italic 是否斜体 // 返回: []string 字体文件列表 -func (fsys *FontFS) fallbackFonts() []string { - for _, item := range fontFallbackFiles { - if matches := fsys.match(item); len(matches) > 0 { - return []string{matches[0]} - } +func (fsys *FontFS) fallbackFonts(bold, italic bool) []string { + if matches := fsys.matchPatternsStyle(fontFilePatterns(fontDefaultSystemNames()...), bold, italic); len(matches) > 0 { + return matches } if len(fsys.names) > 0 { return []string{fsys.names[0]} diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index 0507762..0053ef6 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -189,17 +189,7 @@ func (r *Renderer) fontInfo(font Font) FontInfo { // 返回: []string 字体文件列表 func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []string { names, _ := fs.Glob(fsys, "*") - candidates := fontFileCandidates(names, path.Base) - matches := make([]fontFileMatch, 0, len(candidates)) - seen := make(map[string]int, len(candidates)) - for _, matcher := range newFontPatternMatchers(patterns) { - for _, file := range candidates { - rank := matcher.rankCandidate(file) - appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic) - } - } - sortFontFileMatches(matches) - return fontFileMatchNames(matches) + return fontFileMatches(fontFileCandidates(names, path.Base), patterns, bold, italic) } // fontUsage 统计文档字体使用次数 diff --git a/ofdgo_font_loader.go b/ofdgo_font_loader.go index 2997745..6b347f3 100644 --- a/ofdgo_font_loader.go +++ b/ofdgo_font_loader.go @@ -152,26 +152,29 @@ func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name, exact: true}) } } - if !canLoadSystemFonts() { - for index, fsys := range r.fontFS { - names, _ := fs.Glob(fsys, "*") - for _, name := range names { - sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name}) + if canLoadSystemFonts() { + systemDirs := systemFontDirs() + for _, name := range []string{font.FamilyName, font.FontName} { + for _, systemName := range fontSystemNames(name) { + systemPatterns := fontFilePatterns(systemName) + for _, dir := range systemDirs { + for _, match := range r.matchFontFiles(dir, systemPatterns, bold, italic) { + sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match, exact: true}) + } + } + sources = appendFontSource(sources, seen, fontSource{kind: fontSourceSystem, name: systemName, exact: true}) } } - r.fontSourceCache[fontID] = sources - return sources } - systemDirs := systemFontDirs() - for _, name := range []string{font.FamilyName, font.FontName} { - for _, systemName := range fontSystemNames(name) { - systemPatterns := fontFilePatterns(systemName) - for _, dir := range systemDirs { - for _, match := range r.matchFontFiles(dir, systemPatterns, bold, italic) { - sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match, exact: true}) - } - } - sources = appendFontSource(sources, seen, fontSource{kind: fontSourceSystem, name: systemName, exact: true}) + for index, fsys := range r.fontFS { + for _, name := range fontFSMatchesStyle(fsys, fontFilePatterns(fontDefaultSystemNames()...), bold, italic) { + sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name}) + } + } + for index, fsys := range r.fontFS { + names, _ := fs.Glob(fsys, "*") + for _, name := range names { + sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name}) } } r.fontSourceCache[fontID] = sources @@ -256,16 +259,7 @@ func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bo candidates = fontFileCandidates(files, filepath.Base) r.fontDirCandidates[dir] = candidates } - matches := make([]fontFileMatch, 0, len(candidates)) - index := make(map[string]int, len(candidates)) - for _, matcher := range newFontPatternMatchers(patterns) { - for _, file := range candidates { - rank := matcher.rankCandidate(file) - appendFontFileMatch(&matches, index, matcher, file, rank, bold, italic) - } - } - sortFontFileMatches(matches) - return fontFileMatchNames(matches) + return fontFileMatches(candidates, patterns, bold, italic) } // systemFontDirs 获取系统字体目录 diff --git a/ofdgo_font_match.go b/ofdgo_font_match.go index acab535..a9990b2 100644 --- a/ofdgo_font_match.go +++ b/ofdgo_font_match.go @@ -110,13 +110,6 @@ var fontMatchRules = []fontMatchRule{ }, } -var fontFallbackFiles = []string{ - "simsun.ttc", "simsun.ttf", "nsimsun.ttf", - "msyh.ttc", "msyh.ttf", "simhei.ttf", - "NotoSansCJK*.ttc", "NotoSansSC*.otf", "SourceHanSansSC*.otf", - "arial.ttf", -} - var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "") // FontNormalizeName 规范化字体名称 diff --git a/ofdgo_renderer_text.go b/ofdgo_renderer_text.go index 7c8d28f..659bbcc 100644 --- a/ofdgo_renderer_text.go +++ b/ofdgo_renderer_text.go @@ -196,6 +196,14 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 for i, glyph := range glyphs { str := glyph.Text drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 + if !drawAsGlyphPath { + for _, char := range str { + if face.Font.GlyphIndex(char) == 0 { + drawAsGlyphPath = true + break + } + } + } var glyphPath *canvas.Path var glyphWidth float64 if drawAsGlyphPath {