From c405c2c5bf5c94ba2620e4b03bbe087c65ce5777 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sat, 10 Oct 2026 20:58:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=8F=82=E7=85=A7=E6=A0=87=E5=87=86):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B5=84=E6=BA=90=E4=BE=9D=E8=B5=96=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8E=8B=E7=BC=A9=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_compression.go | 30 +++- ofdgo_reader_dependencies.go | 306 +++++++++++++++++++++++++---------- ofdgo_version_edit.go | 36 ++--- 3 files changed, 264 insertions(+), 108 deletions(-) diff --git a/ofdgo_compression.go b/ofdgo_compression.go index 10f23e7..e6b0297 100644 --- a/ofdgo_compression.go +++ b/ofdgo_compression.go @@ -180,8 +180,9 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) (map[string]bool, bool, error) { names := make(map[string]bool) safe := true - if len(r.OFD.DocBody) != 0 { - dependencies, err := r.documentFiles(ctx, r.DocumentIndex()) + structured := len(r.OFD.DocBody) != 0 + if structured { + dependencies, structures, err := r.documentDependencies(ctx, r.DocumentIndex()) if err != nil { if ctx.Err() != nil { return nil, false, ctx.Err() @@ -193,8 +194,8 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) safe = false break } - names[name] = true } + names = structures } else { for name := range r.fileIndex { names[name] = true @@ -222,8 +223,9 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) buffer := bufio.NewReader(imageInput{ReadCloser: input, context: ctx}) header, _ := buffer.Peek(512) header = bytes.TrimSpace(bytes.TrimPrefix(header, []byte{0xef, 0xbb, 0xbf})) - if len(header) > 0 && header[0] == '<' { + if structured || len(header) > 0 && header[0] == '<' { decoder := xml.NewDecoder(buffer) + var stack []xml.Name for { token, err := decoder.Token() if err == io.EOF { @@ -233,16 +235,27 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) safe = false break } - if element, ok := token.(xml.StartElement); ok { - if classifyOFDNamespace(element.Name.Space) == ofdXMLUnknown { + switch element := token.(type) { + case xml.StartElement: + if len(stack) != 0 && classifyOFDNamespace(element.Name.Space) == ofdXMLUnknown && element.Name.Space != stack[len(stack)-1].Space { safe = false break } + if len(stack) != 0 && stack[len(stack)-1].Local == "Extension" && element.Name.Local == "Data" { + if err := decoder.Skip(); err != nil { + safe = false + } + break + } + stack = append(stack, element.Name) if element.Name.Local == "Pattern" { geometry = false } resource, masked := "", false for _, attr := range element.Attr { + if attr.Name.Space != "" { + continue + } if attr.Name.Local == "Visible" && (attr.Value == "false" || attr.Value == "0") { geometry = false } @@ -257,6 +270,11 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) if masked { masks[resource] = true } + case xml.EndElement: + stack = stack[:len(stack)-1] + } + if !safe { + break } } } diff --git a/ofdgo_reader_dependencies.go b/ofdgo_reader_dependencies.go index b636533..b72467e 100644 --- a/ofdgo_reader_dependencies.go +++ b/ofdgo_reader_dependencies.go @@ -15,8 +15,6 @@ package ofdgo import ( - "bufio" - "bytes" "context" "encoding/xml" "fmt" @@ -24,124 +22,264 @@ import ( "strings" ) -// documentFiles 按标准路径遍历独立文档依赖,签名摘要引用不作为内容依赖 +// 包内引用分为无引用、原始数据及OFD结构文件 +const ( + packageReferenceNone = iota + packageReferenceData + packageReferenceXML +) + +// packageReference 根据节点位置识别文件引用,不将签名摘要引用或扩展数据解释为结构依赖 +// 入参: root 文件根节点, parent 父节点, name 当前节点, attribute 属性名称,空值表示节点文本 +// 返回: int 引用类别 +func packageReference(root, parent, name, attribute string) int { + if attribute != "" { + switch attribute { + case "BaseLoc": + switch { + case root == "OFD" && parent == "Versions" && name == "Version", + root == "Document" && parent == "Pages" && name == "Page", + root == "Document" && parent == "CommonData" && name == "TemplatePage", + root == "Signatures" && parent == "Signatures" && name == "Signature", + root == "Res" && parent == "CompositeGraphicUnits" && name == "CompositeGraphicUnit": + return packageReferenceXML + } + case "FileLoc": + if root == "Annotations" && name == "PageAnnot" { + return packageReferenceXML + } + case "Link": + if root == "Res" && name == "DrawParam" { + return packageReferenceXML + } + case "Profile": + if root == "Res" && name == "ColorSpace" { + return packageReferenceData + } + } + return packageReferenceNone + } + switch name { + case "DocRoot": + if root == "OFD" && parent == "DocBody" || root == "DocVersion" && parent == "DocVersion" { + return packageReferenceXML + } + case "Signatures": + if root == "OFD" && parent == "DocBody" { + return packageReferenceXML + } + case "Cover": + if root == "OFD" && parent == "DocInfo" { + return packageReferenceData + } + case "PublicRes", "DocumentRes": + if root == "Document" && parent == "CommonData" { + return packageReferenceXML + } + case "PageRes": + if root == "Page" && parent == "Page" { + return packageReferenceXML + } + case "Annotations", "Attachments", "CustomTags", "Extensions": + if root == "Document" && parent == "Document" { + return packageReferenceXML + } + case "FileLoc": + switch { + case root == "Annotations" && parent == "Page": + return packageReferenceXML + case root == "Attachments" && parent == "Attachment", root == "CustomTags" && parent == "CustomTag": + return packageReferenceData + } + case "File": + if root == "DocVersion" && parent == "FileList" { + return packageReferenceData + } + case "FontFile": + if root == "Res" && parent == "Font" { + return packageReferenceData + } + case "MediaFile": + if root == "Res" && parent == "MultiMedia" { + return packageReferenceData + } + case "Profile": + if root == "Res" && parent == "ColorSpace" { + return packageReferenceData + } + case "SignedValue": + if root == "Signature" && parent == "Signature" { + return packageReferenceData + } + case "BaseLoc": + if root == "Signature" && parent == "Seal" { + return packageReferenceData + } + case "SchemaLoc": + if root == "CustomTags" && parent == "CustomTag" { + return packageReferenceData + } + case "ExtendData": + if parent == "Extension" { + return packageReferenceData + } + } + return packageReferenceNone +} + +// documentFiles 遍历独立文档及版本依赖,只解析结构文件,不读取附件等原始数据 // 入参: ctx 取消上下文, index 文档索引 // 返回: map[string]bool 包内文件路径, error 读取或取消错误 func (r *Reader) documentFiles(ctx context.Context, index int) (map[string]bool, error) { + files, _, err := r.documentDependencies(ctx, index) + return files, err +} + +// documentDependencies 区分文档全部依赖与需要解析的结构文件,避免重复读取原始数据 +// 入参: ctx 取消上下文, index 文档索引 +// 返回: map[string]bool 全部依赖, map[string]bool 结构文件, error 读取或取消错误 +func (r *Reader) documentDependencies(ctx context.Context, index int) (map[string]bool, map[string]bool, error) { if err := ctx.Err(); err != nil { - return nil, err + return nil, nil, err } if index < 0 || index >= r.DocumentCount() { - return nil, fmt.Errorf("document index out of range: %d", index) + return nil, nil, fmt.Errorf("document index out of range: %d", index) + } + var queue []string + seen := make(map[string]bool) + queued := make(map[string]bool) + add := func(name string, kind int) { + seen[name] = true + if kind == packageReferenceXML && !queued[name] { + queued[name] = true + queue = append(queue, name) + } } - queue := []string{cleanPackagePath(r.OFD.DocBody[index].DocRoot)} data, err := r.readFile("OFD.xml") if err != nil { - return nil, err + return nil, nil, err } root, err := parseEditorXML(data) if err != nil { - return nil, err + return nil, nil, err } body := root.childAt("DocBody", index) if body == nil { - return nil, fmt.Errorf("document body not found: %d", index) + return nil, nil, fmt.Errorf("document body not found: %d", index) } var collect func(*editorXML) collect = func(node *editorXML) { - if node.name.Local == "Cover" || node.name.Local == "Signatures" || node.name.Local == "DocRoot" { + if !node.matchesOFD(node.name.Local) { + return + } + parent := node.parent.name.Local + if kind := packageReference("OFD", parent, node.name.Local, ""); kind != packageReferenceNone { if value := strings.TrimSpace(editorImportText(data, node)); value != "" { - queue = append(queue, resolveResourcePath("OFD.xml", "", value)) + add(resolveResourcePath("OFD.xml", "", value), kind) } } - if node.name.Local == "Version" && node.attr("BaseLoc") != "" { - queue = append(queue, resolveResourcePath("OFD.xml", "", node.attr("BaseLoc"))) + for _, attr := range node.attrs { + if attr.Name.Space != "" || strings.TrimSpace(attr.Value) == "" { + continue + } + if kind := packageReference("OFD", parent, node.name.Local, attr.Name.Local); kind != packageReferenceNone { + add(resolveResourcePath("OFD.xml", "", attr.Value), kind) + } } for _, child := range node.children { collect(child) } } collect(body) - seen := make(map[string]bool) for len(queue) > 0 { if err := ctx.Err(); err != nil { - return nil, err + return nil, nil, err } actual := queue[0] queue = queue[1:] - if seen[actual] { - continue - } - seen[actual] = true if !r.fileNames[actual] { continue } - input, err := r.openFile(actual) + if err := r.documentFileReferences(ctx, actual, add); err != nil { + return nil, nil, err + } + } + return seen, queued, ctx.Err() +} + +// documentFileReferences 流式扫描结构文件,支持同一作用域内的未知命名空间 +// 入参: ctx 取消上下文, name 文件路径, add 接收引用路径及类别 +// 返回: error 读取或结构错误 +func (r *Reader) documentFileReferences(ctx context.Context, name string, add func(string, int)) error { + input, err := r.openFile(name) + if err != nil { + return err + } + defer input.Close() + decoder := xml.NewDecoder(imageInput{ReadCloser: input, context: ctx}) + var stack []xml.Name + root, base := "", "" + var text strings.Builder + for { + if err := ctx.Err(); err != nil { + return err + } + token, err := decoder.Token() + if err == io.EOF { + if root == "" { + return fmt.Errorf("empty document dependency: %s", name) + } + return nil + } if err != nil { - return nil, err + return fmt.Errorf("document dependency %s: %w", name, err) + } + switch node := token.(type) { + case xml.StartElement: + parent := "" + if len(stack) == 0 { + if root != "" { + return fmt.Errorf("multiple XML roots in %s", name) + } + root = node.Name.Local + } else { + owner := stack[len(stack)-1] + parent = owner.Local + if classifyOFDNamespace(node.Name.Space) == ofdXMLUnknown && node.Name.Space != owner.Space { + return fmt.Errorf("cannot determine document dependencies in %s", name) + } + } + if parent == "Extension" && node.Name.Local == "Data" { + if err := decoder.Skip(); err != nil { + return err + } + continue + } + for _, attr := range node.Attr { + if attr.Name.Space != "" || strings.TrimSpace(attr.Value) == "" { + continue + } + if len(stack) == 0 && root == "Res" && attr.Name.Local == "BaseLoc" { + base = attr.Value + } else if kind := packageReference(root, parent, node.Name.Local, attr.Name.Local); kind != packageReferenceNone { + add(resolveResourcePath(name, base, attr.Value), kind) + } + } + stack = append(stack, node.Name) + text.Reset() + case xml.CharData: + if len(stack) >= 2 && packageReference(root, stack[len(stack)-2].Local, stack[len(stack)-1].Local, "") != packageReferenceNone { + text.Write(node) + } + case xml.EndElement: + if len(stack) >= 2 { + kind := packageReference(root, stack[len(stack)-2].Local, node.Name.Local, "") + if value := strings.TrimSpace(text.String()); kind != packageReferenceNone && value != "" { + add(resolveResourcePath(name, base, value), kind) + } + } + stack = stack[:len(stack)-1] + text.Reset() } - buffer := bufio.NewReader(imageInput{ReadCloser: input, context: ctx}) - if prefix, _ := buffer.Peek(3); bytes.Equal(prefix, []byte{0xef, 0xbb, 0xbf}) { - buffer.Discard(3) - } - header, _ := buffer.Peek(128) - for len(header) > 0 && len(bytes.TrimSpace(header)) == 0 { - buffer.Discard(len(header)) - header, _ = buffer.Peek(128) - } - if !bytes.HasPrefix(bytes.TrimSpace(header), []byte("<")) { - input.Close() - continue - } - decoder := xml.NewDecoder(buffer) - base, resource, depth := "", false, 0 - var text strings.Builder - for { - token, err := decoder.Token() - if err == io.EOF { - break - } - if err != nil { - input.Close() - return nil, err - } - switch node := token.(type) { - case xml.StartElement: - if depth == 0 && classifyOFDNamespace(node.Name.Space) == ofdXMLUnknown { - input.Close() - goto nextFile - } - if classifyOFDNamespace(node.Name.Space) == ofdXMLUnknown { - input.Close() - return nil, fmt.Errorf("cannot determine document dependencies in %s", actual) - } - if depth == 0 { - resource = node.Name.Local == "Res" - } - for _, attr := range node.Attr { - if resource && depth == 0 && attr.Name.Local == "BaseLoc" { - base = attr.Value - } else if attr.Name.Local == "BaseLoc" || attr.Name.Local == "FileLoc" || node.Name.Local == "DrawParam" && attr.Name.Local == "Link" || node.Name.Local == "ColorSpace" && attr.Name.Local == "Profile" { - queue = append(queue, resolveResourcePath(actual, base, attr.Value)) - } - } - depth++ - text.Reset() - case xml.CharData: - text.Write(node) - case xml.EndElement: - switch node.Name.Local { - case "DocRoot", "Signatures", "Cover", "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "FileLoc", "Profile", "SignedValue", "SchemaLoc", "ExtendData", "File", "FontFile", "MediaFile", "CustomTags", "Extensions": - if value := strings.TrimSpace(text.String()); value != "" { - queue = append(queue, resolveResourcePath(actual, base, value)) - } - } - depth-- - text.Reset() - } - } - input.Close() - nextFile: } - return seen, ctx.Err() } diff --git a/ofdgo_version_edit.go b/ofdgo_version_edit.go index bfc0446..7126cf4 100644 --- a/ofdgo_version_edit.go +++ b/ofdgo_version_edit.go @@ -83,34 +83,34 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s if node != root && !node.matchesOFD(node.name.Local) { return } - add := func(attribute, value string, structure bool) { + parent := "" + if node.parent != nil { + parent = node.parent.name.Local + } + if parent == "Extension" && node.name.Local == "Data" { + return + } + add := func(attribute, value string, kind int) { if strings.TrimSpace(value) == "" { return } target := resolveResourcePath(name, base, value) file.links = append(file.links, versionEditLink{node: node, attribute: attribute, target: target}) graph.used[target] = true - if structure { + if kind == packageReferenceXML { queue = append(queue, target) } } - switch node.name.Local { - case "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "Signatures", "CustomTags", "Extensions": - if len(node.children) == 0 { - add("", editorImportText(data, node), true) + if kind := packageReference(root.name.Local, parent, node.name.Local, ""); kind != packageReferenceNone { + add("", editorImportText(data, node), kind) + } + for _, attr := range node.attrs { + if attr.Name.Space != "" { + continue + } + if kind := packageReference(root.name.Local, parent, node.name.Local, attr.Name.Local); kind != packageReferenceNone { + add(attr.Name.Local, attr.Value, kind) } - case "Page", "TemplatePage", "CompositeGraphicUnit", "Signature": - add("BaseLoc", node.attr("BaseLoc"), true) - case "PageAnnot": - add("FileLoc", node.attr("FileLoc"), true) - case "DrawParam": - add("Link", node.attr("Link"), true) - case "ColorSpace": - add("Profile", node.attr("Profile"), false) - case "FontFile", "MediaFile", "Profile", "SignedValue", "SchemaLoc", "ExtendData": - add("", editorImportText(data, node), false) - case "FileLoc": - add("", editorImportText(data, node), root.name.Local == "Annotations") } for _, child := range node.children { visit(child)