Files
ofdgo/ofdgo_editor_pages.go
2026-10-19 08:43:50 +08:00

275 lines
8.7 KiB
Go

// 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 在输出快照中清理已删除页面和附件的引用,不修改历史快照或其他文档
// 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
// 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据
// 入参: ctx 取消上下文, parts 输出覆盖条目, generated 本次自产页面及引用,可为nil
// 返回: error 读取、解析或取消错误
func (e *Editor) pruneDocumentReferences(ctx context.Context, parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
if err := ctx.Err(); err != nil {
return err
}
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
}
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(), ctx: 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)), ctx: 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)
}