mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
581 lines
19 KiB
Go
581 lines
19 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"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"maps"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// AnnotationSelection 保存当前编辑器内可重复粘贴的注解原文及资源上下文
|
|
// 来源修改或删除不影响快照,不用于跨文档传输
|
|
type AnnotationSelection struct {
|
|
editor *Editor
|
|
document editorDocumentIdentity
|
|
data [][]byte
|
|
resources []string
|
|
states map[string]map[string]editorCompositeState
|
|
}
|
|
|
|
// Annotations 获取指定页面的注解信息,包含不可见注解,返回值与编辑器相互独立
|
|
// 入参: index 页面索引
|
|
// 返回: []AnnotationInfo 注解信息, error 错误信息
|
|
func (e *Editor) Annotations(index int) ([]AnnotationInfo, error) {
|
|
if index < 0 || index >= len(e.pages) {
|
|
return nil, fmt.Errorf("page index %d out of range", index)
|
|
}
|
|
if e.source == nil {
|
|
return nil, nil
|
|
}
|
|
if _, err := e.source.reader.Doc(); err != nil {
|
|
return nil, err
|
|
}
|
|
return e.source.reader.annotationInfos(index, e.pages[index].ID), nil
|
|
}
|
|
|
|
// UpdateAnnotation 修改注解说明和作者,保留外观、动作、参数及扩展字段
|
|
// 有效修改更新LastModDate并计入一次撤销记录,不受原文件ReadOnly声明限制
|
|
// 入参: index 页面索引, id 注解标识, remark 说明, creator 作者
|
|
// 返回: error 错误信息
|
|
func (e *Editor) UpdateAnnotation(index int, id, remark, creator string) error {
|
|
return e.editAnnotations(index, []string{id}, func(data []byte, node *editorXML) ([]byte, error) {
|
|
standalone, err := editorXMLStandalone(data[node.start:node.end], node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var annotation Annotation
|
|
if err := xml.Unmarshal(standalone, &annotation); err != nil {
|
|
return nil, err
|
|
}
|
|
if annotation.Remark == remark && annotation.Creator == creator {
|
|
return data[node.start:node.end], nil
|
|
}
|
|
data = standalone
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if annotation.Remark != remark {
|
|
var escaped bytes.Buffer
|
|
if err := xml.EscapeText(&escaped, []byte(remark)); err != nil {
|
|
return nil, err
|
|
}
|
|
if child := root.child("Remark"); child != nil {
|
|
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, child, escaped.Bytes())})
|
|
} else if root.open == root.end {
|
|
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, root, root.textXML("Remark", remark))})
|
|
} else {
|
|
data = editorPatchXML(data, []editorXMLPatch{{root.open, root.open, root.textXML("Remark", remark)}})
|
|
}
|
|
}
|
|
root, err = parseEditorXML(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if annotation.Creator != creator {
|
|
data, err = editorXMLAttribute(data, root, "Creator", creator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return editorAnnotationDate(data)
|
|
})
|
|
}
|
|
|
|
// MoveAnnotations 平移同页注解,保留内部图元、动作坐标及原始尺寸,一次操作计入一条撤销记录
|
|
// 入参: index 页面索引, ids 不重复的注解标识, dx 横向毫米偏移, dy 纵向毫米偏移
|
|
// 返回: error 错误信息
|
|
func (e *Editor) MoveAnnotations(index int, ids []string, dx, dy float64) error {
|
|
if !finite(dx) || !finite(dy) {
|
|
return fmt.Errorf("annotation offset must be finite")
|
|
}
|
|
return e.editAnnotations(index, ids, func(data []byte, node *editorXML) ([]byte, error) {
|
|
appearance := node.child("Appearance")
|
|
if appearance == nil {
|
|
return nil, fmt.Errorf("annotation %q has no appearance", node.attr("ID"))
|
|
}
|
|
box, err := ParseBox(appearance.attr("Boundary"))
|
|
if err != nil || !finite(box.X+dx) || !finite(box.Y+dy) || !finite(box.W) || !finite(box.H) || box.W < 0 || box.H < 0 {
|
|
return nil, fmt.Errorf("annotation %q has invalid boundary", node.attr("ID"))
|
|
}
|
|
if dx == 0 && dy == 0 {
|
|
return data[node.start:node.end], nil
|
|
}
|
|
data, err = editorXMLStandalone(data[node.start:node.end], node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appearance = root.child("Appearance")
|
|
boundary := fmt.Sprintf("%s %s %s %s", ofdNumber(box.X+dx), ofdNumber(box.Y+dy), ofdNumber(box.W), ofdNumber(box.H))
|
|
updated, err := editorXMLAttribute(data, appearance, "Boundary", boundary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return editorAnnotationDate(editorPatchXML(data, []editorXMLPatch{{appearance.start, appearance.end, updated}}))
|
|
})
|
|
}
|
|
|
|
// DeleteAnnotations 删除同页注解并清理空的页面注解引用,一次操作计入一条撤销记录
|
|
// 入参: index 页面索引, ids 不重复的注解标识
|
|
// 返回: error 错误信息
|
|
func (e *Editor) DeleteAnnotations(index int, ids []string) error {
|
|
return e.editAnnotations(index, ids, func([]byte, *editorXML) ([]byte, error) { return nil, nil })
|
|
}
|
|
|
|
// editorAnnotationDate 更新注解修改日期,采用标准xs:date格式
|
|
// 入参: data 注解XML片段
|
|
// 返回: []byte 新片段, error 错误信息
|
|
func editorAnnotationDate(data []byte) ([]byte, error) {
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return editorXMLAttribute(data, root, "LastModDate", time.Now().Format("2006-01-02"))
|
|
}
|
|
|
|
// editAnnotations 原子修改页面注解原文,按需分离共用文件并保留输入包与历史快照
|
|
// 入参: index 页面索引, ids 注解标识, edit 单个注解修改函数
|
|
// 返回: error 错误信息
|
|
func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *editorXML) ([]byte, error)) error {
|
|
if index < 0 || index >= len(e.pages) {
|
|
return fmt.Errorf("page index %d out of range", index)
|
|
}
|
|
wanted := make(map[string]bool, len(ids))
|
|
for _, id := range ids {
|
|
key := editorResourceID(id)
|
|
if key == "" || wanted[key] {
|
|
return fmt.Errorf("missing or duplicate annotation ID %q", id)
|
|
}
|
|
wanted[key] = true
|
|
}
|
|
if len(wanted) == 0 {
|
|
return nil
|
|
}
|
|
if e.source == nil || e.source.document.Annotations == "" {
|
|
return fmt.Errorf("page has no annotations")
|
|
}
|
|
reader := e.source.reader
|
|
name, err := editorPageLocation(reader, nil, "", "/"+reader.ResPath(e.source.document.Annotations))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err := reader.readFile(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parts := make(map[string][]byte)
|
|
var patches []editorXMLPatch
|
|
found := make(map[string]bool, len(ids))
|
|
remaining := 0
|
|
pageID := editorResourceID(e.pages[index].ID)
|
|
for _, ref := range root.children {
|
|
if !ref.matchesOFD("Page") {
|
|
continue
|
|
}
|
|
remaining++
|
|
if pageID == "" || editorResourceID(ref.attr("PageID")) != pageID {
|
|
continue
|
|
}
|
|
loc := ref.child("FileLoc")
|
|
if loc == nil {
|
|
return fmt.Errorf("annotation page has no FileLoc")
|
|
}
|
|
file, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, loc)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content, err := reader.readFile(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
page, err := parseEditorXML(content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var changes []editorXMLPatch
|
|
count := 0
|
|
for _, node := range page.children {
|
|
if !node.matchesOFD("Annot") {
|
|
continue
|
|
}
|
|
count++
|
|
id := editorResourceID(node.attr("ID"))
|
|
if !wanted[id] {
|
|
continue
|
|
}
|
|
if found[id] {
|
|
return fmt.Errorf("ambiguous annotation ID %q", id)
|
|
}
|
|
found[id] = true
|
|
value, err := edit(content, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if value == nil {
|
|
count--
|
|
}
|
|
if !bytes.Equal(value, content[node.start:node.end]) {
|
|
changes = append(changes, editorXMLPatch{node.start, node.end, value})
|
|
}
|
|
}
|
|
if len(changes) == 0 {
|
|
continue
|
|
}
|
|
sharedFile := false
|
|
for _, other := range root.children {
|
|
if other == ref || !other.matchesOFD("Page") || other.child("FileLoc") == nil {
|
|
continue
|
|
}
|
|
shared, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, other.child("FileLoc"))))
|
|
if err == nil && shared == file {
|
|
sharedFile = true
|
|
break
|
|
}
|
|
}
|
|
if count == 0 {
|
|
patches = append(patches, editorXMLPatch{ref.start, ref.end, nil})
|
|
remaining--
|
|
if !sharedFile {
|
|
parts[file] = editorPatchXML(content, changes)
|
|
}
|
|
continue
|
|
}
|
|
if sharedFile {
|
|
file = packageAvailableName(reader, parts, e.packageName(path.Join("Annotations", "Page_"+e.pages[index].ID+".xml")))
|
|
var escaped bytes.Buffer
|
|
_ = xml.EscapeText(&escaped, []byte("/"+file))
|
|
patches = append(patches, editorXMLContent(data, loc, escaped.Bytes()))
|
|
}
|
|
parts[file] = editorPatchXML(content, changes)
|
|
}
|
|
for _, id := range ids {
|
|
if !found[editorResourceID(id)] {
|
|
return fmt.Errorf("annotation %q not found", id)
|
|
}
|
|
}
|
|
if len(patches) != 0 {
|
|
parts[name] = editorPatchXML(data, patches)
|
|
if remaining == 0 {
|
|
docName, err := editorPageLocation(reader, nil, "", "/"+reader.documentRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
docData, err := reader.readFile(docName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
doc, err := parseEditorXML(docData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parts[docName], err = editorXMLSetText(docData, doc, [][2]string{{"Annotations", ""}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if len(parts) == 0 {
|
|
return nil
|
|
}
|
|
base, err := e.annotationStructureSource(index, wanted)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return e.commitAnnotationParts(base, parts)
|
|
}
|
|
|
|
// CaptureAnnotations 捕获注解选区,保留未知字段、外观及动作
|
|
// 入参: page 页面索引, ids 不重复的注解标识
|
|
// 返回: *AnnotationSelection 独立快照, error 错误信息
|
|
func (e *Editor) CaptureAnnotations(page int, ids []string) (*AnnotationSelection, error) {
|
|
content, err := e.page(page)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
selection := &AnnotationSelection{editor: e, document: e.documentIdentity(), states: make(map[string]map[string]editorCompositeState)}
|
|
seen := make(map[string]bool)
|
|
for _, id := range ids {
|
|
key := editorResourceID(id)
|
|
if seen[key] {
|
|
return nil, fmt.Errorf("duplicate annotation ID %q", id)
|
|
}
|
|
seen[key] = true
|
|
data, err := e.annotationXML(page, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !editorXMLCopyable(root) {
|
|
return nil, fmt.Errorf("annotation contains unsupported identifiers")
|
|
}
|
|
selection.data = append(selection.data, data)
|
|
selection.states[key] = maps.Clone(e.source.annotationStates[e.pages[page].ID+"/"+key])
|
|
}
|
|
if e.originalPage(page) {
|
|
origin := e.source.pages[e.pages[page].ID]
|
|
for _, name := range content.PageRes {
|
|
selection.resources = append(selection.resources, resolveResourcePath(e.source.reader.ResPath(origin.ref.BaseLoc), "", name))
|
|
}
|
|
}
|
|
return selection, nil
|
|
}
|
|
|
|
// PasteAnnotations 将注解快照粘贴到指定页并平移,保留动作目标和原有外观
|
|
// 全部标识重新分配,页级资源提升为文档资源,一次操作计入一条撤销记录
|
|
// 入参: page 目标页面索引, selection 当前编辑器快照, dx 横向位移,单位为毫米, dy 纵向位移,单位为毫米
|
|
// 返回: []string 新注解标识, error 错误信息
|
|
func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx, dy float64) ([]string, error) {
|
|
if selection == nil || selection.editor != e || !e.documentIdentity().matches(selection.document) || !finite(dx) || !finite(dy) {
|
|
return nil, fmt.Errorf("invalid annotation selection or offset")
|
|
}
|
|
var result []string
|
|
err := e.Transaction(func(edit *Editor) error {
|
|
if page < 0 || page >= len(edit.pages) {
|
|
return fmt.Errorf("page index %d out of range", page)
|
|
}
|
|
if len(selection.data) == 0 {
|
|
return nil
|
|
}
|
|
if err := edit.prepareSourceIDs(); err != nil {
|
|
return err
|
|
}
|
|
var content []byte
|
|
var states map[string]map[string]editorCompositeState
|
|
if edit.source != nil {
|
|
states = maps.Clone(edit.source.annotationStates)
|
|
}
|
|
if states == nil {
|
|
states = make(map[string]map[string]editorCompositeState)
|
|
}
|
|
for _, data := range selection.data {
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ids := make(map[string]string)
|
|
if err := edit.collectCompositeIDs(root, ids); err != nil {
|
|
return err
|
|
}
|
|
id := ids[editorResourceID(root.attr("ID"))]
|
|
states[edit.pages[page].ID+"/"+id] = remapCompositeStates(selection.states[editorResourceID(root.attr("ID"))], ids)
|
|
data, err = editorXMLRemapIDs(data, root, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err = parseEditorXML(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
appearance := root.child("Appearance")
|
|
if appearance == nil {
|
|
return fmt.Errorf("annotation has no appearance")
|
|
}
|
|
box, err := ParseBox(appearance.attr("Boundary"))
|
|
if err != nil || !finite(box.X+dx) || !finite(box.Y+dy) {
|
|
return fmt.Errorf("invalid annotation boundary")
|
|
}
|
|
box.X, box.Y = box.X+dx, box.Y+dy
|
|
fragment, err := editorXMLAttribute(data, appearance, "Boundary", editorBoxString(box))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err = editorAnnotationDate(editorPatchXML(data, []editorXMLPatch{{appearance.start, appearance.end, fragment}}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
|
result = append(result, id)
|
|
}
|
|
if err := edit.appendAnnotations(page, content, false); err != nil {
|
|
return err
|
|
}
|
|
edit.source.annotationStates = states
|
|
if len(selection.resources) != 0 {
|
|
reader := edit.source.reader
|
|
name := reader.documentRoot
|
|
data, err := reader.readFile(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
common := root.child("CommonData")
|
|
registered := make(map[string]bool)
|
|
for _, name := range edit.source.document.CommonData.PublicRes {
|
|
registered[reader.ResPath(name)] = true
|
|
}
|
|
for _, name := range edit.source.document.CommonData.DocumentRes {
|
|
registered[reader.ResPath(name)] = true
|
|
}
|
|
var added []byte
|
|
for _, name := range selection.resources {
|
|
if !registered[name] {
|
|
added = append(added, common.textXML("DocumentRes", "/"+name)...)
|
|
registered[name] = true
|
|
}
|
|
}
|
|
if len(added) != 0 {
|
|
position := common.close
|
|
for _, node := range common.children {
|
|
if node.name.Local == "TemplatePage" || node.name.Local == "DefaultCS" {
|
|
position = node.start
|
|
break
|
|
}
|
|
}
|
|
return edit.commitAnnotationParts(edit.source, map[string][]byte{name: editorPatchXML(data, []editorXMLPatch{{position, position, added}})})
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// TransformAnnotations 对注解选区施加页面仿射变换,保持内部图元与外观边界同步
|
|
// 入参: page 页面索引, ids 注解标识, matrix 可逆页面变换
|
|
// 返回: error 错误信息
|
|
func (e *Editor) TransformAnnotations(page int, ids []string, matrix Matrix) error {
|
|
if _, ok := matrix.Invert(); !ok {
|
|
return fmt.Errorf("annotation transform must be finite and invertible")
|
|
}
|
|
return e.Transaction(func(edit *Editor) error {
|
|
seen := make(map[string]bool)
|
|
for _, id := range ids {
|
|
if seen[id] {
|
|
return fmt.Errorf("duplicate annotation ID %q", id)
|
|
}
|
|
seen[id] = true
|
|
annotation, err := edit.Annotation(page, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if matrix == IdentityMatrix {
|
|
continue
|
|
}
|
|
box, err := ParseBox(annotation.Appearance.Boundary)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bounds := matrix.TransformBox(box)
|
|
if !finite(bounds.X) || !finite(bounds.Y) || !finite(bounds.W) || !finite(bounds.H) || bounds.W < 0 || bounds.H < 0 {
|
|
return fmt.Errorf("annotation transform exceeds finite range")
|
|
}
|
|
path := ObjectPath{Annotation: id}
|
|
reader, _, _, members, err := edit.compositeScope(page, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader.Close()
|
|
indexes := make([]int, len(members))
|
|
for i := range indexes {
|
|
indexes[i] = i
|
|
}
|
|
local := TranslationMatrix(box.X-bounds.X, box.Y-bounds.Y).Multiply(matrix)
|
|
if err := edit.changeCompositeObjects(page, path, indexes, func(Box) Matrix { return local }); err != nil {
|
|
return err
|
|
}
|
|
if err := edit.editAnnotations(page, []string{id}, func(data []byte, node *editorXML) ([]byte, error) {
|
|
fragment, err := editorXMLStandalone(data[node.start:node.end], node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root, err := parseEditorXML(fragment)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appearance := root.child("Appearance")
|
|
updated, err := editorXMLAttribute(fragment, appearance, "Boundary", editorBoxString(bounds))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return editorAnnotationDate(editorPatchXML(fragment, []editorXMLPatch{{appearance.start, appearance.end, updated}}))
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ReplaceAnnotationText 修改注解内部匹配的普通文字,保留各成员位置、旋转及未修改属性
|
|
// 入参: page 页面索引, id 注解标识, old 原文字, value 新文字, style 文字样式增量
|
|
// 返回: int 修改数量, error 错误信息
|
|
func (e *Editor) ReplaceAnnotationText(page int, id, old, value string, style TextStyle) (int, error) {
|
|
count := 0
|
|
err := e.Transaction(func(edit *Editor) error {
|
|
return edit.editCompositeScope(page, ObjectPath{Annotation: id}, func(renderer *Renderer, _ *editorCompositeNode, nodes []*editorCompositeNode) error {
|
|
visiting := make(map[string]bool)
|
|
var replace func([]*editorCompositeNode) error
|
|
replace = func(nodes []*editorCompositeNode) error {
|
|
members, err := edit.measureCompositeMembers(renderer, nodes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, member := range members {
|
|
if member.Object.Type == "TextObject" && member.Object.TextObject.Text() == old {
|
|
if err := edit.updateCompositeText(nodes[i], member, &value, style, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
count++
|
|
} else if member.Object.Type == "CompositeObject" || member.Object.Type == "CompositeGraphicUnit" {
|
|
resource := member.Object.CompositeGraphicUnit.ResourceID
|
|
if id := editorResourceID(resource); id != "" {
|
|
resource = id
|
|
}
|
|
if resource != "" && visiting[resource] {
|
|
return fmt.Errorf("cyclic composite resource %q", resource)
|
|
}
|
|
children, err := edit.compositeMembers(nodes[i], renderer.Reader, renderer, visiting)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
visiting[resource] = true
|
|
if err := replace(children); err != nil {
|
|
return err
|
|
}
|
|
delete(visiting, resource)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return replace(nodes)
|
|
})
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return count, nil
|
|
}
|