feat(转换能力): 完善PDF转换与字体回退
build.yaml / build (push) Successful in 53s

This commit is contained in:
xiaoqidun committed 2026-09-27 13:11:57 +08:00
1 parent 98d9effc8c
commit affc84459e
9 files changed
+103 -37

No files matched your search

+10
View File
@@ -4085,8 +4085,18 @@ async function loadDocumentDetails(openSeq) {
const info = await callWASM("ofdgoDocumentInfo", true); const info = await callWASM("ofdgoDocumentInfo", true);
if (openSeq !== state.openSeq) return; if (openSeq !== state.openSeq) return;
if (!info.detailsPending) { if (!info.detailsPending) {
const knownFonts = new Set((state.doc.fonts || []).map(font => font.id));
const missingFonts = info.fonts?.some(font => font.used > 0 && font.status === "missing" && !knownFonts.has(font.id));
Object.assign(state.doc, info, { detailsPending: false, detailsError: "" }); Object.assign(state.doc, info, { detailsPending: false, detailsError: "" });
renderMeta(); renderMeta();
if (missingFonts && fontManager.permission === "granted" && !state.editorInfo && !document.body.hasAttribute("aria-busy")) {
setBusy(true);
try {
if (await autoLoadDocumentLocalFonts(openSeq) && openSeq === state.openSeq) await applyFontChange();
} finally {
if (openSeq === state.openSeq) setBusy(false);
}
}
return; return;
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20260926152052-3a4cb2e9266a github.com/xiaoqidun/pdfgo v0.0.0-20260927050705-f09ba3f6aa16
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
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
@@ -62,8 +62,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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20260926152052-3a4cb2e9266a h1:BT4VQisXVY8OlUdZFE6WTOIktGk8fCQOfkWlSxvL+cc= github.com/xiaoqidun/pdfgo v0.0.0-20260927050705-f09ba3f6aa16 h1:yzUX5/pLl38Nczw+yQXck/QZ7sPblNvKu5+55cH6ONM=
github.com/xiaoqidun/pdfgo v0.0.0-20260926152052-3a4cb2e9266a/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xiaoqidun/pdfgo v0.0.0-20260927050705-f09ba3f6aa16/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
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.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+7 -1
View File
@@ -921,7 +921,13 @@ func matchFontFiles(args []js.Value) (any, error) {
if currentSession == nil { if currentSession == nil {
return nil, fmt.Errorf("ofd document is not opened") return nil, fmt.Errorf("ofd document is not opened")
} }
return currentSession.Reader.MatchFontFiles(stringsFromJS(args[0])) ids := make([]string, 0, len(currentSession.fontInfos))
for _, info := range currentSession.fontInfos {
if info.Used > 0 {
ids = append(ids, info.ID)
}
}
return currentSession.Reader.MatchFontFiles(stringsFromJS(args[0]), ids...)
} }
// fontFaces 读取上传字体的名称和集合索引 // fontFaces 读取上传字体的名称和集合索引
+8 -3
View File
@@ -182,14 +182,19 @@ func FontFileMatches(files []string, bold, italic bool, names ...string) []strin
return fontFileMatchNames(fontFileMatches(fontFileCandidates(files, path.Base), names, bold, italic)) return fontFileMatchNames(fontFileMatches(fontFileCandidates(files, path.Base), names, bold, italic))
} }
// MatchFontFiles 按文档字体名称和样式选择首选文件,跳过内嵌字体 // MatchFontFiles 按文档字体名称和样式选择首选文件,未声明的使用编号按默认规则回退
// 入参: files 可用字体文件名 // 入参: files 可用字体文件名, ids 已发现的使用编号,省略时仅匹配声明字体
// 返回: []string 所需字体文件名, error 错误信息 // 返回: []string 所需字体文件名, error 错误信息
func (r *Reader) MatchFontFiles(files []string) ([]string, error) { func (r *Reader) MatchFontFiles(files []string, ids ...string) ([]string, error) {
fonts, err := r.Fonts() fonts, err := r.Fonts()
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, id := range ids {
if r.fontCache[id] == nil {
fonts = append(fonts, Font{ID: id})
}
}
candidates := fontFileCandidates(files, path.Base) candidates := fontFileCandidates(files, path.Base)
names := make([]string, 0, len(fonts)) names := make([]string, 0, len(fonts))
seen := make(map[string]bool) seen := make(map[string]bool)
+7 -7
View File
@@ -262,13 +262,13 @@ func (r *Renderer) fontInfos(usage map[string]int) ([]FontInfo, error) {
if seen[id] { if seen[id] {
continue continue
} }
infos = append(infos, FontInfo{ info := r.fontInfo(Font{ID: id})
ID: id, info.FontName, info.Used = id, used
FontName: id, info.Detail = "字体资源未被声明"
Status: FontStatusMissing, if info.Status == FontStatusFallback {
Detail: "字体资源未被声明", info.Detail += ",使用外部字体回退"
Used: used, }
}) infos = append(infos, info)
} }
sort.SliceStable(infos, func(i, j int) bool { sort.SliceStable(infos, func(i, j int) bool {
if infos[i].Used == 0 && infos[j].Used > 0 { if infos[i].Used == 0 && infos[j].Used > 0 {
+26 -20
View File
@@ -152,28 +152,29 @@ func (r *Renderer) resolveFontSource(backend FontBackend, id string, exact bool)
} }
definition = r.Reader.fontCache[id] definition = r.Reader.fontCache[id]
} }
if definition != nil { if definition == nil {
if definition.FontFile != "" { definition = &Font{ID: id}
data, err := r.Reader.FontData(id) }
if err != nil { if definition.FontFile != "" {
return ResolvedFont{}, err data, err := r.Reader.FontData(id)
} if err != nil {
face, err := r.Reader.embeddedFontFace(*definition) return ResolvedFont{}, err
if err != nil {
return ResolvedFont{}, err
}
return ResolvedFont{Data: data, Source: path.Base(definition.FontFile), Face: face, Exact: true}, nil
} }
if source, metrics := r.fontSourceMatch(backend, id, definition, exact); metrics != nil { face, err := r.Reader.embeddedFontFace(*definition)
data := metrics.Write() if err != nil {
faces, err := (FontFile{Data: data}).Faces() return ResolvedFont{}, err
if err != nil {
return ResolvedFont{}, err
}
face := faces[0]
face.Index = source.face
return ResolvedFont{Data: data, Source: source.name, Face: &face, Exact: source.exact}, nil
} }
return ResolvedFont{Data: data, Source: path.Base(definition.FontFile), Face: face, Exact: true}, nil
}
if source, metrics := r.fontSourceMatch(backend, id, definition, exact); metrics != nil {
data := metrics.Write()
faces, err := (FontFile{Data: data}).Faces()
if err != nil {
return ResolvedFont{}, err
}
face := faces[0]
face.Index = source.face
return ResolvedFont{Data: data, Source: source.name, Face: &face, Exact: source.exact}, nil
} }
if exact { if exact {
return ResolvedFont{}, fmt.Errorf("font %q is unavailable", id) return ResolvedFont{}, fmt.Errorf("font %q is unavailable", id)
@@ -238,6 +239,11 @@ func (r *Renderer) fontSources(fontID string, font *Font) []fontSource {
} }
} }
sortFontSources(sources[systemStart:]) sortFontSources(sources[systemStart:])
for _, dir := range r.fontDirs {
for _, match := range r.matchFontFiles(dir, nil, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFile, name: match.name, face: match.face})
}
}
for index := range r.fontFS { for index := range r.fontFS {
for _, match := range r.matchFontFS(index, nil, bold, italic) { for _, match := range r.matchFontFS(index, nil, bold, italic) {
sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face}) sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face})
+41 -2
View File
@@ -21,7 +21,9 @@ import (
"image" "image"
"image/color" "image/color"
"image/png" "image/png"
"maps"
"math" "math"
"slices"
"strconv" "strconv"
"unicode/utf16" "unicode/utf16"
"unicode/utf8" "unicode/utf8"
@@ -409,6 +411,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
// pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号 // pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号
// 复合字体的重复映射区段按PDF字符映射重建,不改变CID对应的字形
// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析 // 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
// 返回: []byte 封装后的字体数据, uint16 缺失度量前的完整字形数, error 错误信息 // 返回: []byte 封装后的字体数据, uint16 缺失度量前的完整字形数, error 错误信息
func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, error) { func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, error) {
@@ -470,9 +473,9 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
tables["hmtx"] = tables["hmtx"][:metricLength] tables["hmtx"] = tables["hmtx"][:metricLength]
changed = true changed = true
} }
if len(tables["cmap"]) == 0 { if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && pdfCmapOverlaps(tables["cmap"]) {
mapping := map[rune]uint16{} mapping := map[rune]uint16{}
for code := range source.Unicode { for _, code := range slices.Sorted(maps.Keys(source.Unicode)) {
glyphs, err := source.Decode([]byte(code)) glyphs, err := source.Decode([]byte(code))
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@@ -536,6 +539,42 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
return result, repairedLimit, err return result, repairedLimit, err
} }
// pdfCmapOverlaps 检查格式4字符映射是否包含交叠区段
// 入参: data cmap表数据
// 返回: bool 是否存在交叠
func pdfCmapOverlaps(data []byte) bool {
if len(data) < 4 {
return false
}
count := int(binary.BigEndian.Uint16(data[2:]))
if count > (len(data)-4)/8 {
return false
}
for index := range count {
offset := uint64(binary.BigEndian.Uint32(data[8+8*index:]))
if offset > uint64(len(data)) || uint64(len(data))-offset < 16 {
continue
}
sub := data[int(offset):]
if binary.BigEndian.Uint16(sub) != 4 {
continue
}
length := int(binary.BigEndian.Uint16(sub[2:]))
segments := int(binary.BigEndian.Uint16(sub[6:])) / 2
if length > len(sub) || 16+8*segments > length {
continue
}
for i := 1; i < segments; i++ {
previous := binary.BigEndian.Uint16(sub[14+2*(i-1):])
start := binary.BigEndian.Uint16(sub[16+2*segments+2*i:])
if start <= previous {
return true
}
}
}
return false
}
// pdfSFNTMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节 // pdfSFNTMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节
// 入参: program OpenType字体数据 // 入参: program OpenType字体数据
// 返回: bool 是否需要重新封装 // 返回: bool 是否需要重新封装
+1 -1
View File
@@ -44,7 +44,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
return nil return nil
} }
if of == nil { if of == nil {
of = &Font{ID: fontID, FontName: "default"} of = &Font{ID: fontID, FontName: "OFDGoFallback"}
} }
fontData := resolved.Data fontData := resolved.Data
fontStyle := canvasFontStyle(of) fontStyle := canvasFontStyle(of)