mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
53769b65ab
commit
492adccb81
35 files changed
+1178
-235
No files matched your search
@@ -68,7 +68,7 @@ func (r *Reader) colorProfile(space *ColorSpace) (*colorProfile, error) {
|
|||||||
}
|
}
|
||||||
name := r.ResPath(space.Profile)
|
name := r.ResPath(space.Profile)
|
||||||
for _, other := range r.colorSpaceCache {
|
for _, other := range r.colorSpaceCache {
|
||||||
if other != nil && other.profile != nil && strings.EqualFold(other.Type, space.Type) && strings.EqualFold(r.ResPath(other.Profile), name) {
|
if other != nil && other.profile != nil && strings.EqualFold(other.Type, space.Type) && r.ResPath(other.Profile) == name {
|
||||||
space.profile = other.profile
|
space.profile = other.profile
|
||||||
return space.profile, space.profile.err
|
return space.profile, space.profile.err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
|
|||||||
}
|
}
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.image != nil {
|
if resource.image != nil {
|
||||||
snapshot.output.images[strings.ToLower(cleanPackagePath(resource.name))] = true
|
snapshot.output.images[cleanPackagePath(resource.name)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
@@ -154,7 +154,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, img := range images {
|
for _, img := range images {
|
||||||
snapshot.output.images[strings.ToLower(cleanPackagePath(img.Location))] = true
|
snapshot.output.images[cleanPackagePath(img.Location)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.Compression.Mode == CompressionLossy {
|
if options.Compression.Mode == CompressionLossy {
|
||||||
@@ -188,9 +188,8 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
|
|||||||
}
|
}
|
||||||
safe = false
|
safe = false
|
||||||
}
|
}
|
||||||
for key := range dependencies {
|
for name := range dependencies {
|
||||||
name, exists := r.fileNamesFold[key]
|
if !r.fileNames[name] {
|
||||||
if !exists {
|
|
||||||
safe = false
|
safe = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -269,7 +268,7 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
|
|||||||
paths := make(map[string]bool)
|
paths := make(map[string]bool)
|
||||||
for _, img := range images {
|
for _, img := range images {
|
||||||
if !safe || masks[editorResourceID(img.ID)] {
|
if !safe || masks[editorResourceID(img.ID)] {
|
||||||
paths[strings.ToLower(cleanPackagePath(img.Location))] = true
|
paths[cleanPackagePath(img.Location)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return paths, safe && geometry, ctx.Err()
|
return paths, safe && geometry, ctx.Err()
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// compressionImages 记录图片各处使用的最大像素需求
|
// compressionImages 记录图片各处使用的最大像素需求
|
||||||
@@ -71,7 +70,7 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
for _, img := range catalog {
|
for _, img := range catalog {
|
||||||
name := strings.ToLower(cleanPackagePath(img.Location))
|
name := cleanPackagePath(img.Location)
|
||||||
if old, ok := images[name]; !ok || old {
|
if old, ok := images[name]; !ok || old {
|
||||||
images[name] = !masks[name]
|
images[name] = !masks[name]
|
||||||
}
|
}
|
||||||
@@ -150,7 +149,7 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo,
|
|||||||
}
|
}
|
||||||
result := make(map[string]image.Point)
|
result := make(map[string]image.Point)
|
||||||
for _, img := range images {
|
for _, img := range images {
|
||||||
name := strings.ToLower(cleanPackagePath(img.Location))
|
name := cleanPackagePath(img.Location)
|
||||||
size := visitor.sizes[editorResourceID(img.ID)]
|
size := visitor.sizes[editorResourceID(img.ID)]
|
||||||
old := result[name]
|
old := result[name]
|
||||||
result[name] = image.Pt(max(old.X, size.X), max(old.Y, size.Y))
|
result[name] = image.Pt(max(old.X, size.X), max(old.Y, size.Y))
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
if err := e.output.ctx.Err(); err != nil {
|
if err := e.output.ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
key := strings.ToLower(cleanPackagePath(name))
|
key := cleanPackagePath(name)
|
||||||
if removed[key] || strings.HasSuffix(name, "/") {
|
if removed[key] || strings.HasSuffix(name, "/") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -126,6 +126,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
preserved := refs.versions[cleanPackagePath(name)]
|
||||||
for _, group := range root.children {
|
for _, group := range root.children {
|
||||||
for _, node := range group.children {
|
for _, node := range group.children {
|
||||||
field := ""
|
field := ""
|
||||||
@@ -147,11 +148,11 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
key := editorResourceLocation(name, root.attr("BaseLoc"), value)
|
key := editorResourceLocation(name, root.attr("BaseLoc"), value)
|
||||||
if field == "MediaFile" && node.attr("Type") != "Image" {
|
if preserved || field == "MediaFile" && node.attr("Type") != "Image" {
|
||||||
refs.files[key] = true
|
refs.files[key] = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if key != strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))) {
|
if key != cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if actual[key] != "" && !refs.files[key] {
|
if actual[key] != "" && !refs.files[key] {
|
||||||
@@ -259,10 +260,10 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
}
|
}
|
||||||
stem := strings.TrimSuffix(actual[key], ext)
|
stem := strings.TrimSuffix(actual[key], ext)
|
||||||
name := stem + ".jpg"
|
name := stem + ".jpg"
|
||||||
for n := 1; reserved[strings.ToLower(name)]; n++ {
|
for n := 1; reserved[name]; n++ {
|
||||||
name = fmt.Sprintf("%s-%d.jpg", stem, n)
|
name = fmt.Sprintf("%s-%d.jpg", stem, n)
|
||||||
}
|
}
|
||||||
reserved[strings.ToLower(name)] = true
|
reserved[name] = true
|
||||||
locations[key] = name
|
locations[key] = name
|
||||||
parts[name] = data
|
parts[name] = data
|
||||||
delete(parts, actual[key])
|
delete(parts, actual[key])
|
||||||
@@ -286,6 +287,9 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, name := range resources {
|
for _, name := range resources {
|
||||||
|
if refs.versions[cleanPackagePath(name)] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
data, err := read(name)
|
data, err := read(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+8
-27
@@ -124,7 +124,7 @@ func (r *Reader) open(options []ReaderOption) error {
|
|||||||
if err := r.indexPackage(); err != nil {
|
if err := r.indexPackage(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, ok := r.fileNamesFold["encryptions.xml"]; ok {
|
if r.fileNames["Encryptions.xml"] {
|
||||||
if err := r.decryptPackage(settings); err != nil {
|
if err := r.decryptPackage(settings); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -135,8 +135,8 @@ func (r *Reader) open(options []ReaderOption) error {
|
|||||||
// packageData 获取包内所有条目的实际字节,保留未识别文件
|
// packageData 获取包内所有条目的实际字节,保留未识别文件
|
||||||
// 返回: map[string][]byte 包内条目, error 错误信息
|
// 返回: map[string][]byte 包内条目, error 错误信息
|
||||||
func (r *Reader) packageData() (map[string][]byte, error) {
|
func (r *Reader) packageData() (map[string][]byte, error) {
|
||||||
parts := make(map[string][]byte, len(r.fileNamesFold))
|
parts := make(map[string][]byte, len(r.fileNames))
|
||||||
for _, name := range r.fileNamesFold {
|
for name := range r.fileNames {
|
||||||
data, err := r.readFile(name)
|
data, err := r.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -198,8 +198,7 @@ func (r *Reader) decryptPackage(settings readerOptions) error {
|
|||||||
extraSeeds := make(map[string]bool)
|
extraSeeds := make(map[string]bool)
|
||||||
for _, entry := range entries.Entries {
|
for _, entry := range entries.Entries {
|
||||||
name := cleanPackagePath(entry.Path)
|
name := cleanPackagePath(entry.Path)
|
||||||
fold := strings.ToLower(name)
|
if !validPackagePath(name) || used[name] {
|
||||||
if !validPackagePath(name) || used[fold] {
|
|
||||||
clear(key)
|
clear(key)
|
||||||
return fmt.Errorf("ambiguous encrypted package path: %q", entry.Path)
|
return fmt.Errorf("ambiguous encrypted package path: %q", entry.Path)
|
||||||
}
|
}
|
||||||
@@ -231,7 +230,7 @@ func (r *Reader) decryptPackage(settings readerOptions) error {
|
|||||||
return ErrInvalidCredentials
|
return ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
delete(parts, actual)
|
delete(parts, actual)
|
||||||
used[fold] = true
|
used[name] = true
|
||||||
}
|
}
|
||||||
clear(key)
|
clear(key)
|
||||||
delete(parts, seedName)
|
delete(parts, seedName)
|
||||||
@@ -239,16 +238,7 @@ func (r *Reader) decryptPackage(settings readerOptions) error {
|
|||||||
for name := range extraSeeds {
|
for name := range extraSeeds {
|
||||||
delete(parts, name)
|
delete(parts, name)
|
||||||
}
|
}
|
||||||
remaining := make(map[string]string, len(parts))
|
maps.Copy(parts, decrypted)
|
||||||
for name := range parts {
|
|
||||||
remaining[strings.ToLower(name)] = name
|
|
||||||
}
|
|
||||||
for name, plain := range decrypted {
|
|
||||||
if existing, ok := remaining[strings.ToLower(name)]; ok {
|
|
||||||
delete(parts, existing)
|
|
||||||
}
|
|
||||||
parts[name] = plain
|
|
||||||
}
|
|
||||||
state.info.Method = seed.Method
|
state.info.Method = seed.Method
|
||||||
for _, user := range seed.Users {
|
for _, user := range seed.Users {
|
||||||
state.info.Users = append(state.info.Users, user.Name)
|
state.info.Users = append(state.info.Users, user.Name)
|
||||||
@@ -257,11 +247,7 @@ func (r *Reader) decryptPackage(settings readerOptions) error {
|
|||||||
state.options = inheritedEncryptionOptions(seed, credential, settings.provider)
|
state.options = inheritedEncryptionOptions(seed, credential, settings.provider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name := range parts {
|
delete(parts, "Encryptions.xml")
|
||||||
if strings.ToLower(name) == "encryptions.xml" {
|
|
||||||
delete(parts, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.Zip, r.files, r.encryption = nil, parts, state
|
r.Zip, r.files, r.encryption = nil, parts, state
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -289,7 +275,7 @@ func decodeEncryptionEntries(data, key, iv []byte, provider CryptoProvider) (enc
|
|||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// encryptionPart 按统一大小写规则读取加密元数据引用
|
// encryptionPart 按大小写敏感路径读取加密元数据引用
|
||||||
// 入参: parts 包内条目, name 引用路径
|
// 入参: parts 包内条目, name 引用路径
|
||||||
// 返回: []byte 条目内容, string 实际路径, error 错误信息
|
// 返回: []byte 条目内容, string 实际路径, error 错误信息
|
||||||
func encryptionPart(parts map[string][]byte, name string) ([]byte, string, error) {
|
func encryptionPart(parts map[string][]byte, name string) ([]byte, string, error) {
|
||||||
@@ -300,11 +286,6 @@ func encryptionPart(parts map[string][]byte, name string) ([]byte, string, error
|
|||||||
if data, ok := parts[name]; ok {
|
if data, ok := parts[name]; ok {
|
||||||
return data, name, nil
|
return data, name, nil
|
||||||
}
|
}
|
||||||
for actual, data := range parts {
|
|
||||||
if strings.ToLower(actual) == strings.ToLower(name) {
|
|
||||||
return data, actual, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, "", fmt.Errorf("missing encrypted entry: %s", name)
|
return nil, "", fmt.Errorf("missing encrypted entry: %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (e *Editor) SetDocumentActions(actions []Action) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
name := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot)
|
name := base.reader.documentRoot
|
||||||
data, err := base.reader.readFile(name)
|
data, err := base.reader.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi
|
|||||||
if len(patches) != 0 {
|
if len(patches) != 0 {
|
||||||
parts[name] = editorPatchXML(data, patches)
|
parts[name] = editorPatchXML(data, patches)
|
||||||
if remaining == 0 {
|
if remaining == 0 {
|
||||||
docName, err := editorPageLocation(reader, nil, "", "/"+cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot))
|
docName, err := editorPageLocation(reader, nil, "", "/"+reader.documentRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
@@ -379,7 +380,7 @@ func (e *Editor) appendAnnotations(index int, annotations []byte, generated bool
|
|||||||
}
|
}
|
||||||
parts := map[string][]byte{name: data, file: content}
|
parts := map[string][]byte{name: data, file: content}
|
||||||
if base.document.Annotations == "" {
|
if base.document.Annotations == "" {
|
||||||
docName := cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot)
|
docName := reader.documentRoot
|
||||||
docData, err := reader.readFile(docName)
|
docData, err := reader.readFile(docName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -448,6 +449,10 @@ func (e *Editor) annotationFiles() []string {
|
|||||||
// 入参: base 原包, parts 修改的文件
|
// 入参: base 原包, parts 修改的文件
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]byte) error {
|
func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]byte) error {
|
||||||
|
parts, err := base.reader.versionChanges(context.Background(), parts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
files := maps.Clone(base.reader.files)
|
files := maps.Clone(base.reader.files)
|
||||||
if files == nil {
|
if files == nil {
|
||||||
files = make(map[string][]byte)
|
files = make(map[string][]byte)
|
||||||
@@ -458,7 +463,7 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by
|
|||||||
}
|
}
|
||||||
files[name] = data
|
files[name] = data
|
||||||
}
|
}
|
||||||
reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex}
|
reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex, selectedVersion: base.reader.selectedVersion}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRoot(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx,
|
|||||||
edit.source.annotationStates = states
|
edit.source.annotationStates = states
|
||||||
if len(selection.resources) != 0 {
|
if len(selection.resources) != 0 {
|
||||||
reader := edit.source.reader
|
reader := edit.source.reader
|
||||||
name := cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot)
|
name := reader.documentRoot
|
||||||
data, err := reader.readFile(name)
|
data, err := reader.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
entry = bytes.TrimPrefix(entry, []byte(xml.Header))
|
entry = bytes.TrimPrefix(entry, []byte(xml.Header))
|
||||||
docName := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot)
|
docName := base.reader.documentRoot
|
||||||
docData, err := base.reader.readFile(docName)
|
docData, err := base.reader.readFile(docName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -220,7 +220,7 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
|
|||||||
if base == nil {
|
if base == nil {
|
||||||
return fmt.Errorf("attachment not found: %s", id)
|
return fmt.Errorf("attachment not found: %s", id)
|
||||||
}
|
}
|
||||||
name := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot)
|
name := base.reader.documentRoot
|
||||||
external := strings.TrimSpace(base.document.Attachments.Path) != ""
|
external := strings.TrimSpace(base.document.Attachments.Path) != ""
|
||||||
if external {
|
if external {
|
||||||
name = base.reader.ResPath(base.document.Attachments.Path)
|
name = base.reader.ResPath(base.document.Attachments.Path)
|
||||||
@@ -277,7 +277,7 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
|
|||||||
if retired == nil {
|
if retired == nil {
|
||||||
retired = make(map[string]bool)
|
retired = make(map[string]bool)
|
||||||
}
|
}
|
||||||
retired[strings.ToLower(cleanPackagePath(base.reader.ResPath(location)))] = true
|
retired[editorResourceLocation(name, "", location)] = true
|
||||||
e.source.retiredAttachments = retired
|
e.source.retiredAttachments = retired
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-14
@@ -93,7 +93,7 @@ func (e *Editor) SetDocumentInfo(index int, info DocInfo) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
reader.files[reader.fileNamesFold["ofd.xml"]] = updated
|
reader.files["OFD.xml"] = updated
|
||||||
return reader.DocumentIndex(), nil
|
return reader.DocumentIndex(), nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -116,8 +116,8 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
|
|||||||
for number := 0; ; number++ {
|
for number := 0; ; number++ {
|
||||||
candidate := fmt.Sprintf("Doc_%d", number)
|
candidate := fmt.Sprintf("Doc_%d", number)
|
||||||
occupied := false
|
occupied := false
|
||||||
for name := range reader.fileNamesFold {
|
for name := range reader.fileNames {
|
||||||
if name == strings.ToLower(candidate) || strings.HasPrefix(name, strings.ToLower(candidate)+"/") {
|
if name == candidate || strings.HasPrefix(name, candidate+"/") {
|
||||||
occupied = true
|
occupied = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
|
|||||||
if existing := root.childAt("DocBody", at); existing != nil {
|
if existing := root.childAt("DocBody", at); existing != nil {
|
||||||
position = existing.start
|
position = existing.start
|
||||||
}
|
}
|
||||||
reader.files[reader.fileNamesFold["ofd.xml"]] = editorPatchXML(data, []editorXMLPatch{{position, position, bytes.TrimPrefix(body, []byte(xml.Header))}})
|
reader.files["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{{position, position, bytes.TrimPrefix(body, []byte(xml.Header))}})
|
||||||
return at, nil
|
return at, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -199,7 +199,7 @@ func (e *Editor) MoveDocument(from, to int) error {
|
|||||||
body := root.childAt("DocBody", index)
|
body := root.childAt("DocBody", index)
|
||||||
patches = append(patches, editorXMLPatch{body.start, body.end, content})
|
patches = append(patches, editorXMLPatch{body.start, body.end, content})
|
||||||
}
|
}
|
||||||
reader.files[reader.fileNamesFold["ofd.xml"]] = editorPatchXML(data, patches)
|
reader.files["OFD.xml"] = editorPatchXML(data, patches)
|
||||||
selected := reader.DocumentIndex()
|
selected := reader.DocumentIndex()
|
||||||
if selected == from {
|
if selected == from {
|
||||||
selected = to
|
selected = to
|
||||||
@@ -232,10 +232,10 @@ func (e *Editor) DeleteDocument(index int) error {
|
|||||||
directory := path.Dir(cleanPackagePath(reader.OFD.DocBody[index].DocRoot))
|
directory := path.Dir(cleanPackagePath(reader.OFD.DocBody[index].DocRoot))
|
||||||
if directory != "." && !slices.ContainsFunc(reader.OFD.DocBody, func(body DocBody) bool {
|
if directory != "." && !slices.ContainsFunc(reader.OFD.DocBody, func(body DocBody) bool {
|
||||||
name := cleanPackagePath(body.DocRoot)
|
name := cleanPackagePath(body.DocRoot)
|
||||||
return name != cleanPackagePath(reader.OFD.DocBody[index].DocRoot) && strings.HasPrefix(strings.ToLower(name), strings.ToLower(directory)+"/")
|
return name != cleanPackagePath(reader.OFD.DocBody[index].DocRoot) && strings.HasPrefix(name, directory+"/")
|
||||||
}) {
|
}) {
|
||||||
for name := range reader.fileNamesFold {
|
for name := range reader.fileNames {
|
||||||
if strings.HasPrefix(name, strings.ToLower(directory)+"/") {
|
if strings.HasPrefix(name, directory+"/") {
|
||||||
removed[name] = true
|
removed[name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,19 +252,19 @@ func (e *Editor) DeleteDocument(index int) error {
|
|||||||
delete(removed, name)
|
delete(removed, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(removed, "ofd.xml")
|
delete(removed, "OFD.xml")
|
||||||
for name := range reader.files {
|
for name := range reader.files {
|
||||||
if removed[strings.ToLower(name)] {
|
if removed[name] {
|
||||||
delete(reader.files, name)
|
delete(reader.files, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if reader.Zip != nil {
|
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 {
|
reader.Zip = &zip.Reader{Comment: reader.Zip.Comment, File: slices.DeleteFunc(slices.Clone(reader.Zip.File), func(file *zip.File) bool {
|
||||||
return removed[strings.ToLower(cleanPackagePath(file.Name))]
|
return removed[cleanPackagePath(file.Name)]
|
||||||
})}
|
})}
|
||||||
}
|
}
|
||||||
body := root.childAt("DocBody", index)
|
body := root.childAt("DocBody", index)
|
||||||
reader.files[reader.fileNamesFold["ofd.xml"]] = editorPatchXML(data, []editorXMLPatch{{start: body.start, end: body.end}})
|
reader.files["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{{start: body.start, end: body.end}})
|
||||||
selected := reader.DocumentIndex()
|
selected := reader.DocumentIndex()
|
||||||
if selected > index {
|
if selected > index {
|
||||||
selected--
|
selected--
|
||||||
@@ -292,6 +292,7 @@ func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reader.files = maps.Clone(reader.files)
|
reader.files = maps.Clone(reader.files)
|
||||||
|
previous := reader.OFD.DocBody[reader.documentIndex]
|
||||||
index, err := edit(reader, data, root)
|
index, err := edit(reader, data, root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -301,10 +302,14 @@ func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, erro
|
|||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRoot(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
current := reader.OFD.DocBody[index]
|
||||||
|
if previous.DocRoot != current.DocRoot || previous.DocInfo.DocID != current.DocInfo.DocID {
|
||||||
|
reader.selectedVersion = nil
|
||||||
|
}
|
||||||
if _, err := reader.Doc(); err != nil {
|
if _, err := reader.Doc(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if e.source != nil && e.documentRoot() == reader.OFD.DocBody[index].DocRoot && e.Info.DocID == reader.OFD.DocBody[index].DocInfo.DocID {
|
if e.source != nil && e.documentRoot() == reader.documentRoot && e.Info.DocID == reader.OFD.DocBody[index].DocInfo.DocID {
|
||||||
source := *e.source
|
source := *e.source
|
||||||
source.reader = reader
|
source.reader = reader
|
||||||
e.source = &source
|
e.source = &source
|
||||||
@@ -331,7 +336,7 @@ func (e *Editor) documentRoot() string {
|
|||||||
if e.source == nil {
|
if e.source == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return e.source.reader.OFD.DocBody[e.DocumentIndex()].DocRoot
|
return e.source.reader.documentRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
// sameDocument 判断快照是否属于同一文档,区别删除后复用目录的新文档
|
// sameDocument 判断快照是否属于同一文档,区别删除后复用目录的新文档
|
||||||
|
|||||||
@@ -407,6 +407,9 @@ func sameBindingFont(source, target []byte) bool {
|
|||||||
// 返回: FontFile 独立字体, error 匹配或解析错误
|
// 返回: FontFile 独立字体, error 匹配或解析错误
|
||||||
func (e *Editor) resolveBindingFont(scan *editorFontDocument, declaration *editorFontDeclaration) (FontFile, error) {
|
func (e *Editor) resolveBindingFont(scan *editorFontDocument, declaration *editorFontDeclaration) (FontFile, error) {
|
||||||
reader := &Reader{Zip: scan.reader.Zip, files: maps.Clone(scan.reader.files), documentIndex: scan.index}
|
reader := &Reader{Zip: scan.reader.Zip, files: maps.Clone(scan.reader.files), documentIndex: scan.index}
|
||||||
|
if scan.index == scan.reader.documentIndex {
|
||||||
|
reader.selectedVersion = scan.reader.selectedVersion
|
||||||
|
}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRoot(); err != nil {
|
||||||
return FontFile{}, err
|
return FontFile{}, err
|
||||||
}
|
}
|
||||||
@@ -501,7 +504,7 @@ func (e *Editor) commitFontSnapshot(ctx context.Context, reader *Reader) error {
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
clean := &Reader{Zip: reader.Zip, files: maps.Clone(reader.files), documentIndex: reader.DocumentIndex(), encryption: e.encryption}
|
clean := &Reader{Zip: reader.Zip, files: maps.Clone(reader.files), documentIndex: reader.DocumentIndex(), encryption: e.encryption, selectedVersion: reader.selectedVersion}
|
||||||
if err := clean.initRoot(); err != nil {
|
if err := clean.initRoot(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -544,7 +547,7 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s
|
|||||||
}
|
}
|
||||||
removed := make(map[string]bool, len(candidates))
|
removed := make(map[string]bool, len(candidates))
|
||||||
for _, candidate := range candidates {
|
for _, candidate := range candidates {
|
||||||
if key := strings.ToLower(cleanPackagePath(candidate)); key != "" {
|
if key := cleanPackagePath(candidate); key != "" {
|
||||||
removed[key] = true
|
removed[key] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,7 +569,7 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, name := range reader.fileNamesFold {
|
for name := range reader.fileNames {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -597,7 +600,7 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s
|
|||||||
}
|
}
|
||||||
if node.matchesOFD("FontFile") {
|
if node.matchesOFD("FontFile") {
|
||||||
location := resolveResourcePath(name, base, strings.TrimSpace(editorImportText(data, node)))
|
location := resolveResourcePath(name, base, strings.TrimSpace(editorImportText(data, node)))
|
||||||
delete(removed, strings.ToLower(cleanPackagePath(location)))
|
delete(removed, cleanPackagePath(location))
|
||||||
}
|
}
|
||||||
return slices.ContainsFunc(node.children, retains)
|
return slices.ContainsFunc(node.children, retains)
|
||||||
}
|
}
|
||||||
@@ -609,12 +612,12 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for name := range reader.files {
|
for name := range reader.files {
|
||||||
if removed[strings.ToLower(cleanPackagePath(name))] {
|
if removed[cleanPackagePath(name)] {
|
||||||
delete(reader.files, name)
|
delete(reader.files, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if reader.Zip != nil {
|
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[strings.ToLower(cleanPackagePath(file.Name))] })}
|
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)] })}
|
||||||
}
|
}
|
||||||
return reader.initRoot()
|
return reader.initRoot()
|
||||||
}
|
}
|
||||||
+38
-13
@@ -62,16 +62,17 @@ type editorFontDeclaration struct {
|
|||||||
// 返回: *editorFontDocument 字体引用, error 读取或结构错误
|
// 返回: *editorFontDocument 字体引用, error 读取或结构错误
|
||||||
func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFontDocument, error) {
|
func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFontDocument, error) {
|
||||||
scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration)}
|
scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration)}
|
||||||
queue := []string{cleanPackagePath(reader.OFD.DocBody[index].DocRoot)}
|
entry, err := reader.documentEntry(index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
queue := []string{entry}
|
||||||
for len(queue) != 0 {
|
for len(queue) != 0 {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
name := queue[0]
|
name := queue[0]
|
||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
if actual := reader.fileNamesFold[strings.ToLower(name)]; actual != "" {
|
|
||||||
name = actual
|
|
||||||
}
|
|
||||||
if scan.files[name] != nil {
|
if scan.files[name] != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -148,9 +149,6 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon
|
|||||||
}
|
}
|
||||||
if value != "" {
|
if value != "" {
|
||||||
target := resolveResourcePath(name, base, value)
|
target := resolveResourcePath(name, base, value)
|
||||||
if actual := reader.fileNamesFold[strings.ToLower(target)]; actual != "" {
|
|
||||||
target = actual
|
|
||||||
}
|
|
||||||
file.links = append(file.links, editorFontLink{node: node, attribute: attribute, target: target})
|
file.links = append(file.links, editorFontLink{node: node, attribute: attribute, target: target})
|
||||||
queue = append(queue, target)
|
queue = append(queue, target)
|
||||||
}
|
}
|
||||||
@@ -172,6 +170,29 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon
|
|||||||
// 入参: ctx 取消上下文, changes 按原文件路径保存的修改
|
// 入参: ctx 取消上下文, changes 按原文件路径保存的修改
|
||||||
// 返回: error 引用扫描或XML修改错误
|
// 返回: error 引用扫描或XML修改错误
|
||||||
func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byte) error {
|
func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byte) error {
|
||||||
|
if s.reader.OFD.DocBody[s.index].versioned {
|
||||||
|
view := *s.reader
|
||||||
|
if s.index != view.documentIndex {
|
||||||
|
view.selectedVersion = nil
|
||||||
|
}
|
||||||
|
view.documentIndex = s.index
|
||||||
|
view.documentRoot, view.versionInfo = "", nil
|
||||||
|
parts, err := view.versionChanges(ctx, changes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
next := *s.reader
|
||||||
|
next.files = maps.Clone(next.files)
|
||||||
|
if next.files == nil {
|
||||||
|
next.files = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
maps.Copy(next.files, parts)
|
||||||
|
if err := next.initRoot(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s.reader = next
|
||||||
|
return nil
|
||||||
|
}
|
||||||
shared := make(map[string]bool)
|
shared := make(map[string]bool)
|
||||||
if s.reader.DocumentCount() > 1 {
|
if s.reader.DocumentCount() > 1 {
|
||||||
for index := range s.reader.DocumentCount() {
|
for index := range s.reader.DocumentCount() {
|
||||||
@@ -187,14 +208,14 @@ func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byt
|
|||||||
}
|
}
|
||||||
paths := make(map[string]string)
|
paths := make(map[string]string)
|
||||||
for name := range changes {
|
for name := range changes {
|
||||||
if shared[strings.ToLower(name)] {
|
if shared[name] {
|
||||||
paths[name] = packageAvailableName(s.reader, changes, name+".font.xml")
|
paths[name] = packageAvailableName(s.reader, changes, name+".font.xml")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for changed := true; changed; {
|
for changed := true; changed; {
|
||||||
changed = false
|
changed = false
|
||||||
for name, file := range s.files {
|
for name, file := range s.files {
|
||||||
if paths[name] != "" || !shared[strings.ToLower(name)] {
|
if paths[name] != "" || !shared[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, link := range file.links {
|
for _, link := range file.links {
|
||||||
@@ -236,7 +257,11 @@ func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if node.open == node.end {
|
||||||
|
patches = append(patches, editorXMLPatch{node.start, node.end, fragment})
|
||||||
|
} else {
|
||||||
patches = append(patches, editorXMLPatch{node.start, node.open, fragment[:header.open]})
|
patches = append(patches, editorXMLPatch{node.start, node.open, fragment[:header.open]})
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if link.attribute == "" && editorImportText(data, node) == editorImportText(file.data, link.node) {
|
if link.attribute == "" && editorImportText(data, node) == editorImportText(file.data, link.node) {
|
||||||
@@ -261,9 +286,9 @@ func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byt
|
|||||||
changes[name] = data
|
changes[name] = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootName := cleanPackagePath(s.reader.OFD.DocBody[s.index].DocRoot)
|
rootName, err := s.reader.documentEntry(s.index)
|
||||||
if actual := s.reader.fileNamesFold[strings.ToLower(rootName)]; actual != "" {
|
if err != nil {
|
||||||
rootName = actual
|
return err
|
||||||
}
|
}
|
||||||
if target := paths[rootName]; target != "" {
|
if target := paths[rootName]; target != "" {
|
||||||
data, err := s.reader.readFile("OFD.xml")
|
data, err := s.reader.readFile("OFD.xml")
|
||||||
@@ -275,7 +300,7 @@ func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byt
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
node := root.childAt("DocBody", s.index).child("DocRoot")
|
node := root.childAt("DocBody", s.index).child("DocRoot")
|
||||||
changes[s.reader.fileNamesFold["ofd.xml"]] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, editorFontXMLText("/"+target))})
|
changes["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, editorFontXMLText("/"+target))})
|
||||||
}
|
}
|
||||||
for name, data := range changes {
|
for name, data := range changes {
|
||||||
if target := paths[name]; target != "" {
|
if target := paths[name]; target != "" {
|
||||||
|
|||||||
+21
-7
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -229,6 +230,7 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
|
|||||||
// ImportPages 将来源当前文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
// ImportPages 将来源当前文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
|
||||||
// 页面索引从0开始,at可等于当前页数;复制关联模板、注释、签章外观和实际引用的资源
|
// 页面索引从0开始,at可等于当前页数;复制关联模板、注释、签章外观和实际引用的资源
|
||||||
// 不导入来源元数据和目录,指向未选页面的跳转被移除;签名数据仅保留原始凭据,不代表合并后文档有效
|
// 不导入来源元数据和目录,指向未选页面的跳转被移除;签名数据仅保留原始凭据,不代表合并后文档有效
|
||||||
|
// 目标文档含版本列表时暂不支持导入带签章页面,返回错误且不修改目标
|
||||||
// 返回后可关闭来源Reader,目标原Reader的生命周期要求不变
|
// 返回后可关闭来源Reader,目标原Reader的生命周期要求不变
|
||||||
// 入参: source 来源阅读器, indexes 来源页面索引,不可重复, at 目标插入位置
|
// 入参: source 来源阅读器, indexes 来源页面索引,不可重复, at 目标插入位置
|
||||||
// 返回: []string 新页面标识, error 错误信息
|
// 返回: []string 新页面标识, error 错误信息
|
||||||
@@ -455,7 +457,7 @@ func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[strin
|
|||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
m.files[resource.name] = nil
|
m.files[resource.name] = nil
|
||||||
}
|
}
|
||||||
m.documentXML, err = m.readFile(source.ResPath(source.OFD.DocBody[source.documentIndex].DocRoot))
|
m.documentXML, err = m.readFile(source.documentRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -535,7 +537,7 @@ func (m *editorPageImport) readFile(name string) ([]byte, error) {
|
|||||||
// outlines 筛选指向导入页面的目录树并复用标准动作迁移
|
// outlines 筛选指向导入页面的目录树并复用标准动作迁移
|
||||||
// 返回: []byte 目录片段, error 错误信息
|
// 返回: []byte 目录片段, error 错误信息
|
||||||
func (m *editorPageImport) outlines() ([]byte, error) {
|
func (m *editorPageImport) outlines() ([]byte, error) {
|
||||||
name := m.reader.ResPath(m.reader.OFD.DocBody[m.reader.documentIndex].DocRoot)
|
name := m.reader.documentRoot
|
||||||
data, err := m.readFile(name)
|
data, err := m.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -838,7 +840,7 @@ func (m *editorPageImport) copyData(name, relative string, evidence bool) (strin
|
|||||||
paths = m.evidence
|
paths = m.evidence
|
||||||
relative = path.Join("Signs", "Signed", path.Base(name))
|
relative = path.Join("Signs", "Signed", path.Base(name))
|
||||||
}
|
}
|
||||||
key := strings.ToLower(cleanPackagePath(name))
|
key := cleanPackagePath(name)
|
||||||
if target := paths[key]; target != "" {
|
if target := paths[key]; target != "" {
|
||||||
return "/" + target, nil
|
return "/" + target, nil
|
||||||
}
|
}
|
||||||
@@ -858,7 +860,7 @@ func (m *editorPageImport) copyData(name, relative string, evidence bool) (strin
|
|||||||
// 入参: name 来源路径, candidate 目标候选路径
|
// 入参: name 来源路径, candidate 目标候选路径
|
||||||
// 返回: string 新路径, error 错误信息
|
// 返回: string 新路径, error 错误信息
|
||||||
func (m *editorPageImport) copyXML(name, candidate string) (string, error) {
|
func (m *editorPageImport) copyXML(name, candidate string) (string, error) {
|
||||||
key := strings.ToLower(cleanPackagePath(name))
|
key := cleanPackagePath(name)
|
||||||
if target := m.paths[key]; target != "" {
|
if target := m.paths[key]; target != "" {
|
||||||
return "/" + target, nil
|
return "/" + target, nil
|
||||||
}
|
}
|
||||||
@@ -1188,7 +1190,7 @@ func (m *editorPageImport) attachment(id string) (string, error) {
|
|||||||
if _, exists := m.attachments[id]; exists {
|
if _, exists := m.attachments[id]; exists {
|
||||||
return m.attachmentID(id)
|
return m.attachmentID(id)
|
||||||
}
|
}
|
||||||
name := m.reader.ResPath(m.reader.OFD.DocBody[m.reader.documentIndex].DocRoot)
|
name := m.reader.documentRoot
|
||||||
if m.doc.Attachments.Path != "" {
|
if m.doc.Attachments.Path != "" {
|
||||||
name = m.reader.ResPath(m.doc.Attachments.Path)
|
name = m.reader.ResPath(m.doc.Attachments.Path)
|
||||||
}
|
}
|
||||||
@@ -1350,7 +1352,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
|||||||
files = make(map[string][]byte)
|
files = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
maps.Copy(files, m.files)
|
maps.Copy(files, m.files)
|
||||||
name := base.reader.ResPath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot)
|
name := base.reader.documentRoot
|
||||||
if file, ok := base.reader.packageFile(name); ok {
|
if file, ok := base.reader.packageFile(name); ok {
|
||||||
name = cleanPackagePath(file.Name)
|
name = cleanPackagePath(file.Name)
|
||||||
}
|
}
|
||||||
@@ -1454,6 +1456,9 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
|||||||
}
|
}
|
||||||
files[name] = data
|
files[name] = data
|
||||||
if len(signatures) != 0 {
|
if len(signatures) != 0 {
|
||||||
|
if base.reader.OFD.DocBody[base.reader.documentIndex].versioned {
|
||||||
|
return nil, fmt.Errorf("signature import into a versioned document is not supported")
|
||||||
|
}
|
||||||
loc, err := mergeImportIndex(base.reader, files, base.document.Signatures, path.Join(m.directory, "Signs", "Signatures.xml"), "Signatures", signatures)
|
loc, err := mergeImportIndex(base.reader, files, base.document.Signatures, path.Join(m.directory, "Signs", "Signatures.xml"), "Signatures", signatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1490,7 +1495,16 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex}
|
parts, err := base.reader.versionChanges(context.Background(), files)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = maps.Clone(base.reader.files)
|
||||||
|
if files == nil {
|
||||||
|
files = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
maps.Copy(files, parts)
|
||||||
|
reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex, selectedVersion: base.reader.selectedVersion}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRoot(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ type PageCapabilities struct {
|
|||||||
// Editor 打开当前文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑
|
// Editor 打开当前文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑
|
||||||
// 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名
|
// 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名
|
||||||
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader
|
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader
|
||||||
|
// 版本共享文件在提交修改时隔离,保留其他版本及其文件清单
|
||||||
// 返回: *Editor 编辑器, error 错误信息
|
// 返回: *Editor 编辑器, error 错误信息
|
||||||
func (r *Reader) Editor() (*Editor, error) {
|
func (r *Reader) Editor() (*Editor, error) {
|
||||||
_, err := r.Doc()
|
_, err := r.Doc()
|
||||||
@@ -419,7 +420,7 @@ func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bo
|
|||||||
// 返回: error 引用歧义或读取错误
|
// 返回: error 引用歧义或读取错误
|
||||||
func (e *Editor) checkDuplicateObjectReferences(duplicates map[string]bool) error {
|
func (e *Editor) checkDuplicateObjectReferences(duplicates map[string]bool) error {
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
for _, name := range reader.fileNamesFold {
|
for name := range reader.fileNames {
|
||||||
if !strings.EqualFold(path.Ext(name), ".xml") {
|
if !strings.EqualFold(path.Ext(name), ".xml") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ func (e *Editor) outlineXML() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
data, err := reader.readFile(reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot))
|
data, err := reader.readFile(reader.documentRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-11
@@ -17,6 +17,7 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -31,7 +32,11 @@ import (
|
|||||||
// 入参: progress 保存进度回调,预览时为nil
|
// 入参: progress 保存进度回调,预览时为nil
|
||||||
// 返回: map[string][]byte 替换及新增条目, error 错误信息
|
// 返回: map[string][]byte 替换及新增条目, error 错误信息
|
||||||
func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) {
|
func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) {
|
||||||
return e.sourcePartsReferences(progress, nil)
|
parts, err := e.sourcePartsReferences(progress, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.source.reader.versionChanges(context.Background(), parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用
|
// sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用
|
||||||
@@ -49,6 +54,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
var stager editorPageStager
|
var stager editorPageStager
|
||||||
source := e.source
|
source := e.source
|
||||||
reader := source.reader
|
reader := source.reader
|
||||||
|
stage = stage && !reader.OFD.DocBody[reader.documentIndex].versioned
|
||||||
pageRefs := make([]Page, len(e.pages))
|
pageRefs := make([]Page, len(e.pages))
|
||||||
originalCount := 0
|
originalCount := 0
|
||||||
for i, page := range e.pages {
|
for i, page := range e.pages {
|
||||||
@@ -111,7 +117,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
commonData := source.document.CommonData
|
commonData := source.document.CommonData
|
||||||
for _, name := range append(slices.Clone(commonData.DocumentRes), commonData.PublicRes...) {
|
for _, name := range append(slices.Clone(commonData.DocumentRes), commonData.PublicRes...) {
|
||||||
resourceFiles = slices.DeleteFunc(resourceFiles, func(file string) bool {
|
resourceFiles = slices.DeleteFunc(resourceFiles, func(file string) bool {
|
||||||
return strings.EqualFold(file, reader.ResPath(name))
|
return file == reader.ResPath(name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(fonts)+len(images)+len(spaces) != 0 {
|
if len(fonts)+len(images)+len(spaces) != 0 {
|
||||||
@@ -134,7 +140,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
}
|
}
|
||||||
pagesChanged := !slices.Equal(pageRefs, source.document.Pages.Page)
|
pagesChanged := !slices.Equal(pageRefs, source.document.Pages.Page)
|
||||||
if pagesChanged || len(resourceFiles) != 0 || len(parts) != 0 && e.maxID != source.document.CommonData.MaxUnitID {
|
if pagesChanged || len(resourceFiles) != 0 || len(parts) != 0 && e.maxID != source.document.CommonData.MaxUnitID {
|
||||||
name := reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot)
|
name := reader.documentRoot
|
||||||
data, err := reader.readFile(name)
|
data, err := reader.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -232,7 +238,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
parts["OFD.xml"] = data
|
parts["OFD.xml"] = data
|
||||||
}
|
}
|
||||||
if e.outlines != nil {
|
if e.outlines != nil {
|
||||||
name := reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot)
|
name := reader.documentRoot
|
||||||
data, ok := parts[name]
|
data, ok := parts[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
var err error
|
var err error
|
||||||
@@ -586,6 +592,14 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
|
|||||||
if err := e.compressResourceReferences(parts, reader, removed, generated); err != nil {
|
if err := e.compressResourceReferences(parts, reader, removed, generated); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
if e.output != nil {
|
||||||
|
ctx = e.output.ctx
|
||||||
|
}
|
||||||
|
parts, err = reader.versionChanges(ctx, parts)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected {
|
if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected {
|
||||||
name := "OFD.xml"
|
name := "OFD.xml"
|
||||||
if file, ok := reader.packageFile(name); ok {
|
if file, ok := reader.packageFile(name); ok {
|
||||||
@@ -610,7 +624,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
|
|||||||
}
|
}
|
||||||
maps.Copy(remaining, parts)
|
maps.Copy(remaining, parts)
|
||||||
for name := range remaining {
|
for name := range remaining {
|
||||||
if removed[strings.ToLower(cleanPackagePath(name))] {
|
if removed[cleanPackagePath(name)] {
|
||||||
delete(remaining, name)
|
delete(remaining, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,7 +645,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := cleanPackagePath(file.Name)
|
name := cleanPackagePath(file.Name)
|
||||||
if removed[strings.ToLower(name)] {
|
if removed[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if data, ok := remaining[name]; ok {
|
if data, ok := remaining[name]; ok {
|
||||||
@@ -710,8 +724,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
|
|||||||
maps.Copy(reader.files, parts)
|
maps.Copy(reader.files, parts)
|
||||||
reader.encryption = e.encryption
|
reader.encryption = e.encryption
|
||||||
for name := range reader.pageHeaderCache {
|
for name := range reader.pageHeaderCache {
|
||||||
actual := reader.fileNamesFold[strings.ToLower(name)]
|
if _, changed := parts[name]; changed {
|
||||||
if _, changed := parts[actual]; changed {
|
|
||||||
delete(reader.pageHeaderCache, name)
|
delete(reader.pageHeaderCache, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,7 +738,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
|
|||||||
files = make(map[string][]byte)
|
files = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
maps.Copy(files, parts)
|
maps.Copy(files, parts)
|
||||||
reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex}
|
reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex, selectedVersion: e.source.reader.selectedVersion}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRoot(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -733,8 +746,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for name, header := range e.source.reader.pageHeaderCache {
|
for name, header := range e.source.reader.pageHeaderCache {
|
||||||
actual := e.source.reader.fileNamesFold[strings.ToLower(name)]
|
if _, changed := parts[name]; !changed {
|
||||||
if _, changed := parts[actual]; !changed {
|
|
||||||
reader.pageHeaderCache[name] = header
|
reader.pageHeaderCache[name] = header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
// prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档
|
// prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档
|
||||||
// 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
|
// 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
|
||||||
|
// 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据
|
||||||
// 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil
|
// 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil
|
||||||
// 返回: error 读取或解析错误
|
// 返回: error 读取或解析错误
|
||||||
func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
|
func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
|
||||||
@@ -50,8 +51,8 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
|
|||||||
bookmarks[bookmark.Name] = true
|
bookmarks[bookmark.Name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
queue := []string{reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot)}
|
queue := []string{reader.documentRoot}
|
||||||
if name := e.source.document.Signatures; name != "" {
|
if name := e.source.document.Signatures; name != "" && !reader.OFD.DocBody[reader.documentIndex].versioned {
|
||||||
queue = append(queue, reader.ResPath(name))
|
queue = append(queue, reader.ResPath(name))
|
||||||
}
|
}
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
@@ -63,10 +64,10 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
|
|||||||
}
|
}
|
||||||
name := queue[0]
|
name := queue[0]
|
||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
if seen[strings.ToLower(name)] {
|
if seen[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[strings.ToLower(name)] = true
|
seen[name] = true
|
||||||
data, ok := parts[name]
|
data, ok := parts[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -164,7 +164,11 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
|
|||||||
s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource)
|
s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource)
|
||||||
}
|
}
|
||||||
if s.objects != nil && editorObjectReference(attr.Name.Local) {
|
if s.objects != nil && editorObjectReference(attr.Name.Local) {
|
||||||
s.objects[attr.Value] = true
|
id := attr.Value
|
||||||
|
if key := editorResourceID(id); key != "" {
|
||||||
|
id = key
|
||||||
|
}
|
||||||
|
s.objects[id] = true
|
||||||
}
|
}
|
||||||
if attr.Name.Local == "Font" {
|
if attr.Name.Local == "Font" {
|
||||||
usage = s.refs.fontUsage(attr.Value)
|
usage = s.refs.fontUsage(attr.Value)
|
||||||
@@ -189,6 +193,14 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource)
|
s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource)
|
||||||
|
if len(s.stack) == 3 && s.stack[0] == "DocVersion" && s.stack[1] == "FileList" && token.Name.Local == "File" {
|
||||||
|
if name := editorResourceLocation(s.name, "", s.text.String()); name != "" {
|
||||||
|
if s.refs.versions == nil {
|
||||||
|
s.refs.versions = make(map[string]bool)
|
||||||
|
}
|
||||||
|
s.refs.versions[name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
if s.objects != nil && (token.Name.Local == "Thumbnail" || token.Name.Local == "Substitution") {
|
if s.objects != nil && (token.Name.Local == "Thumbnail" || token.Name.Local == "Substitution") {
|
||||||
if id := editorResourceID(s.text.String()); id != "" {
|
if id := editorResourceID(s.text.String()); id != "" {
|
||||||
s.objects[id] = true
|
s.objects[id] = true
|
||||||
@@ -219,6 +231,12 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
|
|||||||
func (r *editorResourceRefs) merge(other *editorResourceRefs) {
|
func (r *editorResourceRefs) merge(other *editorResourceRefs) {
|
||||||
maps.Copy(r.ids, other.ids)
|
maps.Copy(r.ids, other.ids)
|
||||||
maps.Copy(r.files, other.files)
|
maps.Copy(r.files, other.files)
|
||||||
|
if len(other.versions) != 0 {
|
||||||
|
if r.versions == nil {
|
||||||
|
r.versions = make(map[string]bool)
|
||||||
|
}
|
||||||
|
maps.Copy(r.versions, other.versions)
|
||||||
|
}
|
||||||
for id, usage := range other.fonts {
|
for id, usage := range other.fonts {
|
||||||
r.fontUsage(id).merge(usage, false)
|
r.fontUsage(id).merge(usage, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type editorResourceRefs struct {
|
|||||||
ids map[string]bool
|
ids map[string]bool
|
||||||
files map[string]bool
|
files map[string]bool
|
||||||
fonts map[string]*editorFontUsage
|
fonts map[string]*editorFontUsage
|
||||||
|
versions map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// compactSourceReferences 合并自产页面的已知引用,其他条目仍完整扫描
|
// compactSourceReferences 合并自产页面的已知引用,其他条目仍完整扫描
|
||||||
@@ -135,6 +136,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
preserved := refs.versions[cleanPackagePath(name)]
|
||||||
var patches []editorXMLPatch
|
var patches []editorXMLPatch
|
||||||
for _, group := range root.children {
|
for _, group := range root.children {
|
||||||
var members []editorXMLPatch
|
var members []editorXMLPatch
|
||||||
@@ -149,18 +151,18 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
id := editorResourceID(node.attr("ID"))
|
id := editorResourceID(node.attr("ID"))
|
||||||
keep := id == "" || refs.ids[id] || field == "MediaFile" && node.attr("Type") != "Image"
|
keep := preserved || id == "" || refs.ids[id] || field == "MediaFile" && node.attr("Type") != "Image"
|
||||||
if file := node.child(field); file != nil {
|
if file := node.child(field); file != nil {
|
||||||
var value string
|
var value string
|
||||||
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
|
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
|
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
|
||||||
alternate := strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)))
|
alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))
|
||||||
if field == "FontFile" {
|
if field == "FontFile" {
|
||||||
usage := refs.fonts[id]
|
usage := refs.fonts[id]
|
||||||
charset := node.attr("Charset")
|
charset := node.attr("Charset")
|
||||||
unsafe := id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
|
unsafe := preserved || id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
|
||||||
for _, file := range []string{location, alternate} {
|
for _, file := range []string{location, alternate} {
|
||||||
if fontFiles[file] == nil {
|
if fontFiles[file] == nil {
|
||||||
fontFiles[file] = newEditorFontUsage()
|
fontFiles[file] = newEditorFontUsage()
|
||||||
@@ -200,7 +202,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
if err := progress.report("fonts", i, len(ordered)); err != nil {
|
if err := progress.report("fonts", i, len(ordered)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
key := strings.ToLower(cleanPackagePath(name))
|
key := cleanPackagePath(name)
|
||||||
usage := fontFiles[key]
|
usage := fontFiles[key]
|
||||||
if e.backends.FontResources == nil || usage == nil || usage.unsafe || refs.files[key] || removed[key] || parts[name] != nil {
|
if e.backends.FontResources == nil || usage == nil || usage.unsafe || refs.files[key] || removed[key] || parts[name] != nil {
|
||||||
continue
|
continue
|
||||||
@@ -222,7 +224,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
}
|
}
|
||||||
maps.Copy(parts, updates)
|
maps.Copy(parts, updates)
|
||||||
for name := range parts {
|
for name := range parts {
|
||||||
if removed[strings.ToLower(cleanPackagePath(name))] {
|
if removed[cleanPackagePath(name)] {
|
||||||
delete(parts, name)
|
delete(parts, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,7 +281,7 @@ func (r *editorResourceRefs) reference(name, base, key, value string, resource b
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.files[editorResourceLocation(name, base, value)] = true
|
r.files[editorResourceLocation(name, base, value)] = true
|
||||||
r.files[strings.ToLower(cleanPackagePath(resolveResourcePath(name, base, value)))] = true
|
r.files[cleanPackagePath(resolveResourcePath(name, base, value))] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorResourceReferenceKind 区分标准标识和文件引用,其他文本不参与资源扫描
|
// editorResourceReferenceKind 区分标准标识和文件引用,其他文本不参与资源扫描
|
||||||
@@ -340,7 +342,7 @@ func editorResourceNode(root *editorXML, group, kind, id string) *editorXML {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,大小写折叠用于保守保留共享文件
|
// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,保留路径大小写
|
||||||
// 入参: name 所在XML, base 资源目录, value 路径值
|
// 入参: name 所在XML, base 资源目录, value 路径值
|
||||||
// 返回: string 包内路径
|
// 返回: string 包内路径
|
||||||
func editorResourceLocation(name, base, value string) string {
|
func editorResourceLocation(name, base, value string) string {
|
||||||
@@ -349,7 +351,7 @@ func editorResourceLocation(name, base, value string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(value, "/") {
|
if strings.HasPrefix(value, "/") {
|
||||||
return strings.ToLower(cleanPackagePath(value))
|
return cleanPackagePath(value)
|
||||||
}
|
}
|
||||||
base = strings.ReplaceAll(base, "\\", "/")
|
base = strings.ReplaceAll(base, "\\", "/")
|
||||||
dir := path.Dir(name)
|
dir := path.Dir(name)
|
||||||
@@ -358,5 +360,5 @@ func editorResourceLocation(name, base, value string) string {
|
|||||||
} else {
|
} else {
|
||||||
dir = path.Join(dir, base)
|
dir = path.Join(dir, base)
|
||||||
}
|
}
|
||||||
return strings.ToLower(cleanPackagePath(path.Join(dir, value)))
|
return cleanPackagePath(path.Join(dir, value))
|
||||||
}
|
}
|
||||||
+1
-1
@@ -49,7 +49,7 @@ func DetectFormat(source io.ReaderAt, size int64) (string, error) {
|
|||||||
if err := reader.indexPackage(); err != nil {
|
if err := reader.indexPackage(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if reader.fileIndexFold["ofd.xml"] != nil || reader.fileIndexFold["encryptions.xml"] != nil {
|
if reader.fileNames["OFD.xml"] || reader.fileNames["Encryptions.xml"] {
|
||||||
return "ofd", nil
|
return "ofd", nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -34,6 +34,24 @@ type DocBody struct {
|
|||||||
DocInfo DocInfo `xml:"DocInfo"`
|
DocInfo DocInfo `xml:"DocInfo"`
|
||||||
DocRoot string `xml:"DocRoot"`
|
DocRoot string `xml:"DocRoot"`
|
||||||
Signatures string `xml:"Signatures"`
|
Signatures string `xml:"Signatures"`
|
||||||
|
versioned bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML 读取文档入口并记录版本列表是否存在,版本描述在选择时读取
|
||||||
|
// 入参: decoder XML解码器, start 起始节点
|
||||||
|
// 返回: error 解析错误
|
||||||
|
func (body *DocBody) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
|
||||||
|
type docBody DocBody
|
||||||
|
var value struct {
|
||||||
|
docBody
|
||||||
|
Versions *struct{} `xml:"Versions"`
|
||||||
|
}
|
||||||
|
if err := decoder.DecodeElement(&value, &start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*body = DocBody(value.docBody)
|
||||||
|
body.versioned = value.Versions != nil
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocInfo 文档元数据
|
// DocInfo 文档元数据
|
||||||
|
|||||||
+5
-7
@@ -53,7 +53,7 @@ func (e *Editor) packageName(relative string) string {
|
|||||||
return packageAvailableName(reader, files, path.Join(e.packageDirectory(), relative))
|
return packageAvailableName(reader, files, path.Join(e.packageDirectory(), relative))
|
||||||
}
|
}
|
||||||
|
|
||||||
// packageAvailableName 仅在包内同名条目冲突时添加序号,忽略大小写差异
|
// packageAvailableName 仅在包内同名条目冲突时添加序号,路径区分大小写
|
||||||
// 入参: reader 原包,可为nil, files 待写入条目, name 期望路径
|
// 入参: reader 原包,可为nil, files 待写入条目, name 期望路径
|
||||||
// 返回: string 未占用路径
|
// 返回: string 未占用路径
|
||||||
func packageAvailableName(reader *Reader, files map[string][]byte, name string) string {
|
func packageAvailableName(reader *Reader, files map[string][]byte, name string) string {
|
||||||
@@ -66,13 +66,11 @@ func packageAvailableName(reader *Reader, files map[string][]byte, name string)
|
|||||||
occupied := false
|
occupied := false
|
||||||
if reader != nil {
|
if reader != nil {
|
||||||
_, occupied = reader.packageFile(candidate)
|
_, occupied = reader.packageFile(candidate)
|
||||||
for file := range reader.files {
|
_, cached := reader.files[candidate]
|
||||||
occupied = occupied || strings.EqualFold(cleanPackagePath(file), candidate)
|
occupied = occupied || cached
|
||||||
}
|
|
||||||
}
|
|
||||||
for file := range files {
|
|
||||||
occupied = occupied || strings.EqualFold(cleanPackagePath(file), candidate)
|
|
||||||
}
|
}
|
||||||
|
_, pending := files[candidate]
|
||||||
|
occupied = occupied || pending
|
||||||
if !occupied {
|
if !occupied {
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-36
@@ -48,13 +48,15 @@ type Reader struct {
|
|||||||
doc *Document
|
doc *Document
|
||||||
relatedPending bool
|
relatedPending bool
|
||||||
documentIndex int
|
documentIndex int
|
||||||
|
documentRoot string
|
||||||
|
selectedVersion *string
|
||||||
|
versionInfo *DocumentVersionInfo
|
||||||
Stamps map[string][]Stamp
|
Stamps map[string][]Stamp
|
||||||
Annots map[string][]Annotation
|
Annots map[string][]Annotation
|
||||||
annotationFiles map[string][]string
|
annotationFiles map[string][]string
|
||||||
fileIndex map[string]*zip.File
|
fileIndex map[string]*zip.File
|
||||||
fileIndexFold map[string]*zip.File
|
|
||||||
files map[string][]byte
|
files map[string][]byte
|
||||||
fileNamesFold map[string]string
|
fileNames map[string]bool
|
||||||
encryption *encryptionState
|
encryption *encryptionState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +96,11 @@ func (r *Reader) DocumentPageCount(index int) (int, error) {
|
|||||||
if index == r.documentIndex && r.doc != nil {
|
if index == r.documentIndex && r.doc != nil {
|
||||||
return len(r.doc.Pages.Page), nil
|
return len(r.doc.Pages.Page), nil
|
||||||
}
|
}
|
||||||
data, err := r.readFile(r.OFD.DocBody[index].DocRoot)
|
root, err := r.documentEntry(index)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
data, err := r.readFile(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -151,7 +157,10 @@ func (r *Reader) docStructure() (*Document, error) {
|
|||||||
return nil, fmt.Errorf("no docbody found")
|
return nil, fmt.Errorf("no docbody found")
|
||||||
}
|
}
|
||||||
docAttr := r.OFD.DocBody[r.documentIndex]
|
docAttr := r.OFD.DocBody[r.documentIndex]
|
||||||
docRootPath := docAttr.DocRoot
|
docRootPath, err := r.DocumentRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
r.RootDir = path.Dir(docRootPath)
|
r.RootDir = path.Dir(docRootPath)
|
||||||
data, err := r.readFile(docRootPath)
|
data, err := r.readFile(docRootPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -162,7 +171,7 @@ func (r *Reader) docStructure() (*Document, error) {
|
|||||||
return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err)
|
||||||
}
|
}
|
||||||
if doc.Signatures == "" {
|
if doc.Signatures == "" {
|
||||||
doc.Signatures = docAttr.Signatures
|
doc.Signatures = r.versionSignatures(docAttr.Signatures)
|
||||||
}
|
}
|
||||||
for _, res := range doc.CommonData.DocumentRes {
|
for _, res := range doc.CommonData.DocumentRes {
|
||||||
r.loadRes(res)
|
r.loadRes(res)
|
||||||
@@ -459,6 +468,11 @@ func (r *Reader) initRoot() error {
|
|||||||
return fmt.Errorf("failed to unmarshal ofd.xml: %w", err)
|
return fmt.Errorf("failed to unmarshal ofd.xml: %w", err)
|
||||||
}
|
}
|
||||||
r.OFD = &ofd
|
r.OFD = &ofd
|
||||||
|
r.doc, r.versionInfo = nil, nil
|
||||||
|
r.documentRoot, r.RootDir = "", ""
|
||||||
|
r.relatedPending, r.fontResourcesRead = false, false
|
||||||
|
r.imageCatalog = nil
|
||||||
|
r.Stamps, r.Annots, r.annotationFiles = nil, nil, nil
|
||||||
r.ResMap = make(map[string]string)
|
r.ResMap = make(map[string]string)
|
||||||
r.resourcesRead = make(map[string]bool)
|
r.resourcesRead = make(map[string]bool)
|
||||||
r.mediaCache = make(map[string]MultiMedia)
|
r.mediaCache = make(map[string]MultiMedia)
|
||||||
@@ -472,7 +486,7 @@ func (r *Reader) initRoot() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexPackage 建立包路径索引,拒绝会使显示与验签产生歧义的同名条目
|
// indexPackage 按大小写敏感路径建立包索引,拒绝规范化后重复的ZIP条目
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (r *Reader) indexPackage() error {
|
func (r *Reader) indexPackage() error {
|
||||||
count := 0
|
count := 0
|
||||||
@@ -480,8 +494,7 @@ func (r *Reader) indexPackage() error {
|
|||||||
count = len(r.Zip.File)
|
count = len(r.Zip.File)
|
||||||
}
|
}
|
||||||
r.fileIndex = make(map[string]*zip.File, count)
|
r.fileIndex = make(map[string]*zip.File, count)
|
||||||
r.fileIndexFold = make(map[string]*zip.File, count)
|
r.fileNames = make(map[string]bool, count+len(r.files))
|
||||||
r.fileNamesFold = make(map[string]string, count+len(r.files))
|
|
||||||
if r.Zip != nil {
|
if r.Zip != nil {
|
||||||
for _, f := range r.Zip.File {
|
for _, f := range r.Zip.File {
|
||||||
if f.FileInfo().IsDir() {
|
if f.FileInfo().IsDir() {
|
||||||
@@ -491,24 +504,18 @@ func (r *Reader) indexPackage() error {
|
|||||||
if !validPackagePath(name) {
|
if !validPackagePath(name) {
|
||||||
return fmt.Errorf("invalid package path: %q", f.Name)
|
return fmt.Errorf("invalid package path: %q", f.Name)
|
||||||
}
|
}
|
||||||
fold := strings.ToLower(name)
|
if previous, ok := r.fileIndex[name]; ok {
|
||||||
if previous, ok := r.fileIndexFold[fold]; ok {
|
|
||||||
return fmt.Errorf("ambiguous package paths: %q and %q", previous.Name, f.Name)
|
return fmt.Errorf("ambiguous package paths: %q and %q", previous.Name, f.Name)
|
||||||
}
|
}
|
||||||
r.fileIndex[name] = f
|
r.fileIndex[name] = f
|
||||||
r.fileIndexFold[fold] = f
|
r.fileNames[name] = true
|
||||||
r.fileNamesFold[fold] = name
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name := range r.files {
|
for name := range r.files {
|
||||||
if name != cleanPackagePath(name) || !validPackagePath(name) {
|
if name != cleanPackagePath(name) || !validPackagePath(name) {
|
||||||
return fmt.Errorf("invalid package path: %q", name)
|
return fmt.Errorf("invalid package path: %q", name)
|
||||||
}
|
}
|
||||||
fold := strings.ToLower(name)
|
r.fileNames[name] = true
|
||||||
if previous, ok := r.fileNamesFold[fold]; ok && previous != name {
|
|
||||||
return fmt.Errorf("ambiguous package paths: %q and %q", previous, name)
|
|
||||||
}
|
|
||||||
r.fileNamesFold[fold] = name
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -544,16 +551,10 @@ func (r *Reader) readFileView(name string) ([]byte, error) {
|
|||||||
// 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息
|
// 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息
|
||||||
func (r *Reader) readFileData(name string) ([]byte, bool, error) {
|
func (r *Reader) readFileData(name string) ([]byte, bool, error) {
|
||||||
name = cleanPackagePath(name)
|
name = cleanPackagePath(name)
|
||||||
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
|
|
||||||
name = actual
|
|
||||||
}
|
|
||||||
if data, ok := r.files[name]; ok {
|
if data, ok := r.files[name]; ok {
|
||||||
return data, true, nil
|
return data, true, nil
|
||||||
}
|
}
|
||||||
if f, ok := r.packageFile(name); ok {
|
if f, ok := r.packageFile(name); ok {
|
||||||
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
|
|
||||||
return data, true, nil
|
|
||||||
}
|
|
||||||
data, err := readZipFile(f)
|
data, err := readZipFile(f)
|
||||||
return data, false, err
|
return data, false, err
|
||||||
}
|
}
|
||||||
@@ -565,16 +566,10 @@ func (r *Reader) readFileData(name string) ([]byte, bool, error) {
|
|||||||
// 返回: io.ReadCloser 文件流, error 错误信息
|
// 返回: io.ReadCloser 文件流, error 错误信息
|
||||||
func (r *Reader) openFile(name string) (io.ReadCloser, error) {
|
func (r *Reader) openFile(name string) (io.ReadCloser, error) {
|
||||||
name = cleanPackagePath(name)
|
name = cleanPackagePath(name)
|
||||||
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
|
|
||||||
name = actual
|
|
||||||
}
|
|
||||||
if data, ok := r.files[name]; ok {
|
if data, ok := r.files[name]; ok {
|
||||||
return io.NopCloser(bytes.NewReader(data)), nil
|
return io.NopCloser(bytes.NewReader(data)), nil
|
||||||
}
|
}
|
||||||
if f, ok := r.packageFile(name); ok {
|
if f, ok := r.packageFile(name); ok {
|
||||||
if data, ok := r.files[cleanPackagePath(f.Name)]; ok {
|
|
||||||
return io.NopCloser(bytes.NewReader(data)), nil
|
|
||||||
}
|
|
||||||
return f.Open()
|
return f.Open()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("file not found: %s", name)
|
return nil, fmt.Errorf("file not found: %s", name)
|
||||||
@@ -593,13 +588,8 @@ func cleanPackagePath(name string) string {
|
|||||||
// 入参: name 文件路径
|
// 入参: name 文件路径
|
||||||
// 返回: *zip.File 压缩包文件, bool 是否存在
|
// 返回: *zip.File 压缩包文件, bool 是否存在
|
||||||
func (r *Reader) packageFile(name string) (*zip.File, bool) {
|
func (r *Reader) packageFile(name string) (*zip.File, bool) {
|
||||||
if f, ok := r.fileIndex[name]; ok {
|
f, ok := r.fileIndex[cleanPackagePath(name)]
|
||||||
return f, true
|
return f, ok
|
||||||
}
|
|
||||||
if f, ok := r.fileIndexFold[strings.ToLower(name)]; ok {
|
|
||||||
return f, true
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// readZipFile 读取zip文件内容
|
// readZipFile 读取zip文件内容
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
|
|
||||||
// documentFiles 按标准路径遍历独立文档依赖,签名摘要引用不作为内容依赖
|
// documentFiles 按标准路径遍历独立文档依赖,签名摘要引用不作为内容依赖
|
||||||
// 入参: ctx 取消上下文, index 文档索引
|
// 入参: ctx 取消上下文, index 文档索引
|
||||||
// 返回: map[string]bool 大小写折叠的文件路径, error 读取或取消错误
|
// 返回: map[string]bool 包内文件路径, error 读取或取消错误
|
||||||
func (r *Reader) documentFiles(ctx context.Context, index int) (map[string]bool, error) {
|
func (r *Reader) documentFiles(ctx context.Context, index int) (map[string]bool, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -67,15 +67,13 @@ func (r *Reader) documentFiles(ctx context.Context, index int) (map[string]bool,
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
name := queue[0]
|
actual := queue[0]
|
||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
key := strings.ToLower(name)
|
if seen[actual] {
|
||||||
if seen[key] {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[key] = true
|
seen[actual] = true
|
||||||
actual, exists := r.fileNamesFold[key]
|
if !r.fileNames[actual] {
|
||||||
if !exists {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
input, err := r.openFile(actual)
|
input, err := r.openFile(actual)
|
||||||
|
|||||||
+9
-20
@@ -219,7 +219,7 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath
|
|||||||
covered := make(map[string]bool)
|
covered := make(map[string]bool)
|
||||||
for _, ref := range report.References {
|
for _, ref := range report.References {
|
||||||
if ref.Checked && ref.OK {
|
if ref.Checked && ref.OK {
|
||||||
covered[r.signatureCoveragePath(ref.Path)] = true
|
covered[cleanPackagePath(ref.Path)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name := range covered {
|
for name := range covered {
|
||||||
@@ -232,12 +232,12 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath
|
|||||||
report.CoverageChecked, report.CoverageOK = true, true
|
report.CoverageChecked, report.CoverageOK = true, true
|
||||||
required := make(map[string]bool)
|
required := make(map[string]bool)
|
||||||
for _, name := range o.Policy.RequiredFiles {
|
for _, name := range o.Policy.RequiredFiles {
|
||||||
required[r.signatureCoveragePath(name)] = true
|
required[cleanPackagePath(name)] = true
|
||||||
}
|
}
|
||||||
if o.Policy.RequireDocumentCoverage {
|
if o.Policy.RequireDocumentCoverage {
|
||||||
required[r.signatureCoveragePath("OFD.xml")] = true
|
required["OFD.xml"] = true
|
||||||
excluded := map[string]bool{r.signatureCoveragePath(listPath): true}
|
excluded := map[string]bool{cleanPackagePath(listPath): true}
|
||||||
name := r.signatureCoveragePath(signatureRefPath(listPath, report.BaseLoc))
|
name := cleanPackagePath(signatureRefPath(listPath, report.BaseLoc))
|
||||||
data, err := r.readFile(name)
|
data, err := r.readFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report.CoverageOK = false
|
report.CoverageOK = false
|
||||||
@@ -252,9 +252,9 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath
|
|||||||
}
|
}
|
||||||
excluded[name] = true
|
excluded[name] = true
|
||||||
if file.SignedValue != "" {
|
if file.SignedValue != "" {
|
||||||
excluded[r.signatureCoveragePath(signatureRefPath(name, file.SignedValue))] = true
|
excluded[cleanPackagePath(signatureRefPath(name, file.SignedValue))] = true
|
||||||
}
|
}
|
||||||
root := path.Dir(r.signatureCoveragePath(o.DocRoot))
|
root := path.Dir(cleanPackagePath(o.DocRoot))
|
||||||
include := func(name string) {
|
include := func(name string) {
|
||||||
if (root == "." || strings.HasPrefix(name, root+"/")) && !excluded[name] && !strings.HasSuffix(name, "/") {
|
if (root == "." || strings.HasPrefix(name, root+"/")) && !excluded[name] && !strings.HasSuffix(name, "/") {
|
||||||
required[name] = true
|
required[name] = true
|
||||||
@@ -281,22 +281,11 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath
|
|||||||
report.CoverageOK = report.CoverageOK && len(report.UncoveredFiles) == 0
|
report.CoverageOK = report.CoverageOK && len(report.UncoveredFiles) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// signatureCoveragePath 复用阅读器大小写索引获取同一文件身份
|
|
||||||
// 入参: name 包内路径
|
|
||||||
// 返回: string 实际包内路径
|
|
||||||
func (r *Reader) signatureCoveragePath(name string) string {
|
|
||||||
name = cleanPackagePath(name)
|
|
||||||
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
|
|
||||||
return actual
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// signatureCoverageReferences 收集标准OFD引用的传递闭包且包含跨文档资源
|
// signatureCoverageReferences 收集标准OFD引用的传递闭包且包含跨文档资源
|
||||||
// 入参: root 文档入口, required 待补充的必需文件集合
|
// 入参: root 文档入口, required 待补充的必需文件集合
|
||||||
// 返回: error 引用文件无法读取或解析时的错误
|
// 返回: error 引用文件无法读取或解析时的错误
|
||||||
func (r *Reader) signatureCoverageReferences(root string, required map[string]bool) error {
|
func (r *Reader) signatureCoverageReferences(root string, required map[string]bool) error {
|
||||||
queue := []string{r.signatureCoveragePath(root)}
|
queue := []string{cleanPackagePath(root)}
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for len(queue) != 0 {
|
for len(queue) != 0 {
|
||||||
name := queue[0]
|
name := queue[0]
|
||||||
@@ -332,7 +321,7 @@ func (r *Reader) signatureCoverageReferences(root string, required map[string]bo
|
|||||||
if resource {
|
if resource {
|
||||||
resBase = base
|
resBase = base
|
||||||
}
|
}
|
||||||
resolved := r.signatureCoveragePath(resolveResourcePath(name, resBase, loc))
|
resolved := cleanPackagePath(resolveResourcePath(name, resBase, loc))
|
||||||
required[resolved] = true
|
required[resolved] = true
|
||||||
if !seen[resolved] {
|
if !seen[resolved] {
|
||||||
queue = append(queue, resolved)
|
queue = append(queue, resolved)
|
||||||
|
|||||||
+15
-8
@@ -299,11 +299,22 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := r.OFD.DocBody[index]
|
body := r.OFD.DocBody[index]
|
||||||
options.DocIndex, options.DocRoot = index, r.signatureCoveragePath(body.DocRoot)
|
|
||||||
if index == r.documentIndex && r.doc != nil && r.doc.Signatures == "" {
|
if index == r.documentIndex && r.doc != nil && r.doc.Signatures == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
data, err := r.readFile(body.DocRoot)
|
view := &Reader{Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index}
|
||||||
|
if index == r.documentIndex {
|
||||||
|
view.selectedVersion = r.selectedVersion
|
||||||
|
}
|
||||||
|
if err := view.initRoot(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
docRoot, err := view.DocumentRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
options.DocIndex, options.DocRoot = index, docRoot
|
||||||
|
data, err := view.readFile(docRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -312,16 +323,12 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if doc.Signatures == "" {
|
if doc.Signatures == "" {
|
||||||
doc.Signatures = body.Signatures
|
doc.Signatures = view.versionSignatures(body.Signatures)
|
||||||
}
|
}
|
||||||
if doc.Signatures == "" {
|
if doc.Signatures == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
view := &Reader{Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index}
|
view.doc, view.RootDir = &doc, path.Dir(docRoot)
|
||||||
if err := view.initRoot(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
view.doc, view.RootDir = &doc, path.Dir(body.DocRoot)
|
|
||||||
sigListPath := view.ResPath(doc.Signatures)
|
sigListPath := view.ResPath(doc.Signatures)
|
||||||
data, err = view.readFile(sigListPath)
|
data, err = view.readFile(sigListPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+10
-11
@@ -78,6 +78,7 @@ type signatureWriteInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SignPackage 签署最终OFD包,失败时返回nil且不修改输入
|
// SignPackage 签署最终OFD包,失败时返回nil且不修改输入
|
||||||
|
// 含版本列表的文档需先通过ExtractVersion提取后签署
|
||||||
// 入参: data 最终包字节, options 签署选项
|
// 入参: data 最终包字节, options 签署选项
|
||||||
// 返回: []byte 签署后的完整包, error 错误信息
|
// 返回: []byte 签署后的完整包, error 错误信息
|
||||||
func SignPackage(data []byte, options SignatureWriteOptions) ([]byte, error) {
|
func SignPackage(data []byte, options SignatureWriteOptions) ([]byte, error) {
|
||||||
@@ -89,6 +90,7 @@ func SignPackage(data []byte, options SignatureWriteOptions) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SignEditorTo 将编辑器序列化为最终文件后签署,保留或显式覆盖加密策略
|
// SignEditorTo 将编辑器序列化为最终文件后签署,保留或显式覆盖加密策略
|
||||||
|
// 含版本列表的文档需先通过ExtractVersion提取后签署
|
||||||
// 不修改编辑器状态;OnWriteProgress可在准备、签署和加密阶段取消,准备失败不写入目标
|
// 不修改编辑器状态;OnWriteProgress可在准备、签署和加密阶段取消,准备失败不写入目标
|
||||||
// 输出目标不得覆盖源文件;目标自身写入失败时调用方应丢弃部分输出
|
// 输出目标不得覆盖源文件;目标自身写入失败时调用方应丢弃部分输出
|
||||||
// 入参: writer 输出目标, editor 编辑器, options 签署选项
|
// 入参: writer 输出目标, editor 编辑器, options 签署选项
|
||||||
@@ -121,6 +123,7 @@ func SignEditorTo(writer io.Writer, editor *Editor, options SignatureWriteOption
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SignTo 从阅读器包内容创建独立快照,完成签署和回验后写出
|
// SignTo 从阅读器包内容创建独立快照,完成签署和回验后写出
|
||||||
|
// 含版本列表的文档需先通过ExtractVersion提取后签署
|
||||||
// 不采用可变的Doc/OFD模型;调用方应先将Editor写出为最终包
|
// 不采用可变的Doc/OFD模型;调用方应先将Editor写出为最终包
|
||||||
// 准备失败不写入writer;writer自身失败可能已写入部分输出,不保证目标原子替换
|
// 准备失败不写入writer;writer自身失败可能已写入部分输出,不保证目标原子替换
|
||||||
// 入参: writer 独立的输出目标,不得覆盖源文件, options 签署选项
|
// 入参: writer 独立的输出目标,不得覆盖源文件, options 签署选项
|
||||||
@@ -147,6 +150,9 @@ func signatureWriteOutput(r *Reader, options SignatureWriteOptions) ([]byte, err
|
|||||||
if r == nil {
|
if r == nil {
|
||||||
return nil, fmt.Errorf("nil signature reader")
|
return nil, fmt.Errorf("nil signature reader")
|
||||||
}
|
}
|
||||||
|
if r.documentIndex >= 0 && r.documentIndex < r.DocumentCount() && r.OFD.DocBody[r.documentIndex].versioned {
|
||||||
|
return nil, fmt.Errorf("in-package version signing is not supported; extract the version before signing")
|
||||||
|
}
|
||||||
encryption := options.Encryption
|
encryption := options.Encryption
|
||||||
if encryption == nil && r.encryption != nil {
|
if encryption == nil && r.encryption != nil {
|
||||||
encryption = r.encryption.options
|
encryption = r.encryption.options
|
||||||
@@ -275,7 +281,7 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for name := range parts {
|
for name := range parts {
|
||||||
if removed[strings.ToLower(cleanPackagePath(name))] {
|
if removed[cleanPackagePath(name)] {
|
||||||
delete(parts, name)
|
delete(parts, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,7 +370,7 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
|
|||||||
directory = path.Join(path.Dir(listPath), "Sign_"+strconv.Itoa(n))
|
directory = path.Join(path.Dir(listPath), "Sign_"+strconv.Itoa(n))
|
||||||
occupied := false
|
occupied := false
|
||||||
for name := range parts {
|
for name := range parts {
|
||||||
if strings.EqualFold(name, directory) || strings.HasPrefix(strings.ToLower(name), strings.ToLower(directory)+"/") {
|
if name == directory || strings.HasPrefix(name, directory+"/") {
|
||||||
occupied = true
|
occupied = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -474,7 +480,7 @@ func signatureWriteIDs(list *Signatures, listPath string, parts map[string][]byt
|
|||||||
var sequence signatureIDSequence
|
var sequence signatureIDSequence
|
||||||
sequence.observe(list.MaxSignID)
|
sequence.observe(list.MaxSignID)
|
||||||
add := func(id string) error {
|
add := func(id string) error {
|
||||||
if !signatureWriteIDValid(id) || used[id] {
|
if !ofdXMLIDValid(id) || used[id] {
|
||||||
return fmt.Errorf("invalid or duplicate signature ID: %s", id)
|
return fmt.Errorf("invalid or duplicate signature ID: %s", id)
|
||||||
}
|
}
|
||||||
used[id] = true
|
used[id] = true
|
||||||
@@ -534,14 +540,7 @@ func signatureWriteXML(name string, value any) ([]byte, error) {
|
|||||||
func signatureWriteAvailable(parts map[string][]byte, name string) string {
|
func signatureWriteAvailable(parts map[string][]byte, name string) string {
|
||||||
original := name
|
original := name
|
||||||
for i := 1; ; i++ {
|
for i := 1; ; i++ {
|
||||||
found := false
|
if _, found := parts[name]; !found {
|
||||||
for existing := range parts {
|
|
||||||
if strings.EqualFold(existing, name) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
name = path.Join(path.Dir(original), strconv.Itoa(i)+"_"+path.Base(original))
|
name = path.Join(path.Dir(original), strconv.Itoa(i)+"_"+path.Base(original))
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"unicode/utf8"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SignaturePageStamps 生成所选页面相同毫米坐标的印章位置并检查页内范围
|
// SignaturePageStamps 生成所选页面相同毫米坐标的印章位置并检查页内范围
|
||||||
@@ -110,7 +109,7 @@ func signatureWriteStamps(r *Reader, stamps []SignatureStamp, seal bool) ([]Sign
|
|||||||
if stamp.ID == "" {
|
if stamp.ID == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !signatureWriteIDValid(stamp.ID) || ids[stamp.ID] {
|
if !ofdXMLIDValid(stamp.ID) || ids[stamp.ID] {
|
||||||
return nil, fmt.Errorf("invalid or duplicate stamp ID: %s", stamp.ID)
|
return nil, fmt.Errorf("invalid or duplicate stamp ID: %s", stamp.ID)
|
||||||
}
|
}
|
||||||
ids[stamp.ID] = true
|
ids[stamp.ID] = true
|
||||||
@@ -142,31 +141,6 @@ func signatureWriteStamps(r *Reader, stamps []SignatureStamp, seal bool) ([]Sign
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// signatureWriteIDValid 按XML 1.0的NCName规则校验签章标识
|
|
||||||
// 入参: id 标识
|
|
||||||
// 返回: bool 是否有效
|
|
||||||
func signatureWriteIDValid(id string) bool {
|
|
||||||
if id == "" || !utf8.ValidString(id) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i, c := range id {
|
|
||||||
if c == '_' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' ||
|
|
||||||
c >= 0xC0 && c <= 0xD6 || c >= 0xD8 && c <= 0xF6 ||
|
|
||||||
c >= 0xF8 && c <= 0x2FF || c >= 0x370 && c <= 0x37D ||
|
|
||||||
c >= 0x37F && c <= 0x1FFF || c >= 0x200C && c <= 0x200D ||
|
|
||||||
c >= 0x2070 && c <= 0x218F || c >= 0x2C00 && c <= 0x2FEF ||
|
|
||||||
c >= 0x3001 && c <= 0xD7FF || c >= 0xF900 && c <= 0xFDCF ||
|
|
||||||
c >= 0xFDF0 && c <= 0xFFFD || c >= 0x10000 && c <= 0xEFFFF {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if i == 0 || !(c == '-' || c == '.' || c >= '0' && c <= '9' ||
|
|
||||||
c == 0xB7 || c >= 0x300 && c <= 0x36F || c >= 0x203F && c <= 0x2040) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// signatureWriteBoxValid 判断印章区域是否有限且尺寸为正
|
// signatureWriteBoxValid 判断印章区域是否有限且尺寸为正
|
||||||
// 入参: box 区域
|
// 入参: box 区域
|
||||||
// 返回: bool 是否有效
|
// 返回: bool 是否有效
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DocumentVersionInfo 文档版本信息,Location、DocRoot及Files中的路径均相对于包根目录
|
||||||
|
// Index为版本号,Version为该版本适用的格式版本,CreationDate保留原始日期文本
|
||||||
|
type DocumentVersionInfo struct {
|
||||||
|
ID string
|
||||||
|
Index int32
|
||||||
|
Current bool
|
||||||
|
Location string
|
||||||
|
Version string
|
||||||
|
Name string
|
||||||
|
CreationDate string
|
||||||
|
DocRoot string
|
||||||
|
Files []VersionFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionFile 文档版本中的文件标识与包内路径
|
||||||
|
type VersionFile struct {
|
||||||
|
ID string
|
||||||
|
Location string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentVersions 按入口顺序读取当前文档的版本信息,不切换文档或读取文件清单中的内容
|
||||||
|
// 返回独立副本;没有版本列表时返回nil,不以版本号推测默认版本
|
||||||
|
// 返回: []DocumentVersionInfo 版本信息, error 读取或描述错误
|
||||||
|
func (r *Reader) DocumentVersions() ([]DocumentVersionInfo, error) {
|
||||||
|
entries, err := r.documentVersionEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i, entry := range entries {
|
||||||
|
info, err := r.readDocumentVersion(entry.ID, entry.Location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("version %s: %w", entry.ID, err)
|
||||||
|
}
|
||||||
|
info.Index, info.Current = entry.Index, entry.Current
|
||||||
|
entries[i] = info
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentVersionEntries 读取当前文档的版本入口,不提前加载各版本描述
|
||||||
|
// 返回: []DocumentVersionInfo 入口属性, error 读取或描述错误
|
||||||
|
func (r *Reader) documentVersionEntries() ([]DocumentVersionInfo, error) {
|
||||||
|
if r.documentIndex < 0 || r.documentIndex >= r.DocumentCount() {
|
||||||
|
return nil, fmt.Errorf("no docbody found")
|
||||||
|
}
|
||||||
|
data, err := r.readFile("OFD.xml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body := root.childAt("DocBody", r.documentIndex)
|
||||||
|
if body == nil {
|
||||||
|
return nil, fmt.Errorf("document body not found: %d", r.documentIndex)
|
||||||
|
}
|
||||||
|
versions := body.child("Versions")
|
||||||
|
if versions == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if body.childAt("Versions", 1) != nil {
|
||||||
|
return nil, fmt.Errorf("multiple version lists")
|
||||||
|
}
|
||||||
|
var result []DocumentVersionInfo
|
||||||
|
ids := make(map[string]bool)
|
||||||
|
for _, node := range versions.children {
|
||||||
|
if !node.matchesOFD("Version") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := strings.TrimSpace(node.attr("ID"))
|
||||||
|
if !ofdXMLIDValid(id) || ids[id] {
|
||||||
|
return nil, fmt.Errorf("invalid or duplicate version ID %q", id)
|
||||||
|
}
|
||||||
|
ids[id] = true
|
||||||
|
index, err := strconv.ParseInt(strings.TrimSpace(node.attr("Index")), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid version Index for %s: %w", id, err)
|
||||||
|
}
|
||||||
|
current := false
|
||||||
|
if node.hasAttr("Current") {
|
||||||
|
switch strings.TrimSpace(node.attr("Current")) {
|
||||||
|
case "true", "1":
|
||||||
|
current = true
|
||||||
|
case "false", "0":
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid version Current for %s", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
location, err := versionFileLocation("OFD.xml", node.attr("BaseLoc"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("version %s: %w", id, err)
|
||||||
|
}
|
||||||
|
result = append(result, DocumentVersionInfo{ID: id, Index: int32(index), Current: current, Location: location})
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty version list")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readDocumentVersion 读取版本描述,按描述文件位置解析文件清单和文档入口
|
||||||
|
// 入参: id 入口中的版本标识, location 描述文件路径
|
||||||
|
// 返回: DocumentVersionInfo 版本信息, error 读取或描述错误
|
||||||
|
func (r *Reader) readDocumentVersion(id, location string) (DocumentVersionInfo, error) {
|
||||||
|
var info DocumentVersionInfo
|
||||||
|
data, err := r.readFile(location)
|
||||||
|
if err != nil {
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
if root.name.Local != "DocVersion" || strings.TrimSpace(root.attr("ID")) != id {
|
||||||
|
return info, fmt.Errorf("invalid version descriptor %s", location)
|
||||||
|
}
|
||||||
|
list, docRoot := root.child("FileList"), root.child("DocRoot")
|
||||||
|
if list == nil || docRoot == nil {
|
||||||
|
return info, fmt.Errorf("missing version FileList or DocRoot in %s", location)
|
||||||
|
}
|
||||||
|
if root.childAt("FileList", 1) != nil || root.childAt("DocRoot", 1) != nil {
|
||||||
|
return info, fmt.Errorf("duplicate version FileList or DocRoot in %s", location)
|
||||||
|
}
|
||||||
|
entry, err := versionFileLocation(location, editorImportText(data, docRoot))
|
||||||
|
if err != nil {
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
info = DocumentVersionInfo{ID: id, Location: location, Version: root.attr("Version"), Name: root.attr("Name"), CreationDate: root.attr("CreationDate"), DocRoot: entry}
|
||||||
|
ids := map[string]bool{id: true}
|
||||||
|
found := false
|
||||||
|
for _, node := range list.children {
|
||||||
|
if !node.matchesOFD("File") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fileID := strings.TrimSpace(node.attr("ID"))
|
||||||
|
if !ofdXMLIDValid(fileID) || ids[fileID] {
|
||||||
|
return DocumentVersionInfo{}, fmt.Errorf("invalid or duplicate version file ID %q", fileID)
|
||||||
|
}
|
||||||
|
ids[fileID] = true
|
||||||
|
name, err := versionFileLocation(location, editorImportText(data, node))
|
||||||
|
if err != nil {
|
||||||
|
return DocumentVersionInfo{}, err
|
||||||
|
}
|
||||||
|
found = found || name == entry
|
||||||
|
info.Files = append(info.Files, VersionFile{ID: fileID, Location: name})
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return DocumentVersionInfo{}, fmt.Errorf("version DocRoot absent from FileList: %s", entry)
|
||||||
|
}
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionFileLocation 解析版本描述中的ST_Loc,拒绝空路径和包外路径
|
||||||
|
// 入参: owner 所在XML路径, value 引用路径
|
||||||
|
// 返回: string 包内路径, error 路径错误
|
||||||
|
func versionFileLocation(owner, value string) (string, error) {
|
||||||
|
name := resolveResourcePath(owner, "", value)
|
||||||
|
if name == "" || !validPackagePath(name) {
|
||||||
|
return "", fmt.Errorf("invalid version file location %q", value)
|
||||||
|
}
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
// 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"
|
||||||
|
"maps"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// versionEditFile 保存版本内容XML及标准文件引用,不解析字体、图片或附件数据
|
||||||
|
type versionEditFile struct {
|
||||||
|
data []byte
|
||||||
|
links []versionEditLink
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionEditLink 记录文件引用的位置与包内目标
|
||||||
|
type versionEditLink struct {
|
||||||
|
node *editorXML
|
||||||
|
attribute string
|
||||||
|
target string
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionEditGraph 保存一个入口的结构文件与全部直接、间接文件依赖
|
||||||
|
type versionEditGraph struct {
|
||||||
|
files map[string]*versionEditFile
|
||||||
|
used map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionEditGraph 沿标准文件引用建立版本依赖图,二进制资源只记录路径
|
||||||
|
// 入参: ctx 取消上下文, entry 文档入口, parts 待修改条目
|
||||||
|
// 返回: *versionEditGraph 依赖图, error 读取或结构错误
|
||||||
|
func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[string][]byte) (*versionEditGraph, error) {
|
||||||
|
graph := &versionEditGraph{files: make(map[string]*versionEditFile), used: make(map[string]bool)}
|
||||||
|
queue := []string{entry}
|
||||||
|
for len(queue) != 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
if graph.files[name] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, ok := parts[name]
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
data, err = r.readFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("version file %s: %w", name, err)
|
||||||
|
}
|
||||||
|
file := &versionEditFile{data: data}
|
||||||
|
graph.files[name], graph.used[name] = file, true
|
||||||
|
base := ""
|
||||||
|
if root.name.Local == "Res" {
|
||||||
|
base = root.attr("BaseLoc")
|
||||||
|
}
|
||||||
|
var visit func(*editorXML)
|
||||||
|
visit = func(node *editorXML) {
|
||||||
|
if node != root && !node.matchesOFD(node.name.Local) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
add := func(attribute, value string, structure bool) {
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(root)
|
||||||
|
}
|
||||||
|
return graph, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionSharedFiles 汇总其他版本、主入口及其他文档依赖,保护共享条目不被覆盖
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: map[string]bool 受保护路径, error 读取或结构错误
|
||||||
|
func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error) {
|
||||||
|
shared := make(map[string]bool)
|
||||||
|
data, err := r.readFile("OFD.xml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body := root.childAt("DocBody", r.documentIndex)
|
||||||
|
if info := body.child("DocInfo"); info != nil {
|
||||||
|
if cover := info.child("Cover"); cover != nil {
|
||||||
|
name, err := versionFileLocation("OFD.xml", editorImportText(data, cover))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
shared[name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index := range r.DocumentCount() {
|
||||||
|
if index == r.documentIndex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files, err := r.documentFiles(ctx, index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
maps.Copy(shared, files)
|
||||||
|
}
|
||||||
|
versions, err := r.DocumentVersions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, version := range versions {
|
||||||
|
if r.versionInfo != nil && version.ID == r.versionInfo.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
shared[version.Location] = true
|
||||||
|
for _, file := range version.Files {
|
||||||
|
shared[file.Location] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.versionInfo != nil {
|
||||||
|
entry, err := versionFileLocation("OFD.xml", r.OFD.DocBody[r.documentIndex].DocRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
graph, err := r.versionEditGraph(ctx, entry, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
maps.Copy(shared, graph.used)
|
||||||
|
}
|
||||||
|
return shared, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionChanges 隔离当前入口修改涉及的共享文件,同步版本清单,不修改输入包或修改集
|
||||||
|
// 入参: ctx 取消上下文, changes 待写入条目
|
||||||
|
// 返回: map[string][]byte 隔离后的修改集, error 读取、引用或结构错误
|
||||||
|
func (r *Reader) versionChanges(ctx context.Context, changes map[string][]byte) (map[string][]byte, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entry, err := r.DocumentRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !r.OFD.DocBody[r.documentIndex].versioned || len(changes) == 0 {
|
||||||
|
return changes, ctx.Err()
|
||||||
|
}
|
||||||
|
parts := maps.Clone(changes)
|
||||||
|
for name, data := range parts {
|
||||||
|
if original, err := r.readFileView(name); err == nil && bytes.Equal(original, data) {
|
||||||
|
delete(parts, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return parts, ctx.Err()
|
||||||
|
}
|
||||||
|
shared, err := r.versionSharedFiles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
graph, err := r.versionEditGraph(ctx, entry, parts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
owned := maps.Clone(graph.used)
|
||||||
|
if r.versionInfo != nil {
|
||||||
|
for _, file := range r.versionInfo.Files {
|
||||||
|
owned[file.Location] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths := make(map[string]string)
|
||||||
|
reserved := maps.Clone(parts)
|
||||||
|
var pending []string
|
||||||
|
allocate := func(name string) {
|
||||||
|
extension := path.Ext(name)
|
||||||
|
target := packageAvailableName(r, reserved, strings.TrimSuffix(name, extension)+".edit"+extension)
|
||||||
|
paths[name], reserved[target] = target, nil
|
||||||
|
pending = append(pending, name)
|
||||||
|
}
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(parts)) {
|
||||||
|
if name != "OFD.xml" && shared[name] && owned[name] {
|
||||||
|
allocate(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parents := make(map[string][]string)
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(graph.files)) {
|
||||||
|
for _, link := range graph.files[name].links {
|
||||||
|
parents[link.target] = append(parents[link.target], name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i < len(pending); i++ {
|
||||||
|
for _, name := range parents[pending[i]] {
|
||||||
|
if shared[name] && paths[name] == "" {
|
||||||
|
allocate(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, file := range graph.files {
|
||||||
|
data, err := file.rewrite(paths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, file.data) {
|
||||||
|
parts[name] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for source, target := range paths {
|
||||||
|
data, ok := parts[source]
|
||||||
|
if !ok {
|
||||||
|
data, err = r.readFile(source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts[target] = data
|
||||||
|
delete(parts, source)
|
||||||
|
}
|
||||||
|
if r.versionInfo != nil {
|
||||||
|
if err := r.versionManifest(parts, paths, graph.used, shared); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if target := paths[entry]; target != "" {
|
||||||
|
data, ok := parts["OFD.xml"]
|
||||||
|
if !ok {
|
||||||
|
data, err = r.readFile("OFD.xml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
node := root.childAt("DocBody", r.documentIndex).child("DocRoot")
|
||||||
|
parts["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, editorFontXMLText("/"+target))})
|
||||||
|
}
|
||||||
|
return parts, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准
|
||||||
|
// 入参: paths 原路径到新路径的映射
|
||||||
|
// 返回: []byte 更新后的XML, error 属性编码错误
|
||||||
|
func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
headers := make(map[*editorXML][]versionEditLink)
|
||||||
|
for _, link := range file.links {
|
||||||
|
if paths[link.target] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if link.attribute == "" {
|
||||||
|
patches = append(patches, editorXMLContent(file.data, link.node, editorFontXMLText("/"+paths[link.target])))
|
||||||
|
} else {
|
||||||
|
headers[link.node] = append(headers[link.node], link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for node, links := range headers {
|
||||||
|
data := file.data[node.start:node.end]
|
||||||
|
for _, link := range links {
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err = editorXMLAttribute(data, root, link.attribute, "/"+paths[link.target])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if node.open == node.end {
|
||||||
|
patches = append(patches, editorXMLPatch{node.start, node.end, data})
|
||||||
|
} else {
|
||||||
|
patches = append(patches, editorXMLPatch{node.start, node.open, data[:root.open]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return editorPatchXML(file.data, patches), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionManifest 更新所选版本的入口与文件清单,保留已有文件标识及扩展字段
|
||||||
|
// 入参: parts 输出条目, paths 隔离路径, used 当前内容依赖, shared 其他入口依赖
|
||||||
|
// 返回: error 描述读取或XML编码错误
|
||||||
|
func (r *Reader) versionManifest(parts map[string][]byte, paths map[string]string, used, shared map[string]bool) error {
|
||||||
|
version := r.versionInfo
|
||||||
|
data, err := r.readFile(version.Location)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
list := root.child("FileList")
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
listed := make(map[string]bool)
|
||||||
|
ids := map[string]bool{version.ID: true}
|
||||||
|
for _, node := range list.children {
|
||||||
|
if !node.matchesOFD("File") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids[strings.TrimSpace(node.attr("ID"))] = true
|
||||||
|
name, err := versionFileLocation(version.Location, editorImportText(data, node))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if target := paths[name]; target != "" {
|
||||||
|
name = target
|
||||||
|
patches = append(patches, editorXMLContent(data, node, editorFontXMLText("/"+target)))
|
||||||
|
}
|
||||||
|
listed[name] = true
|
||||||
|
}
|
||||||
|
var added []byte
|
||||||
|
serial := 0
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(used)) {
|
||||||
|
if target := paths[name]; target != "" {
|
||||||
|
name = target
|
||||||
|
}
|
||||||
|
if listed[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := ""
|
||||||
|
for id == "" || ids[id] {
|
||||||
|
serial++
|
||||||
|
id = "file" + strconv.Itoa(serial)
|
||||||
|
}
|
||||||
|
ids[id], listed[name] = true, true
|
||||||
|
item, err := list.containerXML("File", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}}, editorFontXMLText("/"+name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
added = append(added, bytes.TrimPrefix(item, []byte(xml.Header))...)
|
||||||
|
}
|
||||||
|
if len(added) != 0 {
|
||||||
|
patches = append(patches, editorXMLPatch{list.close, list.close, added})
|
||||||
|
}
|
||||||
|
if target := paths[version.DocRoot]; target != "" {
|
||||||
|
patches = append(patches, editorXMLContent(data, root.child("DocRoot"), editorFontXMLText("/"+target)))
|
||||||
|
}
|
||||||
|
updated := editorPatchXML(data, patches)
|
||||||
|
if bytes.Equal(updated, data) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name := version.Location
|
||||||
|
if shared[name] {
|
||||||
|
extension := path.Ext(name)
|
||||||
|
name = packageAvailableName(r, parts, strings.TrimSuffix(name, extension)+".edit"+extension)
|
||||||
|
entry, ok := parts["OFD.xml"]
|
||||||
|
if !ok {
|
||||||
|
entry, err = r.readFile("OFD.xml")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, node := range root.childAt("DocBody", r.documentIndex).child("Versions").children {
|
||||||
|
if node.matchesOFD("Version") && node.attr("ID") == version.ID {
|
||||||
|
fragment, err := editorXMLAttribute(entry, node, "BaseLoc", "/"+name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parts["OFD.xml"] = editorPatchXML(entry, []editorXMLPatch{{node.start, node.end, fragment}})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts[name] = updated
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// 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"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtractVersion 将当前文档的指定版本提取为独立阅读器,可继续编辑或另存,不改变原包
|
||||||
|
// 仅保留版本文件清单中的文件并重建单文档入口,不保留版本列表;原签名可能因入口变化而失效
|
||||||
|
// 文件内容按需读取,使用期间不得关闭持有输入文件的原阅读器;返回值不关闭输入文件
|
||||||
|
// 入参: id 版本标识,空值选择唯一的Current版本,没有默认版本或存在歧义时返回错误
|
||||||
|
// 返回: *Reader 版本阅读器, error 选择、读取或文件缺失错误
|
||||||
|
func (r *Reader) ExtractVersion(id string) (*Reader, error) {
|
||||||
|
entries, err := r.documentVersionEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var selected *DocumentVersionInfo
|
||||||
|
for i := range entries {
|
||||||
|
entry := &entries[i]
|
||||||
|
if id != "" && entry.ID == id || id == "" && entry.Current {
|
||||||
|
if selected != nil {
|
||||||
|
return nil, fmt.Errorf("multiple current document versions")
|
||||||
|
}
|
||||||
|
selected = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
if id == "" {
|
||||||
|
return nil, fmt.Errorf("no current document version")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("document version not found: %s", id)
|
||||||
|
}
|
||||||
|
version, err := r.readDocumentVersion(selected.ID, selected.Location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("version %s: %w", selected.ID, err)
|
||||||
|
}
|
||||||
|
files := make(map[string]bool, len(version.Files))
|
||||||
|
for _, file := range version.Files {
|
||||||
|
if !r.fileNames[file.Location] {
|
||||||
|
return nil, fmt.Errorf("version %s file not found: %s", selected.ID, file.Location)
|
||||||
|
}
|
||||||
|
files[file.Location] = true
|
||||||
|
}
|
||||||
|
root, err := r.extractedVersionRoot(version, files)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
next := &Reader{Path: r.Path, files: map[string][]byte{"OFD.xml": root}, encryption: r.encryption}
|
||||||
|
for name := range files {
|
||||||
|
if name != "OFD.xml" {
|
||||||
|
if data, ok := r.files[name]; ok {
|
||||||
|
next.files[name] = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.Zip != nil {
|
||||||
|
next.Zip = &zip.Reader{Comment: r.Zip.Comment}
|
||||||
|
for _, file := range r.Zip.File {
|
||||||
|
name := cleanPackagePath(file.Name)
|
||||||
|
if name != "OFD.xml" && files[name] && !file.FileInfo().IsDir() {
|
||||||
|
next.Zip.File = append(next.Zip.File, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := next.initRoot(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := next.docStructure(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractedVersionRoot 重建版本副本入口,保留元数据及命名空间,移除其他文档与版本列表
|
||||||
|
// 入参: version 版本描述, files 版本包含的文件
|
||||||
|
// 返回: []byte 主入口XML, error 读取或结构错误
|
||||||
|
func (r *Reader) extractedVersionRoot(version DocumentVersionInfo, files map[string]bool) ([]byte, error) {
|
||||||
|
data, err := r.readFile("OFD.xml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body := root.childAt("DocBody", r.documentIndex)
|
||||||
|
if body == nil || body.child("DocRoot") == nil {
|
||||||
|
return nil, fmt.Errorf("document entry not found: %d", r.documentIndex)
|
||||||
|
}
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
for _, child := range root.children {
|
||||||
|
if child.matchesOFD("DocBody") && child != body {
|
||||||
|
patches = append(patches, editorXMLPatch{start: child.start, end: child.end})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry := body.child("DocRoot")
|
||||||
|
patches = append(patches, editorXMLContent(data, entry, editorFontXMLText(version.DocRoot)))
|
||||||
|
if versions := body.child("Versions"); versions != nil {
|
||||||
|
patches = append(patches, editorXMLPatch{start: versions.start, end: versions.end})
|
||||||
|
}
|
||||||
|
references := []*editorXML{body.child("Signatures")}
|
||||||
|
if info := body.child("DocInfo"); info != nil {
|
||||||
|
references = append(references, info.child("Cover"))
|
||||||
|
}
|
||||||
|
for _, node := range references {
|
||||||
|
if node == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := resolveResourcePath("OFD.xml", "", editorImportText(data, node))
|
||||||
|
if files[name] {
|
||||||
|
patches = append(patches, editorXMLContent(data, node, editorFontXMLText("/"+name)))
|
||||||
|
} else {
|
||||||
|
patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = editorPatchXML(data, patches)
|
||||||
|
if version.Version != "" {
|
||||||
|
root, err = parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err = editorXMLAttribute(data, root, "Version", version.Version)
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// 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 "fmt"
|
||||||
|
|
||||||
|
// DocumentRoot 获取当前阅读入口的包内路径,不改写OFD中的主入口
|
||||||
|
// 未显式选择时使用唯一的Current版本;没有默认版本时使用主入口,多个默认版本返回错误
|
||||||
|
// 返回: string 阅读入口, error 版本选择或描述错误
|
||||||
|
func (r *Reader) DocumentRoot() (string, error) {
|
||||||
|
if r.documentRoot != "" {
|
||||||
|
return r.documentRoot, nil
|
||||||
|
}
|
||||||
|
if r.documentIndex < 0 || r.documentIndex >= r.DocumentCount() {
|
||||||
|
return "", fmt.Errorf("no docbody found")
|
||||||
|
}
|
||||||
|
body := r.OFD.DocBody[r.documentIndex]
|
||||||
|
if r.selectedVersion != nil && *r.selectedVersion == "" || !body.versioned && r.selectedVersion == nil {
|
||||||
|
root, err := versionFileLocation("OFD.xml", body.DocRoot)
|
||||||
|
if err == nil {
|
||||||
|
r.documentRoot = root
|
||||||
|
}
|
||||||
|
return root, err
|
||||||
|
}
|
||||||
|
entries, err := r.documentVersionEntries()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var selected *DocumentVersionInfo
|
||||||
|
for i := range entries {
|
||||||
|
entry := &entries[i]
|
||||||
|
if r.selectedVersion != nil && entry.ID == *r.selectedVersion || r.selectedVersion == nil && entry.Current {
|
||||||
|
if selected != nil {
|
||||||
|
return "", fmt.Errorf("multiple current document versions")
|
||||||
|
}
|
||||||
|
selected = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if selected == nil {
|
||||||
|
if r.selectedVersion != nil {
|
||||||
|
return "", fmt.Errorf("document version not found: %s", *r.selectedVersion)
|
||||||
|
}
|
||||||
|
root, err := versionFileLocation("OFD.xml", body.DocRoot)
|
||||||
|
if err == nil {
|
||||||
|
r.documentRoot = root
|
||||||
|
}
|
||||||
|
return root, err
|
||||||
|
}
|
||||||
|
info, err := r.readDocumentVersion(selected.ID, selected.Location)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("version %s: %w", selected.ID, err)
|
||||||
|
}
|
||||||
|
info.Index, info.Current = selected.Index, selected.Current
|
||||||
|
r.documentRoot, r.versionInfo = info.DocRoot, &info
|
||||||
|
return r.documentRoot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentVersionID 获取当前阅读的版本标识,主入口返回空字符串
|
||||||
|
// 返回: string 版本标识, error 版本选择或描述错误
|
||||||
|
func (r *Reader) DocumentVersionID() (string, error) {
|
||||||
|
if _, err := r.DocumentRoot(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if r.versionInfo == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return r.versionInfo.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentVersion 创建指定版本的独立阅读器,保留整个包,不改变当前阅读器或共享可变缓存
|
||||||
|
// 返回值不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器
|
||||||
|
// 入参: id 版本标识,空值显式选择主入口;Document可重新按Current选择默认版本
|
||||||
|
// 返回: *Reader 版本阅读器, error 选择或读取错误
|
||||||
|
func (r *Reader) DocumentVersion(id string) (*Reader, error) {
|
||||||
|
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: r.documentIndex, selectedVersion: &id}
|
||||||
|
if err := next.initRoot(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := next.Doc(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentEntry 获取指定文档的阅读入口,当前文档保留显式选择,其他文档使用默认版本
|
||||||
|
// 入参: index 文档索引
|
||||||
|
// 返回: string 包内路径, error 选择错误
|
||||||
|
func (r *Reader) documentEntry(index int) (string, error) {
|
||||||
|
if index == r.documentIndex {
|
||||||
|
return r.DocumentRoot()
|
||||||
|
}
|
||||||
|
view := *r
|
||||||
|
view.documentIndex, view.documentRoot = index, ""
|
||||||
|
view.selectedVersion, view.versionInfo = nil, nil
|
||||||
|
return view.DocumentRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionSignatures 按OFD入口解析包级签名路径,选中版本仅使用文件清单内的签名入口
|
||||||
|
// 入参: location OFD入口中的签名路径
|
||||||
|
// 返回: string 签名路径,不属于该版本时为空
|
||||||
|
func (r *Reader) versionSignatures(location string) string {
|
||||||
|
if location == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
name := resolveResourcePath("OFD.xml", "", location)
|
||||||
|
if r.versionInfo == nil {
|
||||||
|
return "/" + name
|
||||||
|
}
|
||||||
|
for _, file := range r.versionInfo.Files {
|
||||||
|
if file.Location == name {
|
||||||
|
return "/" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
+1
-1
@@ -190,7 +190,7 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
for _, name := range slices.Sorted(maps.Keys(parts)) {
|
for _, name := range slices.Sorted(maps.Keys(parts)) {
|
||||||
if removed[strings.ToLower(cleanPackagePath(name))] {
|
if removed[cleanPackagePath(name)] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := e.writeOutputEntry(archive, zip.FileHeader{Name: name, Method: zip.Deflate}, parts[name]); err != nil {
|
if err := e.writeOutputEntry(archive, zip.FileHeader{Name: name, Method: zip.Deflate}, parts[name]); err != nil {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
|
import "unicode/utf8"
|
||||||
|
|
||||||
// 旧版与2016版OFD命名空间
|
// 旧版与2016版OFD命名空间
|
||||||
const (
|
const (
|
||||||
ofdNamespaceLegacy = "http://www.ofdspec.org"
|
ofdNamespaceLegacy = "http://www.ofdspec.org"
|
||||||
@@ -46,3 +48,28 @@ func classifyOFDNamespace(namespace string) ofdXMLNamespace {
|
|||||||
return ofdXMLUnknown
|
return ofdXMLUnknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ofdXMLIDValid 按XML 1.0的NCName规则校验xs:ID,不用于数值型ST_ID
|
||||||
|
// 入参: id 标识
|
||||||
|
// 返回: bool 是否有效
|
||||||
|
func ofdXMLIDValid(id string) bool {
|
||||||
|
if id == "" || !utf8.ValidString(id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, c := range id {
|
||||||
|
if c == '_' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' ||
|
||||||
|
c >= 0xC0 && c <= 0xD6 || c >= 0xD8 && c <= 0xF6 ||
|
||||||
|
c >= 0xF8 && c <= 0x2FF || c >= 0x370 && c <= 0x37D ||
|
||||||
|
c >= 0x37F && c <= 0x1FFF || c >= 0x200C && c <= 0x200D ||
|
||||||
|
c >= 0x2070 && c <= 0x218F || c >= 0x2C00 && c <= 0x2FEF ||
|
||||||
|
c >= 0x3001 && c <= 0xD7FF || c >= 0xF900 && c <= 0xFDCF ||
|
||||||
|
c >= 0xFDF0 && c <= 0xFFFD || c >= 0x10000 && c <= 0xEFFFF {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i == 0 || !(c == '-' || c == '.' || c >= '0' && c <= '9' ||
|
||||||
|
c == 0xB7 || c >= 0x300 && c <= 0x36F || c >= 0x203F && c <= 0x2040) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
Reference in new issue
Block a user