mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
512 lines
17 KiB
Go
512 lines
17 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 (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"maps"
|
|
"path"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// editorDocumentIdentity 标记编辑状态所属的文档和版本,不持有页面或资源数据
|
|
type editorDocumentIdentity struct {
|
|
root string
|
|
id string
|
|
version string
|
|
fresh bool
|
|
single bool
|
|
fromNew bool
|
|
}
|
|
|
|
// DocumentCount 获取编辑包内的文档数量,新建编辑器包含一个当前文档
|
|
// 返回: int 文档数量
|
|
func (e *Editor) DocumentCount() int {
|
|
if e.source == nil {
|
|
return 1
|
|
}
|
|
return e.source.reader.DocumentCount()
|
|
}
|
|
|
|
// DocumentIndex 获取当前编辑文档索引,从0开始
|
|
// 返回: int 文档索引
|
|
func (e *Editor) DocumentIndex() int {
|
|
if e.source == nil {
|
|
return 0
|
|
}
|
|
return e.source.reader.DocumentIndex()
|
|
}
|
|
|
|
// DocumentInfo 获取指定文档信息的独立副本,不切换当前文档
|
|
// 入参: index 文档索引,从0开始
|
|
// 返回: DocInfo 文档信息, error 索引错误
|
|
func (e *Editor) DocumentInfo(index int) (DocInfo, error) {
|
|
if index < 0 || index >= e.DocumentCount() {
|
|
return DocInfo{}, fmt.Errorf("document index %d out of range", index)
|
|
}
|
|
if index == e.DocumentIndex() {
|
|
return cloneEditorData(e.Info), nil
|
|
}
|
|
info := cloneEditorData(e.source.reader.OFD.DocBody[index].DocInfo)
|
|
if info.CustomDatas != nil {
|
|
for i := range info.CustomDatas.CustomData {
|
|
info.CustomDatas.CustomData[i].sourceIndex = i + 1
|
|
}
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// SetDocumentInfo 修改指定文档信息,保留扩展内容,不切换当前文档,可通过Undo撤销
|
|
// 入参: index 文档索引,从0开始, info 文档信息
|
|
// 返回: error 修改错误
|
|
func (e *Editor) SetDocumentInfo(index int, info DocInfo) error {
|
|
return e.SetDocumentInfoContext(context.Background(), index, info)
|
|
}
|
|
|
|
// SetDocumentInfoContext 修改文档信息,取消或失败时保留元数据和撤销历史
|
|
// 入参: ctx 取消上下文, index 文档索引,从0开始, info 文档信息
|
|
// 返回: error 修改或取消错误
|
|
func (e *Editor) SetDocumentInfoContext(ctx context.Context, index int, info DocInfo) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
before, err := e.DocumentInfo(index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if reflect.DeepEqual(before, info) {
|
|
return ctx.Err()
|
|
}
|
|
if index == e.DocumentIndex() {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
e.SetInfo(info)
|
|
return nil
|
|
}
|
|
return e.editDocumentsContext(ctx, func(reader *Reader, data []byte, _ *editorXML) (int, error) {
|
|
updated, err := editorUpdateInfoXML(data, NewEditor().Info.DocID, index, before, info)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
reader.files["OFD.xml"] = updated
|
|
return reader.DocumentIndex(), nil
|
|
})
|
|
}
|
|
|
|
// AddDocument 在包内插入含一页的空白文档并选中,不修改输入包
|
|
// 当前文档的修改保留,新文档使用独立目录和文档标识;可通过Undo撤销
|
|
// 入参: title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量
|
|
// 返回: int 新文档索引, error 创建错误
|
|
func (e *Editor) AddDocument(title string, width, height float64, at int) (int, error) {
|
|
return e.AddDocumentContext(context.Background(), title, width, height, at)
|
|
}
|
|
|
|
// AddDocumentContext 插入并选中空白文档,取消或失败时保留原包和撤销历史
|
|
// 入参: ctx 取消上下文, title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量
|
|
// 返回: int 新文档索引, error 创建或取消错误
|
|
func (e *Editor) AddDocumentContext(ctx context.Context, title string, width, height float64, at int) (int, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
if at < 0 || at > e.DocumentCount() {
|
|
return 0, fmt.Errorf("document index %d out of range", at)
|
|
}
|
|
created := NewEditor()
|
|
created.Info.Title = title
|
|
if _, err := created.AddPage(width, height); err != nil {
|
|
return 0, err
|
|
}
|
|
err := e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
|
occupied := make(map[string]bool)
|
|
for name := range reader.fileNames {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
directory, _, _ := strings.Cut(name, "/")
|
|
occupied[directory] = true
|
|
}
|
|
directory := ""
|
|
for number := 0; ; number++ {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
candidate := fmt.Sprintf("Doc_%d", number)
|
|
if !occupied[candidate] {
|
|
directory = candidate
|
|
break
|
|
}
|
|
}
|
|
createdReader, err := created.Reader(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for name, content := range createdReader.files {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
if name == "OFD.xml" {
|
|
continue
|
|
}
|
|
reader.files[directory+strings.TrimPrefix(name, "Doc_0")] = content
|
|
}
|
|
body, err := encodeOFDXML(func(x *ofdXML) {
|
|
x.root("DocBody", nil)
|
|
x.start("DocInfo", nil)
|
|
for _, field := range [][2]string{{"DocID", created.Info.DocID}, {"Title", title}, {"CreationDate", created.Info.CreationDate}, {"Creator", ofdCreator}} {
|
|
if field[1] != "" {
|
|
x.text(field[0], field[1])
|
|
}
|
|
}
|
|
x.end("DocInfo")
|
|
x.text("DocRoot", directory+"/Document.xml")
|
|
x.end("DocBody")
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
position := root.close
|
|
if existing := root.childAt("DocBody", at); existing != nil {
|
|
position = existing.start
|
|
}
|
|
reader.files["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{{position, position, bytes.TrimPrefix(body, []byte(xml.Header))}})
|
|
return at, nil
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return at, nil
|
|
}
|
|
|
|
// RenameDocument 修改指定文档标题,保留其余元数据和内容,可通过Undo撤销
|
|
// 入参: index 文档索引,从0开始, title 标题,空值移除标题
|
|
// 返回: error 修改错误
|
|
func (e *Editor) RenameDocument(index int, title string) error {
|
|
return e.RenameDocumentContext(context.Background(), index, title)
|
|
}
|
|
|
|
// RenameDocumentContext 修改文档标题,取消或失败时保留其他元数据和撤销历史
|
|
// 入参: ctx 取消上下文, index 文档索引,从0开始, title 标题,空值移除标题
|
|
// 返回: error 修改或取消错误
|
|
func (e *Editor) RenameDocumentContext(ctx context.Context, index int, title string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
info, err := e.DocumentInfo(index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info.Title = title
|
|
return e.SetDocumentInfoContext(ctx, index, info)
|
|
}
|
|
|
|
// MoveDocument 调整包内文档顺序,当前文档随条目移动,不重写内容,可通过Undo撤销
|
|
// 入参: from 原文档索引, to 目标索引,均从0开始
|
|
// 返回: error 移动错误
|
|
func (e *Editor) MoveDocument(from, to int) error {
|
|
return e.MoveDocumentContext(context.Background(), from, to)
|
|
}
|
|
|
|
// MoveDocumentContext 调整文档顺序,取消或失败时保留当前文档和撤销历史
|
|
// 入参: ctx 取消上下文, from 原文档索引, to 目标索引,均从0开始
|
|
// 返回: error 移动或取消错误
|
|
func (e *Editor) MoveDocumentContext(ctx context.Context, from, to int) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if from < 0 || from >= e.DocumentCount() || to < 0 || to >= e.DocumentCount() {
|
|
return fmt.Errorf("document index out of range")
|
|
}
|
|
if from == to {
|
|
return nil
|
|
}
|
|
return e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
|
nodes := make([]*editorXML, 0, reader.DocumentCount())
|
|
bodies := make([][]byte, 0, reader.DocumentCount())
|
|
for _, body := range root.children {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
if body.matchesOFD("DocBody") {
|
|
nodes = append(nodes, body)
|
|
bodies = append(bodies, data[body.start:body.end])
|
|
}
|
|
}
|
|
if len(nodes) != reader.DocumentCount() {
|
|
return 0, fmt.Errorf("document entry count mismatch")
|
|
}
|
|
moveEditorItem(bodies, from, to)
|
|
var patches []editorXMLPatch
|
|
for index, content := range bodies {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
body := nodes[index]
|
|
patches = append(patches, editorXMLPatch{body.start, body.end, content})
|
|
}
|
|
reader.files["OFD.xml"] = editorPatchXML(data, patches)
|
|
selected := reader.DocumentIndex()
|
|
if selected == from {
|
|
selected = to
|
|
} else if from < selected && to >= selected {
|
|
selected--
|
|
} else if from > selected && to <= selected {
|
|
selected++
|
|
}
|
|
return selected, nil
|
|
})
|
|
}
|
|
|
|
// DeleteDocument 删除指定文档并清理独占文件,保留其他文档的共享资源,可通过Undo撤销
|
|
// 包内至少保留一个文档;删除当前文档时选中后一个,末尾则选中前一个
|
|
// 原签名保留,修改根索引或受保护文件可能使原签名失效
|
|
// 入参: index 文档索引,从0开始
|
|
// 返回: error 删除错误
|
|
func (e *Editor) DeleteDocument(index int) error {
|
|
return e.DeleteDocumentContext(context.Background(), index)
|
|
}
|
|
|
|
// DeleteDocumentContext 删除指定文档及其独占文件,取消或失败时保留当前状态和撤销历史
|
|
// 包内至少保留一个文档,其他文档及版本仍在使用的文件不删除
|
|
// 入参: ctx 取消上下文, index 文档索引,从0开始
|
|
// 返回: error 删除或取消错误
|
|
func (e *Editor) DeleteDocumentContext(ctx context.Context, index int) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if index < 0 || index >= e.DocumentCount() {
|
|
return fmt.Errorf("document index %d out of range", index)
|
|
}
|
|
if e.DocumentCount() == 1 {
|
|
return fmt.Errorf("package must contain at least one document")
|
|
}
|
|
return e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
|
removed, err := reader.documentFiles(ctx, index)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
directory := path.Dir(cleanPackagePath(reader.OFD.DocBody[index].DocRoot))
|
|
if directory != "." && !slices.ContainsFunc(reader.OFD.DocBody, func(body DocBody) bool {
|
|
name := cleanPackagePath(body.DocRoot)
|
|
return name != cleanPackagePath(reader.OFD.DocBody[index].DocRoot) && strings.HasPrefix(name, directory+"/")
|
|
}) {
|
|
for name := range reader.fileNames {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
if strings.HasPrefix(name, directory+"/") {
|
|
removed[name] = true
|
|
}
|
|
}
|
|
}
|
|
var others []int
|
|
for other := range reader.OFD.DocBody {
|
|
if other != index {
|
|
others = append(others, other)
|
|
}
|
|
}
|
|
kept, err := reader.documentFiles(ctx, others...)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for name := range kept {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
delete(removed, name)
|
|
}
|
|
delete(removed, "OFD.xml")
|
|
for name := range reader.files {
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
if removed[name] {
|
|
delete(reader.files, name)
|
|
}
|
|
}
|
|
if reader.Zip != nil {
|
|
reader.Zip = &zip.Reader{Comment: reader.Zip.Comment, File: slices.DeleteFunc(slices.Clone(reader.Zip.File), func(file *zip.File) bool {
|
|
return removed[cleanPackagePath(file.Name)]
|
|
})}
|
|
}
|
|
body := root.childAt("DocBody", index)
|
|
reader.files["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{{start: body.start, end: body.end}})
|
|
selected := reader.DocumentIndex()
|
|
if selected > index {
|
|
selected--
|
|
} else if selected == index {
|
|
selected = min(index, reader.DocumentCount()-2)
|
|
}
|
|
return selected, nil
|
|
})
|
|
}
|
|
|
|
// editDocumentsContext 在独立快照中修改文档入口,确认未取消后提交完整状态
|
|
// 入参: ctx 取消上下文, edit 修改包快照并返回新的当前文档索引
|
|
// 返回: error 修改或取消错误
|
|
func (e *Editor) editDocumentsContext(ctx context.Context, edit func(*Reader, []byte, *editorXML) (int, error)) error {
|
|
reader, err := e.readerContext(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return e.editDocumentSnapshot(ctx, reader, edit)
|
|
}
|
|
|
|
// editDocumentSnapshot 修改已有独立快照的文档入口,成功后提交并记录撤销历史
|
|
// 入参: ctx 取消上下文, reader 包含当前编辑内容的独立快照, edit 修改快照并返回当前文档索引
|
|
// 返回: error 修改或取消错误
|
|
func (e *Editor) editDocumentSnapshot(ctx context.Context, reader *Reader, edit func(*Reader, []byte, *editorXML) (int, error)) error {
|
|
data, err := reader.readFileViewContext(ctx, "OFD.xml")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := parseEditorXMLContext(ctx, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader.files = maps.Clone(reader.files)
|
|
previous := reader.OFD.DocBody[reader.documentIndex]
|
|
index, err := edit(reader, data, root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
before := e.transactionSnapshot()
|
|
reader.documentIndex, reader.doc = index, nil
|
|
if err := reader.initRootContext(ctx); err != nil {
|
|
return err
|
|
}
|
|
current := reader.OFD.DocBody[index]
|
|
if previous.DocRoot != current.DocRoot || previous.DocInfo.DocID != current.DocInfo.DocID {
|
|
reader.selectedVersion = nil
|
|
}
|
|
if _, err := reader.DocContext(ctx); err != nil {
|
|
return err
|
|
}
|
|
next := *e
|
|
if err := next.adoptDocumentReader(ctx, reader); err != nil {
|
|
return err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
*e = next
|
|
e.recordTransaction(before)
|
|
return nil
|
|
}
|
|
|
|
// adoptDocumentReader 接入已加载的编辑入口,同文档重建自定义字段来源,跨文档或版本时重建页面与资源状态
|
|
// 入参: ctx 取消上下文, reader 不拥有输入文件的独立阅读器
|
|
// 返回: error 编辑器初始化错误
|
|
func (e *Editor) adoptDocumentReader(ctx context.Context, reader *Reader) error {
|
|
version, err := reader.DocumentVersionID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
current, err := e.DocumentVersionID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if e.source != nil && e.documentRoot() == reader.documentRoot && current == version && e.Info.DocID == reader.OFD.DocBody[reader.documentIndex].DocInfo.DocID {
|
|
source := *e.source
|
|
source.reader = reader
|
|
if len(e.structureRemaps) != 0 {
|
|
tags, err := reader.CustomTagsContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
source.structureStarts = make(map[int]map[string]int, len(tags))
|
|
starts := e.structureRemapStarts()
|
|
for index := range tags {
|
|
source.structureStarts[index] = starts
|
|
}
|
|
}
|
|
source.info.CustomDatas = cloneEditorData(reader.OFD.DocBody[reader.documentIndex].DocInfo.CustomDatas)
|
|
if source.info.CustomDatas != nil {
|
|
for i := range source.info.CustomDatas.CustomData {
|
|
source.info.CustomDatas.CustomData[i].sourceIndex = i + 1
|
|
}
|
|
}
|
|
e.Info.CustomDatas = cloneEditorData(source.info.CustomDatas)
|
|
e.source = &source
|
|
} else {
|
|
next, err := reader.EditorContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
next.history, next.historyIndex, next.historyLimit = e.history, e.historyIndex, e.historyLimit
|
|
next.revision, next.serial = e.revision, e.serial
|
|
next.OnWriteProgress, next.output = e.OnWriteProgress, e.output
|
|
next.encryption = e.encryption
|
|
next.saveTarget = e.saveTarget
|
|
next.backends, next.fontDirs, next.fontFS = e.backends, e.fontDirs, e.fontFS
|
|
next.SetResources(e.runtimeResources)
|
|
next.validatedPaths = e.validatedPaths
|
|
*e = *next
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// documentRoot 获取当前文档的包内入口,区分不同文档的资源和标识域
|
|
// 返回: string 文档入口,新建单文档编辑器为空
|
|
func (e *Editor) documentRoot() string {
|
|
if e.source == nil {
|
|
return ""
|
|
}
|
|
return e.source.reader.documentRoot
|
|
}
|
|
|
|
// sameDocument 判断快照是否属于同一文档版本,区别删除后复用目录的新文档
|
|
// 入参: other 编辑快照
|
|
// 返回: bool 是否为同一文档
|
|
func (e *Editor) sameDocument(other *Editor) bool {
|
|
return e.documentIdentity().matches(other.documentIdentity())
|
|
}
|
|
|
|
// documentIdentity 获取当前文档的入口、原始标识及版本,文档排序不改变身份
|
|
// 返回: editorDocumentIdentity 文档身份
|
|
func (e *Editor) documentIdentity() editorDocumentIdentity {
|
|
if e.source == nil {
|
|
return editorDocumentIdentity{id: e.Info.DocID, fresh: true}
|
|
}
|
|
version, _ := e.DocumentVersionID()
|
|
return editorDocumentIdentity{root: e.documentRoot(), id: e.source.info.DocID, version: version, single: e.DocumentCount() == 1, fromNew: e.source.fromNew}
|
|
}
|
|
|
|
// matches 判断两个编辑状态是否属于同一文档版本,允许新文档首次建立包结构
|
|
// 入参: other 另一编辑状态的文档身份
|
|
// 返回: bool 是否为同一文档
|
|
func (identity editorDocumentIdentity) matches(other editorDocumentIdentity) bool {
|
|
if identity.fresh || other.fresh {
|
|
if identity.fresh && other.fresh {
|
|
return true
|
|
}
|
|
fresh, packaged := identity, other
|
|
if !fresh.fresh {
|
|
fresh, packaged = other, identity
|
|
}
|
|
return packaged.version == "" && packaged.single && (packaged.fromNew || fresh.id != "" && fresh.id == packaged.id)
|
|
}
|
|
return identity.root == other.root && identity.id == other.id && identity.version == other.version
|
|
}
|