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
+63 -176

No files matched your search

+7 -108
View File
@@ -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`;
+1 -1
View File
@@ -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
)
+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/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=
+7 -6
View File
@@ -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 从浏览器值读取二进制数据
+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 {
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]}
+1 -11
View File
@@ -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 统计文档字体使用次数
+14 -20
View File
@@ -152,16 +152,7 @@ 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})
}
}
r.fontSourceCache[fontID] = sources
return sources
}
if canLoadSystemFonts() {
systemDirs := systemFontDirs()
for _, name := range []string{font.FamilyName, font.FontName} {
for _, systemName := range fontSystemNames(name) {
@@ -174,6 +165,18 @@ func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle
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
return 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 获取系统字体目录
-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(" ", "", "-", "", "_", "", "(", "", ")", "", "(", "", ")", "")
// FontNormalizeName 规范化字体名称
+8
View File
@@ -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 {