From affc84459e04ba9f64f85bb956c6e496c9c6f82e Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sun, 27 Sep 2026 13:11:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BD=AC=E6=8D=A2=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84PDF=E8=BD=AC=E6=8D=A2=E4=B8=8E=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 10 ++++++ go.mod | 2 +- go.sum | 4 +-- internal/webui/wasm.go | 8 ++++- ofdgo_font_fs.go | 11 +++++-- ofdgo_font_info.go | 14 ++++----- ofdgo_font_source.go | 46 ++++++++++++++++------------ ofdgo_pdf_object.go | 43 ++++++++++++++++++++++++-- ofdgo_renderer_font_loader_canvas.go | 2 +- 9 files changed, 103 insertions(+), 37 deletions(-) diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index b376780..7f72fc7 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -4085,8 +4085,18 @@ async function loadDocumentDetails(openSeq) { const info = await callWASM("ofdgoDocumentInfo", true); if (openSeq !== state.openSeq) return; 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: "" }); 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; } } diff --git a/go.mod b/go.mod index c445772..7510da9 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 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/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 4e5815e..52a372c 100644 --- a/go.sum +++ b/go.sum @@ -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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= 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-20260926152052-3a4cb2e9266a/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260927050705-f09ba3f6aa16 h1:yzUX5/pLl38Nczw+yQXck/QZ7sPblNvKu5+55cH6ONM= +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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 6d8f874..e684358 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -921,7 +921,13 @@ func matchFontFiles(args []js.Value) (any, error) { if currentSession == nil { 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 读取上传字体的名称和集合索引 diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index ca82251..758c286 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -182,14 +182,19 @@ func FontFileMatches(files []string, bold, italic bool, names ...string) []strin return fontFileMatchNames(fontFileMatches(fontFileCandidates(files, path.Base), names, bold, italic)) } -// MatchFontFiles 按文档字体名称和样式选择首选文件,跳过内嵌字体 -// 入参: files 可用字体文件名 +// MatchFontFiles 按文档字体名称和样式选择首选文件,未声明的使用编号按默认规则回退 +// 入参: files 可用字体文件名, ids 已发现的使用编号,省略时仅匹配声明字体 // 返回: []string 所需字体文件名, error 错误信息 -func (r *Reader) MatchFontFiles(files []string) ([]string, error) { +func (r *Reader) MatchFontFiles(files []string, ids ...string) ([]string, error) { fonts, err := r.Fonts() if err != nil { return nil, err } + for _, id := range ids { + if r.fontCache[id] == nil { + fonts = append(fonts, Font{ID: id}) + } + } candidates := fontFileCandidates(files, path.Base) names := make([]string, 0, len(fonts)) seen := make(map[string]bool) diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index 1f1edd5..8a77dca 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -262,13 +262,13 @@ func (r *Renderer) fontInfos(usage map[string]int) ([]FontInfo, error) { if seen[id] { continue } - infos = append(infos, FontInfo{ - ID: id, - FontName: id, - Status: FontStatusMissing, - Detail: "字体资源未被声明", - Used: used, - }) + info := r.fontInfo(Font{ID: id}) + info.FontName, info.Used = id, used + info.Detail = "字体资源未被声明" + if info.Status == FontStatusFallback { + info.Detail += ",使用外部字体回退" + } + infos = append(infos, info) } sort.SliceStable(infos, func(i, j int) bool { if infos[i].Used == 0 && infos[j].Used > 0 { diff --git a/ofdgo_font_source.go b/ofdgo_font_source.go index f353924..c6eca06 100644 --- a/ofdgo_font_source.go +++ b/ofdgo_font_source.go @@ -152,28 +152,29 @@ func (r *Renderer) resolveFontSource(backend FontBackend, id string, exact bool) } definition = r.Reader.fontCache[id] } - if definition != nil { - if definition.FontFile != "" { - data, err := r.Reader.FontData(id) - if err != nil { - return ResolvedFont{}, err - } - face, err := r.Reader.embeddedFontFace(*definition) - if err != nil { - return ResolvedFont{}, err - } - return ResolvedFont{Data: data, Source: path.Base(definition.FontFile), Face: face, Exact: true}, nil + if definition == nil { + definition = &Font{ID: id} + } + if definition.FontFile != "" { + data, err := r.Reader.FontData(id) + if err != nil { + return ResolvedFont{}, err } - 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 + face, err := r.Reader.embeddedFontFace(*definition) + 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 { + 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 { 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:]) + 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 _, match := range r.matchFontFS(index, nil, bold, italic) { sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: match.name, face: match.face}) diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index d3e0067..86d65e3 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -21,7 +21,9 @@ import ( "image" "image/color" "image/png" + "maps" "math" + "slices" "strconv" "unicode/utf16" "unicode/utf8" @@ -409,6 +411,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { } // pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号 +// 复合字体的重复映射区段按PDF字符映射重建,不改变CID对应的字形 // 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析 // 返回: []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] changed = true } - if len(tables["cmap"]) == 0 { + if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && pdfCmapOverlaps(tables["cmap"]) { mapping := map[rune]uint16{} - for code := range source.Unicode { + for _, code := range slices.Sorted(maps.Keys(source.Unicode)) { glyphs, err := source.Decode([]byte(code)) if err != nil { return nil, 0, err @@ -536,6 +539,42 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er 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 判断字体表目录是否引用了未写入的末尾对齐字节 // 入参: program OpenType字体数据 // 返回: bool 是否需要重新封装 diff --git a/ofdgo_renderer_font_loader_canvas.go b/ofdgo_renderer_font_loader_canvas.go index 9280b3e..f64d64e 100644 --- a/ofdgo_renderer_font_loader_canvas.go +++ b/ofdgo_renderer_font_loader_canvas.go @@ -44,7 +44,7 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { return nil } if of == nil { - of = &Font{ID: fontID, FontName: "default"} + of = &Font{ID: fontID, FontName: "OFDGoFallback"} } fontData := resolved.Data fontStyle := canvasFontStyle(of)