// 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" "encoding/xml" "fmt" "io" "maps" "strings" ) // pageReferenceNode 记录跳转引用扫描中的父节点及删除范围 type pageReferenceNode struct { name string start, patch int children, removed int standard, drop bool } // pruneDocumentReferences 在输出快照中清理已删除页面和附件的引用,不修改历史快照或其他文档 // 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据 // 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据 // 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil // 返回: error 读取、解析或取消错误 func (e *Editor) pruneDocumentReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error { removed := maps.Clone(e.removedPages) if removed == nil { removed = make(map[string]bool) } for id := range e.source.pages { removed[editorResourceID(id)] = true } for id := range e.source.annotationPages { removed[editorResourceID(id)] = true } for _, page := range e.pages { delete(removed, editorResourceID(page.ID)) } delete(removed, "") if len(removed) == 0 && len(e.source.removedAttachments) == 0 { return nil } ctx := context.Background() if e.output != nil { ctx = e.output.ctx } reader := e.source.reader bookmarks := make(map[string]bool) for _, bookmark := range e.source.document.Bookmarks.Bookmark { if removed[editorResourceID(bookmark.Dest.PageID)] { bookmarks[bookmark.Name] = true } } queue := []string{reader.documentRoot} if name := e.source.document.Signatures; len(removed) != 0 && name != "" && !reader.OFD.DocBody[reader.documentIndex].versioned { queue = append(queue, reader.ResPath(name)) } seen := make(map[string]bool) for len(queue) > 0 { if err := ctx.Err(); err != nil { return err } name := queue[0] queue = queue[1:] if seen[name] { continue } seen[name] = true data, ok := parts[name] if !ok { var err error data, err = reader.readFileViewContext(ctx, name) if err != nil { return err } } else if known := generated[name]; data == nil && known.staged != nil { input := imageInput{ReadCloser: known.staged.open(), context: ctx} var err error data, err = io.ReadAll(input) input.Close() if err != nil { return err } } root, err := parseEditorXMLContext(ctx, data) if err != nil { return err } var patches []editorXMLPatch var walk func(*editorXML) error walk = func(node *editorXML) error { if err := ctx.Err(); err != nil { return err } if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown { return nil } remove := false switch node.name.Local { case "Action": if target := node.child("Goto"); target != nil { if dest := target.child("Dest"); dest != nil { remove = removed[editorResourceID(dest.attr("PageID"))] } else if bookmark := target.child("Bookmark"); bookmark != nil { remove = bookmarks[bookmark.attr("Name")] } } else if attachment := node.child("GotoA"); attachment != nil { remove = e.source.removedAttachments[attachment.attr("AttachID")] } case "Bookmark": if node.parent != nil && node.parent.name.Local == "Bookmarks" { remove = bookmarks[node.attr("Name")] } case "Page": if root.name.Local == "Annotations" { remove = removed[editorResourceID(node.attr("PageID"))] } case "Dest": remove = removed[editorResourceID(node.attr("PageID"))] case "StampAnnot": remove = removed[editorResourceID(node.attr("PageRef"))] case "Extension": remove = removed[editorResourceID(node.attr("RefId"))] } if remove { patches = append(patches, editorXMLPatch{start: node.start, end: node.end}) return nil } var location string switch node.name.Local { case "Page", "TemplatePage", "Signature": if node.parent != nil && (node.parent.name.Local == "Pages" || node.parent.name.Local == "CommonData" || node.parent.name.Local == "Signatures") { location = node.attr("BaseLoc") } case "PublicRes", "DocumentRes", "PageRes", "Annotations", "Extensions": if len(node.children) == 0 { location = strings.TrimSpace(editorImportText(data, node)) } case "FileLoc": if root.name.Local == "Annotations" { location = strings.TrimSpace(editorImportText(data, node)) } case "CustomTags", "ExtendData", "Data": return nil } if location != "" { resolved, err := editorPageLocation(reader, parts, name, location) if err != nil { return err } queue = append(queue, resolved) } start := len(patches) for _, child := range node.children { if err := walk(child); err != nil { return err } } if (node.name.Local == "Actions" || node.name.Local == "Bookmarks") && len(node.children) > 0 && len(patches)-start == len(node.children) { empty := true for i, child := range node.children { empty = empty && patches[start+i].start == child.start && patches[start+i].end == child.end } if empty { patches = append(patches[:start], editorXMLPatch{start: node.start, end: node.end}) } } return nil } if err := walk(root); err != nil { return err } if len(patches) != 0 { parts[name] = editorPatchXML(data, patches) delete(generated, name) } } return ctx.Err() } // pruneCreatedPageReferences 清理创作文档中被明确删除页面的直接跳转,不检查其他目标是否可达 // 入参: ctx 取消上下文, data 创作XML, removed 已删除页面标识 // 返回: []byte 更新后的XML, error 解析或取消错误 func pruneCreatedPageReferences(ctx context.Context, data []byte, removed map[string]bool) ([]byte, error) { d := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) var stack []pageReferenceNode var patches []editorXMLPatch root := false for { if err := ctx.Err(); err != nil { return nil, err } start := int(d.InputOffset()) token, err := d.Token() if err == io.EOF { break } if err != nil { return nil, err } switch token := token.(type) { case xml.StartElement: node := pageReferenceNode{name: token.Name.Local, start: start, patch: len(patches), standard: token.Name.Space == ofdNamespace2016} if len(stack) == 0 { if root { return nil, fmt.Errorf("multiple XML roots") } root = true } else { parent := &stack[len(stack)-1] parent.children++ node.standard = node.standard && parent.standard } if node.standard && node.name == "Dest" && len(stack) >= 2 && stack[len(stack)-1].name == "Goto" && stack[len(stack)-2].name == "Action" { for _, attr := range token.Attr { if attr.Name.Space == "" && attr.Name.Local == "PageID" && removed[editorResourceID(attr.Value)] { stack[len(stack)-2].drop = true } } } stack = append(stack, node) case xml.EndElement: node := stack[len(stack)-1] stack = stack[:len(stack)-1] if node.drop || node.standard && node.name == "Actions" && node.removed > 0 && node.children == node.removed { patches = append(patches[:node.patch], editorXMLPatch{start: node.start, end: int(d.InputOffset())}) if len(stack) > 0 { stack[len(stack)-1].removed++ } } } } if !root { return nil, fmt.Errorf("empty XML document") } if len(patches) > 0 { data = editorPatchXML(data, patches) } if err := ctx.Err(); err != nil { return nil, err } return data, nil } // editorPageLocation 按所在XML解析引用,绝对路径以包根目录为基准 // 入参: reader 原包, parts 覆盖条目, name 所在XML, value 路径 // 返回: string 包内路径, error 路径不存在 func editorPageLocation(reader *Reader, parts map[string][]byte, name, value string) (string, error) { candidate := resolveResourcePath(name, "", value) if _, ok := parts[candidate]; ok { return candidate, nil } if _, ok := reader.files[candidate]; ok { return candidate, nil } if file, ok := reader.packageFile(candidate); ok { return cleanPackagePath(file.Name), nil } return "", fmt.Errorf("page reference %q in %s not found", value, name) }