feat(渲染能力): 统一字体回退逻辑,修复阅读与PDF导出字体不一致的问题
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-09-09 20:46:17 +08:00
1 parent e5e07f92cd
commit 853ffc6327
9 files changed
+70 -183

No files matched your search

+7 -108
View File
@@ -1,21 +1,4 @@
const MM_TO_PX = 96 / 25.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 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;
@@ -38,7 +21,7 @@ const WASM_CALLBACKS = [
"ofdgoExportFormats", "ofdgoExportFormats",
"ofdgoExportPage", "ofdgoExportPage",
"ofdgoExportPDF", "ofdgoExportPDF",
"ofdgoFontSystemNames", "ofdgoFontFileMatches",
]; ];
let wasmPromise = null; let wasmPromise = null;
@@ -807,12 +790,10 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
renderFontList(); renderFontList();
return false; return false;
} }
const docWanted = fontNameKeys(docNames);
const docLoadLimit = Math.min(LOCAL_FONT_LOAD_LIMIT, Math.max(4, docNames.length * 3)); 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) { if (!selected.length) {
const fallbackWanted = fontNameKeys(COMMON_FONT_NAMES); selected = selectLocalFonts(available, [], 6);
selected = selectLocalFonts(available, fallbackWanted, 6);
} }
const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到文档所需字体"; const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到文档所需字体";
const fonts = []; const fonts = [];
@@ -852,26 +833,10 @@ function uniqueLocalFonts(fonts) {
return selected; return selected;
} }
function selectLocalFonts(fonts, wanted, limit) { function selectLocalFonts(fonts, names, limit) {
const ranked = []; const available = new Map(uniqueLocalFonts(fonts).map((font) => [localFontName(font), font]));
for (const font of fonts) { const matched = callWASM("ofdgoFontFileMatches", [...available.keys()], names);
const rank = localFontMatchRank(font, wanted); return matched.slice(0, limit).map((name) => available.get(name));
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);
} }
async function fontData(fonts) { async function fontData(fonts) {
@@ -967,72 +932,6 @@ function externalDocumentFontNames() {
return names; 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) { 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`;
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.26.0
require ( require (
github.com/tdewolff/canvas v0.0.0-20260901160717-dae8cd8e19a7 github.com/tdewolff/canvas v0.0.0-20260901160717-dae8cd8e19a7
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 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/image v0.46.0
golang.org/x/text v0.42.0 golang.org/x/text v0.42.0
) )
+2 -2
View File
@@ -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/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 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ= 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-20260909105725-e4eceed095da h1:qvwltcRzcntuKMFH/qlABdat9PsCsAjq7dcwKINVijA=
github.com/xiaoqidun/jbig2 v0.0.0-20260909060133-14ddd3628846/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= 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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+7 -6
View File
@@ -43,7 +43,7 @@ func RunWASM() {
registerCallback("ofdgoExportFormats", exportFormats) registerCallback("ofdgoExportFormats", exportFormats)
registerCallback("ofdgoExportPage", exportPage) registerCallback("ofdgoExportPage", exportPage)
registerCallback("ofdgoExportPDF", exportPDF) registerCallback("ofdgoExportPDF", exportPDF)
registerCallback("ofdgoFontSystemNames", fontSystemNames) registerCallback("ofdgoFontFileMatches", fontFileMatches)
select {} select {}
} }
@@ -224,12 +224,13 @@ func bytesToJS(data []byte) js.Value {
return value return value
} }
// fontSystemNames 获取系统字体名称 // fontFileMatches 匹配浏览器字体文件名称
// 入参: args 浏览器参数 // 入参: args 浏览器参数
// 返回: any 系统字体名称, error 错误信息 // 返回: any 匹配的字体文件名称, error 错误信息
func fontSystemNames(args []js.Value) (any, error) { func fontFileMatches(args []js.Value) (any, error) {
names := stringsFromJS(jsArg(args, 0)) files := stringsFromJS(jsArg(args, 0))
return ofdgo.FontSystemNames(names...), nil names := stringsFromJS(jsArg(args, 1))
return ofdgo.FontFileMatches(files, false, false, names...), nil
} }
// bytesFromJS 从浏览器值读取二进制数据 // bytesFromJS 从浏览器值读取二进制数据
+23 -21
View File
@@ -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 { if matches := fsys.matchPatternsStyle(fontFilePatterns(names...), bold, italic); len(matches) > 0 {
return matches[0], true 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 matches[0], false
} }
return "", false return "", false
} }
// match 匹配字体文件模式 // FontFileMatches 匹配指定样式的字体文件名称
// 入参: pattern 匹配模式 // 入参: files 可用字体文件名, bold 是否粗体, italic 是否斜体, names 字体名称,留空匹配默认回退字体
// 返回: []string 字体文件列表 // 返回: []string 按优先级排列的匹配文件名
func (fsys *FontFS) match(pattern string) []string { func FontFileMatches(files []string, bold, italic bool, names ...string) []string {
return fsys.matchStyle(pattern, false, false) if len(names) == 0 {
} names = fontDefaultSystemNames()
}
// matchStyle 匹配指定样式的字体文件模式 return fontFileMatches(fontFileCandidates(files, path.Base), fontFilePatterns(names...), bold, italic)
// 入参: pattern 匹配模式, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表
func (fsys *FontFS) matchStyle(pattern string, bold, italic bool) []string {
return fsys.matchPatternsStyle([]string{pattern}, bold, italic)
} }
// matchPatternsStyle 匹配指定样式的字体文件模式 // matchPatternsStyle 匹配指定样式的字体文件模式
// 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体 // 入参: patterns 匹配模式列表, bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表 // 返回: []string 字体文件列表
func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string { func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []string {
matches := make([]fontFileMatch, 0, len(fsys.candidates)) return fontFileMatches(fsys.candidates, patterns, bold, italic)
seen := make(map[string]int, len(fsys.candidates)) }
// 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 _, matcher := range newFontPatternMatchers(patterns) {
for _, file := range fsys.candidates { for _, file := range candidates {
rank := matcher.rankCandidate(file) rank := matcher.rankCandidate(file)
appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic) appendFontFileMatch(&matches, seen, matcher, file, rank, bold, italic)
} }
@@ -166,12 +169,11 @@ func (fsys *FontFS) matchPatternsStyle(patterns []string, bold, italic bool) []s
} }
// fallbackFonts 获取回退字体文件 // fallbackFonts 获取回退字体文件
// 入参: bold 是否粗体, italic 是否斜体
// 返回: []string 字体文件列表 // 返回: []string 字体文件列表
func (fsys *FontFS) fallbackFonts() []string { func (fsys *FontFS) fallbackFonts(bold, italic bool) []string {
for _, item := range fontFallbackFiles { if matches := fsys.matchPatternsStyle(fontFilePatterns(fontDefaultSystemNames()...), bold, italic); len(matches) > 0 {
if matches := fsys.match(item); len(matches) > 0 { return matches
return []string{matches[0]}
}
} }
if len(fsys.names) > 0 { if len(fsys.names) > 0 {
return []string{fsys.names[0]} return []string{fsys.names[0]}
+1 -11
View File
@@ -189,17 +189,7 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
// 返回: []string 字体文件列表 // 返回: []string 字体文件列表
func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []string { func fontFSMatchesStyle(fsys fs.FS, patterns []string, bold, italic bool) []string {
names, _ := fs.Glob(fsys, "*") names, _ := fs.Glob(fsys, "*")
candidates := fontFileCandidates(names, path.Base) return fontFileMatches(fontFileCandidates(names, path.Base), patterns, bold, italic)
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)
} }
// fontUsage 统计文档字体使用次数 // fontUsage 统计文档字体使用次数
+21 -27
View File
@@ -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}) sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name, exact: true})
} }
} }
if !canLoadSystemFonts() { if canLoadSystemFonts() {
for index, fsys := range r.fontFS { systemDirs := systemFontDirs()
names, _ := fs.Glob(fsys, "*") for _, name := range []string{font.FamilyName, font.FontName} {
for _, name := range names { for _, systemName := range fontSystemNames(name) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: 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 index, fsys := range r.fontFS {
for _, name := range []string{font.FamilyName, font.FontName} { for _, name := range fontFSMatchesStyle(fsys, fontFilePatterns(fontDefaultSystemNames()...), bold, italic) {
for _, systemName := range fontSystemNames(name) { sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
systemPatterns := fontFilePatterns(systemName) }
for _, dir := range systemDirs { }
for _, match := range r.matchFontFiles(dir, systemPatterns, bold, italic) { for index, fsys := range r.fontFS {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match, exact: true}) names, _ := fs.Glob(fsys, "*")
} for _, name := range names {
} sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name})
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceSystem, name: systemName, exact: true})
} }
} }
r.fontSourceCache[fontID] = sources r.fontSourceCache[fontID] = sources
@@ -256,16 +259,7 @@ func (r *Renderer) matchFontFiles(dir string, patterns []string, bold, italic bo
candidates = fontFileCandidates(files, filepath.Base) candidates = fontFileCandidates(files, filepath.Base)
r.fontDirCandidates[dir] = candidates r.fontDirCandidates[dir] = candidates
} }
matches := make([]fontFileMatch, 0, len(candidates)) return fontFileMatches(candidates, patterns, bold, italic)
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)
} }
// systemFontDirs 获取系统字体目录 // systemFontDirs 获取系统字体目录
-7
View File
@@ -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(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "") var fontNameReplacer = strings.NewReplacer(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
// FontNormalizeName 规范化字体名称 // FontNormalizeName 规范化字体名称
+8
View File
@@ -196,6 +196,14 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
for i, glyph := range glyphs { for i, glyph := range glyphs {
str := glyph.Text str := glyph.Text
drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 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 glyphPath *canvas.Path
var glyphWidth float64 var glyphWidth float64
if drawAsGlyphPath { if drawAsGlyphPath {