diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index b000ed1..4aff1d5 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -530,7 +530,7 @@ func (e *Editor) restoreFontSnapshot(state Editor) { *e = state } -// pruneExternalFontFiles 清理全包不再引用的字体文件,遇到无法识别的扩展时保留资源 +// pruneExternalFontFiles 清理全包无引用字体并同步版本清单,保留附件及无法确认的扩展引用 // 入参: ctx 取消上下文, reader 包快照, candidates 待清理的字体路径 // 返回: error 取消或读取错误 func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...string) error { @@ -546,26 +546,29 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s if len(removed) == 0 { return nil } + files, structures := make(map[string]bool), map[string]bool{"OFD.xml": true} for i := range reader.DocumentCount() { - files, err := reader.documentFiles(ctx, i) + _, xmlFiles, err := reader.documentDependencies(ctx, i) if err != nil { if ctx.Err() != nil { return ctx.Err() } return nil } - for name := range files { - delete(removed, name) - } - if len(removed) == 0 { - return nil + maps.Copy(structures, xmlFiles) + } + versions := make(map[string]*editorFontXML) + names := slices.Sorted(maps.Keys(structures)) + for _, name := range slices.Sorted(maps.Keys(reader.fileNames)) { + if !structures[name] && strings.HasSuffix(strings.ToLower(name), ".xml") { + names = append(names, name) } } - for name := range reader.fileNames { + for _, name := range names { if err := ctx.Err(); err != nil { return err } - if !strings.HasSuffix(strings.ToLower(name), ".xml") { + if files[name] && !structures[name] { continue } data, err := reader.readFile(name) @@ -580,19 +583,36 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s if root.matchesOFD("Res") { base = root.attr("BaseLoc") } + if root.matchesOFD("DocVersion") { + versions[name] = &editorFontXML{name: name, data: data, root: root} + } var retains func(*editorXML) bool retains = func(node *editorXML) bool { if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown || slices.Contains([]string{"CustomTags", "Extensions", "ExtendData", "Data"}, node.name.Local) { return true } + parent := "" + if node.parent != nil { + parent = node.parent.name.Local + } for _, attr := range node.attrs { if attr.Name.Space != "" && attr.Name.Space != "xmlns" && attr.Name.Space != "http://www.w3.org/XML/1998/namespace" { return true } + if kind := packageReference(root.name.Local, parent, node.name.Local, attr.Name.Local); attr.Name.Space == "" && kind != packageReferenceNone { + location := cleanPackagePath(resolveResourcePath(name, base, attr.Value)) + delete(removed, location) + if kind == packageReferenceData { + files[location] = true + } + } } - if node.matchesOFD("FontFile") { + if kind := packageReference(root.name.Local, parent, node.name.Local, ""); kind != packageReferenceNone && !(root.matchesOFD("DocVersion") && parent == "FileList" && node.matchesOFD("File")) { location := resolveResourcePath(name, base, strings.TrimSpace(editorImportText(data, node))) delete(removed, cleanPackagePath(location)) + if kind == packageReferenceData { + files[cleanPackagePath(location)] = true + } } return slices.ContainsFunc(node.children, retains) } @@ -603,6 +623,27 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s if err := ctx.Err(); err != nil { return err } + updates := make(map[string][]byte) + for name, file := range versions { + var patches []editorXMLPatch + if list := file.root.child("FileList"); list != nil { + for _, node := range list.children { + if node.matchesOFD("File") && removed[resolveResourcePath(name, "", strings.TrimSpace(editorImportText(file.data, node)))] { + patches = append(patches, editorXMLPatch{start: node.start, end: node.end}) + } + } + } + if len(patches) != 0 { + updates[name] = editorPatchXML(file.data, patches) + } + } + if err := ctx.Err(); err != nil { + return err + } + if reader.files == nil { + reader.files = make(map[string][]byte) + } + maps.Copy(reader.files, updates) for name := range reader.files { if removed[cleanPackagePath(name)] { delete(reader.files, name) diff --git a/ofdgo_editor_font_cleanup.go b/ofdgo_editor_font_cleanup.go new file mode 100644 index 0000000..142766d --- /dev/null +++ b/ofdgo_editor_font_cleanup.go @@ -0,0 +1,103 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "bytes" + "context" + "io" + "maps" + "slices" +) + +// compactVersionFonts 清理仅由版本清单保留的无引用字体,同步清单且不修改编辑器快照 +// 入参: ctx 取消上下文, parts 输出修改, removed 待删除条目 +// 返回: error 读取、结构或取消错误 +func (e *Editor) compactVersionFonts(ctx context.Context, parts map[string][]byte, removed map[string]bool) error { + reader := e.source.reader + versioned := false + for _, body := range reader.OFD.DocBody { + versioned = versioned || body.versioned + } + if !versioned { + return nil + } + protected, err := reader.hasOutputSignatures() + if err != nil || protected { + return err + } + view := &Reader{Zip: reader.Zip, files: maps.Clone(reader.files)} + if view.files == nil { + view.files = make(map[string][]byte) + } + maps.Copy(view.files, parts) + if err := view.initRoot(); err != nil { + return err + } + candidates, seen := make(map[string]bool), make(map[string]bool) + for index := range view.DocumentCount() { + view.documentIndex = index + versions, err := view.DocumentVersions() + if err != nil { + return err + } + for _, version := range versions { + for _, file := range version.Files { + if err := ctx.Err(); err != nil { + return err + } + name := file.Location + if seen[name] || removed[name] { + continue + } + seen[name] = true + input, err := view.openFile(name) + if err != nil { + return err + } + var header [4]byte + n, err := io.ReadFull(input, header[:]) + input.Close() + if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { + return err + } + if n == len(header) { + switch string(header[:]) { + case "\x00\x01\x00\x00", "OTTO", "ttcf": + candidates[name] = true + } + } + } + } + } + if len(candidates) == 0 { + return nil + } + if err := pruneExternalFontFiles(ctx, view, slices.Collect(maps.Keys(candidates))...); err != nil { + return err + } + for name, data := range view.files { + if original, ok := reader.files[name]; !ok || !bytes.Equal(original, data) { + parts[name] = data + } + } + for name := range candidates { + if !view.fileNames[name] { + removed[name] = true + delete(parts, name) + } + } + return nil +} diff --git a/ofdgo_editor_package.go b/ofdgo_editor_package.go index d4f807c..b696060 100644 --- a/ofdgo_editor_package.go +++ b/ofdgo_editor_package.go @@ -620,6 +620,9 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress return 0, err } } + if err := e.compactVersionFonts(ctx, parts, removed); err != nil { + return 0, err + } if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected { name := "OFD.xml" if file, ok := reader.packageFile(name); ok {