feat(编辑压缩): 规范导入编辑,优化资源压缩
build.yaml / build (push) Successful in 7m31s

This commit is contained in:
xiaoqidun committed 2026-10-12 00:43:15 +08:00
1 parent 225f60919b
commit 19dc79e8e3
23 files changed
+836 -289

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
github.com/xiaoqidun/jbig2 v1.1.4 github.com/xiaoqidun/jbig2 v1.1.4
github.com/xiaoqidun/pdfgo v0.0.0-20261011113302-5ef97798f3e9 github.com/xiaoqidun/pdfgo v0.0.0-20261011164142-f98f64ee48bb
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
golang.org/x/image v0.47.0 golang.org/x/image v0.47.0
golang.org/x/text v0.43.0 golang.org/x/text v0.43.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e h1:OuRcvG5g5AieBN3
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c= github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
github.com/xiaoqidun/pdfgo v0.0.0-20261011113302-5ef97798f3e9 h1:ET5BMHDQ18Tr8sMUX7K8tw3FYMtBnl6pBYLYy0OI550= github.com/xiaoqidun/pdfgo v0.0.0-20261011164142-f98f64ee48bb h1:oxXcuj9fGKNLDYmSSiJjQXKkxSWQpmaNJwoF4SEtPZU=
github.com/xiaoqidun/pdfgo v0.0.0-20261011113302-5ef97798f3e9/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E= github.com/xiaoqidun/pdfgo v0.0.0-20261011164142-f98f64ee48bb/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+6 -3
View File
@@ -95,7 +95,7 @@ func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[stri
return fmt.Errorf("file not found: %s", name) return fmt.Errorf("file not found: %s", name)
} }
var err error var err error
data, err = reader.readFile(name) data, err = reader.readFileViewContext(ctx, name)
if err != nil { if err != nil {
return err return err
} }
@@ -110,15 +110,18 @@ func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[stri
continue continue
} }
for _, node := range group.children { for _, node := range group.children {
if err := ctx.Err(); err != nil {
return err
}
location := indexes[name][editorResourceID(node.attr("ID"))] location := indexes[name][editorResourceID(node.attr("ID"))]
if !node.matchesOFD("ColorSpace") || location == "" { if !node.matchesOFD("ColorSpace") || location == "" {
continue continue
} }
fragment, err := editorXMLAttribute(data, node, "Profile", "/"+location) patch, err := editorXMLAttributePatch(data, node, "Profile", "/"+location)
if err != nil { if err != nil {
return err return err
} }
patches = append(patches, editorXMLPatch{start: node.start, end: node.end, data: fragment}) patches = append(patches, patch)
} }
} }
updates[name] = editorPatchXML(data, patches) updates[name] = editorPatchXML(data, patches)
+21 -18
View File
@@ -72,10 +72,13 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
names[name] = true names[name] = true
} }
read := func(name string) ([]byte, error) { read := func(name string) ([]byte, error) {
if err := e.output.ctx.Err(); err != nil {
return nil, err
}
if data, ok := parts[name]; ok { if data, ok := parts[name]; ok {
return data, nil return data, nil
} }
return reader.readFileView(name) return reader.readFileViewContext(e.output.ctx, name)
} }
actual := make(map[string]string) actual := make(map[string]string)
for _, name := range slices.Sorted(maps.Keys(names)) { for _, name := range slices.Sorted(maps.Keys(names)) {
@@ -108,13 +111,16 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if err != nil { if err != nil {
return err return err
} }
root, err := parseEditorXML(data) root, err := parseEditorXMLContext(e.output.ctx, data)
if err != nil { if err != nil {
return err return err
} }
preserved := refs.versions[cleanPackagePath(name)] 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 {
if err := e.output.ctx.Err(); err != nil {
return err
}
field := "" field := ""
if group.name.Local == "Fonts" && node.name.Local == "Font" { if group.name.Local == "Fonts" && node.name.Local == "Font" {
field = "FontFile" field = "FontFile"
@@ -189,6 +195,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if err := e.prepareOutputImages(imageKeys, load, imageResults, imageSources); err != nil { if err := e.prepareOutputImages(imageKeys, load, imageResults, imageSources); err != nil {
return err return err
} }
var fontCache editorSourceFontCache
for _, key := range ordered { for _, key := range ordered {
if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] { if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] {
continue continue
@@ -197,7 +204,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if !files[resourceFile{key, role}] { if !files[resourceFile{key, role}] {
continue continue
} }
if err := editorProgress(e.OnWriteProgress).report("compress", e.output.completed, 0); err != nil { if err := editorProgress(e.OnWriteProgress).withContext(e.output.ctx).report("compress", e.output.completed, 0); err != nil {
return err return err
} }
source, prefetched := imageSources[key] source, prefetched := imageSources[key]
@@ -214,10 +221,13 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
source.key.hash = sha256.Sum256(data) source.key.hash = sha256.Sum256(data)
} }
if usage := fontUsage[key]; role == "FontFile" && usage != nil && !usage.unsafe { if usage := fontUsage[key]; role == "FontFile" && usage != nil && !usage.unsafe {
candidate, err := e.backends.FontResources.SubsetSourceFont(data, FontUsage{Characters: slices.Sorted(maps.Keys(usage.chars)), Glyphs: slices.Sorted(maps.Keys(usage.glyphs))}) candidate, err := fontCache.subset(e.output.ctx, e.backends.FontResources, data, usage)
if err != nil { if err != nil {
return fmt.Errorf("subset font %s: %w", key, err) return fmt.Errorf("subset font %s: %w", key, err)
} }
if err := e.output.ctx.Err(); err != nil {
return err
}
if len(candidate) != 0 && len(candidate) < len(data) { if len(candidate) != 0 && len(candidate) < len(data) {
data = candidate data = candidate
parts[actual[key]] = data parts[actual[key]] = data
@@ -302,13 +312,16 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if err != nil { if err != nil {
return err return err
} }
root, err := parseEditorXML(data) root, err := parseEditorXMLContext(e.output.ctx, data)
if err != nil { if err != nil {
return err return err
} }
var patches []editorXMLPatch var patches []editorXMLPatch
for _, group := range root.children { for _, group := range root.children {
for _, node := range group.children { for _, node := range group.children {
if err := e.output.ctx.Err(); err != nil {
return err
}
for _, field := range []string{"FontFile", "MediaFile"} { for _, field := range []string{"FontFile", "MediaFile"} {
file := node.child(field) file := node.child(field)
if file == nil { if file == nil {
@@ -319,7 +332,6 @@ 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)
fragment := data[node.start:node.end]
location := locations[key] location := locations[key]
if first, ok := aliases[key]; ok { if first, ok := aliases[key]; ok {
location = locations[first] location = locations[first]
@@ -327,23 +339,14 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if location != "" && location != actual[key] { if location != "" && location != actual[key] {
var escaped bytes.Buffer var escaped bytes.Buffer
xml.EscapeText(&escaped, []byte("/"+location)) xml.EscapeText(&escaped, []byte("/"+location))
patch := editorXMLContent(data, file, escaped.Bytes()) patches = append(patches, editorXMLContent(data, file, escaped.Bytes()))
patch.start -= node.start
patch.end -= node.start
fragment = editorPatchXML(fragment, []editorXMLPatch{patch})
} }
if field == "MediaFile" && formats[key] != "" && node.attr("Format") != formats[key] { if field == "MediaFile" && formats[key] != "" && node.attr("Format") != formats[key] {
parsed, err := parseEditorXML(fragment) patch, err := editorXMLAttributePatch(data, node, "Format", formats[key])
if err != nil { if err != nil {
return err return err
} }
fragment, err = editorXMLAttribute(fragment, parsed, "Format", formats[key]) patches = append(patches, patch)
if err != nil {
return err
}
}
if !bytes.Equal(fragment, data[node.start:node.end]) {
patches = append(patches, editorXMLPatch{start: node.start, end: node.end, data: fragment})
} }
} }
} }
+4 -1
View File
@@ -188,7 +188,7 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx,
// 入参: page 页面索引, ids 注解标识, matrix 可逆页面变换 // 入参: page 页面索引, ids 注解标识, matrix 可逆页面变换
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) TransformAnnotations(page int, ids []string, matrix Matrix) error { func (e *Editor) TransformAnnotations(page int, ids []string, matrix Matrix) error {
if _, ok := matrix.Invert(); !ok || !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) || !finite(matrix.e) || !finite(matrix.f) { if _, ok := matrix.Invert(); !ok {
return fmt.Errorf("annotation transform must be finite and invertible") return fmt.Errorf("annotation transform must be finite and invertible")
} }
return e.Transaction(func(edit *Editor) error { return e.Transaction(func(edit *Editor) error {
@@ -210,6 +210,9 @@ func (e *Editor) TransformAnnotations(page int, ids []string, matrix Matrix) err
return err return err
} }
bounds := matrix.TransformBox(box) bounds := matrix.TransformBox(box)
if !finite(bounds.X) || !finite(bounds.Y) || !finite(bounds.W) || !finite(bounds.H) || bounds.W < 0 || bounds.H < 0 {
return fmt.Errorf("annotation transform exceeds finite range")
}
path := ObjectPath{Annotation: id} path := ObjectPath{Annotation: id}
reader, _, _, members, err := edit.compositeScope(page, path) reader, _, _, members, err := edit.compositeScope(page, path)
if err != nil { if err != nil {
+10 -2
View File
@@ -84,7 +84,7 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
} }
node := doc.child("Attachments") node := doc.child("Attachments")
parts := map[string][]byte{file: bytes.Clone(data)} parts := map[string][]byte{file: bytes.Clone(data)}
if base.document.Attachments.Path != "" { if strings.TrimSpace(base.document.Attachments.Path) != "" {
name := base.reader.ResPath(base.document.Attachments.Path) name := base.reader.ResPath(base.document.Attachments.Path)
existing, err := base.reader.readFile(name) existing, err := base.reader.readFile(name)
if err != nil { if err != nil {
@@ -203,7 +203,7 @@ func (e *Editor) ReplaceAttachment(id string, data []byte) error {
return nil return nil
} }
// DeleteAttachment 删除附件声明,保存时清理可确认无引用的内容,支持撤销和重做 // DeleteAttachment 删除附件声明,输出时清理相关动作及可确认无引用的内容,支持撤销和重做
// 入参: id 附件标识 // 入参: id 附件标识
// 返回: error 附件结构或提交错误 // 返回: error 附件结构或提交错误
func (e *Editor) DeleteAttachment(id string) error { func (e *Editor) DeleteAttachment(id string) error {
@@ -270,6 +270,14 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
if err := e.commitAnnotationParts(base, parts); err != nil { if err := e.commitAnnotationParts(base, parts); err != nil {
return err return err
} }
if len(updated) == 0 {
removed := maps.Clone(base.removedAttachments)
if removed == nil {
removed = make(map[string]bool)
}
removed[id] = true
e.source.removedAttachments = removed
}
if file := node.child("FileLoc"); file != nil { if file := node.child("FileLoc"); file != nil {
location := strings.TrimSpace(editorImportText(fragment, file)) location := strings.TrimSpace(editorImportText(fragment, file))
if location != "" { if location != "" {
+2 -1
View File
@@ -653,7 +653,8 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
if transform && node.object.Type == "ImageObject" && bounds.Bounds.W > 0 && bounds.Bounds.H > 0 { if transform && node.object.Type == "ImageObject" && bounds.Bounds.W > 0 && bounds.Bounds.H > 0 {
clips := node.object.ImageObject.Clips clips := node.object.ImageObject.Clips
_, capability.CropImage = node.matrix(clips.usesObjectMatrix()).Invert() _, capability.CropImage = node.matrix(clips.usesObjectMatrix()).Invert()
capability.FitImage = capability.CropImage && axisAlignedMatrix(NewMatrix(node.object.ImageObject.CTM)) _, fitErr := imageFitBasis(node.object.ImageObject.CTM)
capability.FitImage = capability.CropImage && fitErr == nil
capability.ResetCrop = node.states[editorObjectID(node.object)].crop != nil capability.ResetCrop = node.states[editorObjectID(node.object)].crop != nil
} }
_, ctm := editorGeometry(node.object) _, ctm := editorGeometry(node.object)
+18 -2
View File
@@ -108,7 +108,7 @@ func (e *Editor) ResetImageCrop(page int, id string) error {
return e.updateObjectOrigins(page, []GraphicObject{node.object}, true, map[string]*editorObjectOrigin{id: next}) return e.updateObjectOrigins(page, []GraphicObject{node.object}, true, map[string]*editorObjectOrigin{id: next})
} }
// FitCompositeImage 按原像素比例适应或填充内部图片框,保留原始裁剪和蒙版 // FitCompositeImage 按原像素比例适应或填充内部图片框,保留轴方向、原始裁剪和蒙版
// 入参: page 页面索引, path 父路径, index 图片序号, mode 为contain或cover // 入参: page 页面索引, path 父路径, index 图片序号, mode 为contain或cover
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) FitCompositeImage(page int, path ObjectPath, index int, mode string) error { func (e *Editor) FitCompositeImage(page int, path ObjectPath, index int, mode string) error {
@@ -117,6 +117,14 @@ func (e *Editor) FitCompositeImage(page int, path ObjectPath, index int, mode st
if !members[0].Capabilities.FitImage { if !members[0].Capabilities.FitImage {
return fmt.Errorf("composite image fitting is not supported") return fmt.Errorf("composite image fitting is not supported")
} }
before, changed := node.data, node.changed
id := editorObjectID(node.object)
state := node.states[id]
defer func() {
if bytes.Equal(before, node.data) && reflect.DeepEqual(state, node.states[id]) {
node.changed = changed
}
}()
if err := resetCompositeImageCrop(node); err != nil { if err := resetCompositeImageCrop(node); err != nil {
return err return err
} }
@@ -143,7 +151,10 @@ func (e *Editor) FitCompositeImage(page int, path ObjectPath, index int, mode st
frame = TranslationMatrix(box.X, box.Y).Multiply(node.parent) frame = TranslationMatrix(box.X, box.Y).Multiply(node.parent)
} }
clips := object.ImageObject.Clips clips := object.ImageObject.Clips
inverse, _ := node.matrix(clips.usesObjectMatrix()).Invert() inverse, ok := node.matrix(clips.usesObjectMatrix()).Invert()
if !ok {
return fmt.Errorf("fitted image transform is not invertible")
}
return appendCompositeClip(node, shape, inverse.Multiply(frame), true) return appendCompositeClip(node, shape, inverse.Multiply(frame), true)
}) })
} }
@@ -174,6 +185,11 @@ func (n *editorCompositeNode) matrix(transform bool) Matrix {
// 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原序号 // 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原序号
// 返回: error 错误信息 // 返回: error 错误信息
func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Matrix, track bool) error { func appendCompositeClip(node *editorCompositeNode, shape PathObject, matrix Matrix, track bool) error {
for _, value := range matrix.Values() {
if !finite(value) {
return fmt.Errorf("clip transform exceeds finite range")
}
}
clip := Clip{Area: []ClipArea{{CTM: matrix.String(), Path: []PathObject{shape}}}} clip := Clip{Area: []ClipArea{{CTM: matrix.String(), Path: []PathObject{shape}}}}
if clips := *editorObjectClips(&node.object); clips != nil && slices.ContainsFunc(clips.Clip, func(previous Clip) bool { return reflect.DeepEqual(previous, clip) }) { if clips := *editorObjectClips(&node.object); clips != nil && slices.ContainsFunc(clips.Clip, func(previous Clip) bool { return reflect.DeepEqual(previous, clip) }) {
return nil return nil
+3
View File
@@ -254,6 +254,9 @@ func (e *Editor) copyCompositeNodes(renderer *Renderer, nodes []*editorComposite
// 入参: object 修改后的对象 // 入参: object 修改后的对象
// 返回: error 错误信息 // 返回: error 错误信息
func (n *editorCompositeNode) update(object GraphicObject) error { func (n *editorCompositeNode) update(object GraphicObject) error {
if err := validateEditorGeometry(object); err != nil {
return err
}
data, err := editorXMLObject(n.data, n.node, n.object, object) data, err := editorXMLObject(n.data, n.node, n.object, object)
if err != nil || bytes.Equal(data, n.data) { if err != nil || bytes.Equal(data, n.data) {
return err return err
+52
View File
@@ -16,7 +16,9 @@ package ofdgo
import ( import (
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"encoding/binary"
"fmt" "fmt"
"maps" "maps"
"slices" "slices"
@@ -38,6 +40,56 @@ type editorFontSubset struct {
mapped bool mapped bool
} }
// editorSourceFontKey 区分原字体内容及完整用字集合
type editorSourceFontKey struct {
source [32]byte
usage [32]byte
}
// editorSourceFontCache 在单次资源处理中复用裁剪结果,空值表示无体积收益
type editorSourceFontCache map[editorSourceFontKey][]byte
// subset 按原字体和用字复用安全子集,不缓存错误、取消结果或原始字体
// 入参: ctx 取消上下文, backend 字体资源后端, data 原字体, usage 用字记录
// 返回: []byte 更小的字体子集,无收益时为nil, error 裁剪或取消错误
func (c *editorSourceFontCache) subset(ctx context.Context, backend FontResourceBackend, data []byte, usage *editorFontUsage) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if usage == nil || usage.unsafe || backend == nil {
return nil, nil
}
characters, glyphs := slices.Sorted(maps.Keys(usage.chars)), slices.Sorted(maps.Keys(usage.glyphs))
encoded := make([]byte, 0, 8+4*len(characters)+2*len(glyphs))
encoded = binary.LittleEndian.AppendUint32(encoded, uint32(len(characters)))
for _, char := range characters {
encoded = binary.LittleEndian.AppendUint32(encoded, uint32(char))
}
encoded = binary.LittleEndian.AppendUint32(encoded, uint32(len(glyphs)))
for _, glyph := range glyphs {
encoded = binary.LittleEndian.AppendUint16(encoded, glyph)
}
key := editorSourceFontKey{source: sha256.Sum256(data), usage: sha256.Sum256(encoded)}
if result, ok := (*c)[key]; ok {
return result, ctx.Err()
}
result, err := backend.SubsetSourceFont(data, FontUsage{Characters: characters, Glyphs: glyphs})
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
if len(result) == 0 || len(result) >= len(data) {
result = nil
}
if *c == nil {
*c = make(editorSourceFontCache)
}
(*c)[key] = result
return result, nil
}
// AddFont 注册OpenType字体,集合字体按索引提取,重复资源复用标识,引用后写入文档 // AddFont 注册OpenType字体,集合字体按索引提取,重复资源复用标识,引用后写入文档
// 入参: file 字体文件, index 集合内字体索引,单字体为0 // 入参: file 字体文件, index 集合内字体索引,单字体为0
// 返回: string 字体资源标识, error 错误信息 // 返回: string 字体资源标识, error 错误信息
+252 -91
View File
@@ -43,24 +43,29 @@ type editorPageImport struct {
doc *Document doc *Document
directory string directory string
target *Reader target *Reader
paths map[string]string xmlPaths map[string]string
dataPaths map[string]string
templatePaths map[string]string templatePaths map[string]string
evidence map[string]string evidence map[string]string
resourceXML []byte resourceXML []byte
pageParts []byte pageParts []byte
documentXML []byte documentXML []byte
documentRoot *editorXML documentRoot *editorXML
docRefs map[string]map[string]*editorXML
maximum int maximum int
ids map[string]string ids map[string]string
pages map[string]bool pages map[string]bool
files map[string][]byte files map[string][]byte
resources map[string]editorImportEntry resources map[string]editorImportEntry
resourceFiles map[string]bool
used map[string][]byte used map[string][]byte
templates map[string]TemplatePage templates map[string]TemplatePage
templateParts map[string][]byte templateParts map[string][]byte
defaultCS string defaultCS string
attachments map[string][]byte attachments map[string][]byte
attachmentMap map[string]editorImportEntry
attachmentIDs map[string]bool attachmentIDs map[string]bool
bookmarks map[string]Dest
signatureIDs *signatureIDSequence signatureIDs *signatureIDSequence
copyPage bool copyPage bool
strictLinks bool strictLinks bool
@@ -483,9 +488,15 @@ func (e *Editor) prepareImport(ctx context.Context, source *Reader, doc *Documen
} }
} }
m := &editorPageImport{ctx: ctx, reader: source, doc: doc, directory: e.packageDirectory(), target: base.reader, maximum: maximum, copyPage: copyPage, progress: progress, m := &editorPageImport{ctx: ctx, reader: source, doc: doc, directory: e.packageDirectory(), target: base.reader, maximum: maximum, copyPage: copyPage, progress: progress,
paths: make(map[string]string), evidence: make(map[string]string), ids: make(map[string]string), pages: make(map[string]bool, len(selected)), xmlPaths: make(map[string]string), dataPaths: make(map[string]string), evidence: make(map[string]string), ids: make(map[string]string), pages: make(map[string]bool, len(selected)),
files: make(map[string][]byte), resources: make(map[string]editorImportEntry), used: make(map[string][]byte), files: make(map[string][]byte), resources: make(map[string]editorImportEntry), used: make(map[string][]byte),
templates: make(map[string]TemplatePage), templateParts: make(map[string][]byte), attachments: make(map[string][]byte)} templates: make(map[string]TemplatePage), templateParts: make(map[string][]byte), attachments: make(map[string][]byte), bookmarks: make(map[string]Dest, len(doc.Bookmarks.Bookmark))}
for _, bookmark := range doc.Bookmarks.Bookmark {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
m.bookmarks[bookmark.Name] = bookmark.Dest
}
for id, value := range selected { for id, value := range selected {
m.pages[editorImportKey(id)] = value m.pages[editorImportKey(id)] = value
} }
@@ -516,7 +527,7 @@ func (e *Editor) prepareImport(ctx context.Context, source *Reader, doc *Documen
m.templates[editorImportKey(template.ID)] = template m.templates[editorImportKey(template.ID)] = template
} }
if doc.CommonData.DefaultCS != 0 { if doc.CommonData.DefaultCS != 0 {
m.defaultCS, err = m.reference(strconv.Itoa(doc.CommonData.DefaultCS)) m.defaultCS, err = m.resource(strconv.Itoa(doc.CommonData.DefaultCS), "ColorSpace")
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -556,7 +567,7 @@ func (m *editorPageImport) encodeResources() error {
return nil return nil
} }
var err error var err error
m.resourceXML, err = editorXMLContainer("Res", nil, content) m.resourceXML, err = editorXMLContainer("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}, content)
return err return err
} }
@@ -588,13 +599,12 @@ func (m *editorPageImport) outlines() ([]byte, error) {
if root == nil { if root == nil {
return nil, nil return nil, nil
} }
bookmarks := make(map[string]Dest, len(m.doc.Bookmarks.Bookmark))
for _, bookmark := range m.doc.Bookmarks.Bookmark {
bookmarks[bookmark.Name] = bookmark.Dest
}
var patches []editorXMLPatch var patches []editorXMLPatch
var keep func(*editorXML) (bool, error) var keep func(*editorXML) (bool, error)
keep = func(node *editorXML) (bool, error) { keep = func(node *editorXML) (bool, error) {
if err := m.ctx.Err(); err != nil {
return false, err
}
start, selected := len(patches), false start, selected := len(patches), false
if actions := node.child("Actions"); actions != nil { if actions := node.child("Actions"); actions != nil {
var value struct { var value struct {
@@ -605,7 +615,7 @@ func (m *editorPageImport) outlines() ([]byte, error) {
} }
for _, action := range value.Action { for _, action := range value.Action {
if action.Goto != nil { if action.Goto != nil {
if dest := gotoDest(action.Goto, bookmarks); dest != nil && m.pages[editorImportKey(dest.PageID)] { if dest := gotoDest(action.Goto, m.bookmarks); dest != nil && m.pages[editorImportKey(dest.PageID)] {
selected = true selected = true
} }
} }
@@ -639,6 +649,9 @@ func (m *editorPageImport) outlines() ([]byte, error) {
} }
var validate func(*editorXML) error var validate func(*editorXML) error
validate = func(node *editorXML) error { validate = func(node *editorXML) error {
if err := m.ctx.Err(); err != nil {
return err
}
for _, attr := range node.attrs { for _, attr := range node.attrs {
if attr.Name.Space == "" || attr.Name.Space == "xmlns" { if attr.Name.Space == "" || attr.Name.Space == "xmlns" {
continue continue
@@ -765,10 +778,17 @@ func (m *editorPageImport) id(value string) string {
return id return id
} }
// resourceIndex 登记资源定义,按引用延迟复制资源内容 // resourceIndex 每个索引只登记一次,拒绝重复资源标识,按引用延迟复制资源内容
// 入参: name 资源索引路径 // 入参: name 资源索引路径
// 返回: error 错误信息 // 返回: error 错误信息
func (m *editorPageImport) resourceIndex(name string) error { func (m *editorPageImport) resourceIndex(name string) error {
if err := m.ctx.Err(); err != nil {
return err
}
name = cleanPackagePath(name)
if m.resourceFiles[name] {
return nil
}
data, err := m.readFile(name) data, err := m.readFile(name)
if err != nil { if err != nil {
return err return err
@@ -780,15 +800,54 @@ func (m *editorPageImport) resourceIndex(name string) error {
if root.name.Local != "Res" { if root.name.Local != "Res" {
return fmt.Errorf("invalid resource index %q", name) return fmt.Errorf("invalid resource index %q", name)
} }
entries := make(map[string]editorImportEntry)
for _, group := range root.children { for _, group := range root.children {
for _, node := range group.children { for _, node := range group.children {
m.resources[editorImportKey(node.attr("ID"))] = editorImportEntry{name: name, base: root.attr("BaseLoc"), group: group.name.Local, data: data, node: node} if err := m.ctx.Err(); err != nil {
return err
}
id := editorImportKey(node.attr("ID"))
if _, exists := entries[id]; exists {
return fmt.Errorf("duplicate resource ID %q in %s", id, name)
}
if _, exists := m.resources[id]; exists {
return fmt.Errorf("duplicate resource ID %q in %s", id, name)
}
entries[id] = editorImportEntry{name: name, base: root.attr("BaseLoc"), group: group.name.Local, data: data, node: node}
} }
} }
maps.Copy(m.resources, entries)
if m.resourceFiles == nil {
m.resourceFiles = make(map[string]bool)
}
m.resourceFiles[name] = true
return nil return nil
} }
// reference 复制被引用的资源及其传递依赖 // resource 按引用用途校验资源类型并迁移,避免生成悬空或类型不符的引用
// 入参: value 来源资源标识, kind 资源类型或多媒体用途,空值仅检查资源是否存在
// 返回: string 新标识, error 资源缺失、类型或迁移错误
func (m *editorPageImport) resource(value, kind string) (string, error) {
if err := m.ctx.Err(); err != nil {
return "", err
}
entry, exists := m.resources[editorImportKey(value)]
if !exists {
return "", fmt.Errorf("resource %q not found", value)
}
if kind != "" {
expected, media := kind, ""
if kind == "Image" || kind == "Audio" || kind == "Video" {
kind, media = "MultiMedia", kind
}
if entry.group != kind+"s" || entry.node == nil || !entry.node.matchesOFD(kind) || media != "" && entry.node.attr("Type") != media {
return "", fmt.Errorf("invalid resource type for %q: expected %s", value, expected)
}
}
return m.reference(value)
}
// reference 复制被引用的资源及其传递依赖,拒绝循环依赖
// 入参: value 来源资源标识 // 入参: value 来源资源标识
// 返回: string 新标识, error 错误信息 // 返回: string 新标识, error 错误信息
func (m *editorPageImport) reference(value string) (string, error) { func (m *editorPageImport) reference(value string) (string, error) {
@@ -802,7 +861,9 @@ func (m *editorPageImport) reference(value string) (string, error) {
} }
} }
if entry, ok := m.resources[key]; ok { if entry, ok := m.resources[key]; ok {
if _, seen := m.used[key]; !seen { if data, seen := m.used[key]; seen && data == nil {
return "", fmt.Errorf("cyclic resource reference %q", value)
} else if !seen {
m.used[key] = nil m.used[key] = nil
data, err := m.encode(entry, entry.node) data, err := m.encode(entry, entry.node)
if err != nil { if err != nil {
@@ -832,29 +893,52 @@ func (m *editorPageImport) reference(value string) (string, error) {
// 入参: group 父节点名称, kind 引用节点名称, source 原标识, id 新标识, loc 新路径 // 入参: group 父节点名称, kind 引用节点名称, source 原标识, id 新标识, loc 新路径
// 返回: []byte 引用XML, error 错误信息 // 返回: []byte 引用XML, error 错误信息
func (m *editorPageImport) documentReference(group, kind, source, id, loc string) ([]byte, error) { func (m *editorPageImport) documentReference(group, kind, source, id, loc string) ([]byte, error) {
if parent := m.documentRoot.child(group); parent != nil { if err := m.ctx.Err(); err != nil {
for _, node := range parent.children { return nil, err
if node.name.Local != kind || editorImportKey(node.attr("ID")) != editorImportKey(source) {
continue
}
data, err := editorXMLStandalone(m.documentXML[node.start:node.end], node)
if err != nil {
return nil, err
}
for _, attr := range [][2]string{{"ID", id}, {"BaseLoc", loc}} {
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
data, err = editorXMLAttribute(data, root, attr[0], attr[1])
if err != nil {
return nil, err
}
}
return data, nil
}
} }
return nil, fmt.Errorf("missing %s reference %q", kind, source) nodes := m.docRefs[kind]
if nodes == nil {
nodes = make(map[string]*editorXML)
if parent := m.documentRoot.child(group); parent != nil {
for _, node := range parent.children {
if err := m.ctx.Err(); err != nil {
return nil, err
}
if !node.matchesOFD(kind) {
continue
}
key := editorImportKey(node.attr("ID"))
if nodes[key] != nil {
return nil, fmt.Errorf("duplicate %s reference %q", kind, key)
}
nodes[key] = node
}
}
if m.docRefs == nil {
m.docRefs = make(map[string]map[string]*editorXML)
}
m.docRefs[kind] = nodes
}
node := nodes[editorImportKey(source)]
if node == nil {
return nil, fmt.Errorf("missing %s reference %q", kind, source)
}
data, err := editorXMLStandalone(m.documentXML[node.start:node.end], node)
if err != nil {
return nil, err
}
for _, attr := range [][2]string{{"ID", id}, {"BaseLoc", loc}} {
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
patch, err := editorXMLAttributePatch(data, root, attr[0], attr[1])
if err != nil {
return nil, err
}
data = editorPatchXML(data, []editorXMLPatch{patch})
}
return data, nil
} }
// resolve 按所在XML和资源基准目录解析包内路径 // resolve 按所在XML和资源基准目录解析包内路径
@@ -871,11 +955,11 @@ func (m *editorPageImport) resolve(name, base, value string) (string, error) {
return "", fmt.Errorf("import reference %q in %s not found", value, name) return "", fmt.Errorf("import reference %q in %s not found", value, name)
} }
// copyData 复制二进制资源或签名引用原文,凭据与当前引用使用独立映射 // copyData 复制原始数据,资源内容、签名凭据与重写XML使用独立路径映射
// 入参: name 来源路径, relative 文档内候选路径, evidence 是否原始签名凭据 // 入参: name 来源路径, relative 文档内候选路径, evidence 是否原始签名凭据
// 返回: string 新路径, error 错误信息 // 返回: string 新路径, error 错误信息
func (m *editorPageImport) copyData(name, relative string, evidence bool) (string, error) { func (m *editorPageImport) copyData(name, relative string, evidence bool) (string, error) {
paths := m.paths paths := m.dataPaths
if evidence { if evidence {
paths = m.evidence paths = m.evidence
relative = path.Join("Signs", "Signed", path.Base(name)) relative = path.Join("Signs", "Signed", path.Base(name))
@@ -900,7 +984,7 @@ func (m *editorPageImport) copyData(name, relative string, evidence bool) (strin
// 入参: name 来源路径, candidate 目标候选路径, template 是否按模板页复制 // 入参: name 来源路径, candidate 目标候选路径, template 是否按模板页复制
// 返回: string 新路径, error 错误信息 // 返回: string 新路径, error 错误信息
func (m *editorPageImport) copyXML(name, candidate string, template bool) (string, error) { func (m *editorPageImport) copyXML(name, candidate string, template bool) (string, error) {
paths := m.paths paths := m.xmlPaths
if template { if template {
if m.templatePaths == nil { if m.templatePaths == nil {
m.templatePaths = make(map[string]string) m.templatePaths = make(map[string]string)
@@ -1007,11 +1091,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
if err := xml.Unmarshal(entry.data[gotoNode.start:gotoNode.end], &action); err != nil { if err := xml.Unmarshal(entry.data[gotoNode.start:gotoNode.end], &action); err != nil {
return nil, err return nil, err
} }
bookmarks := make(map[string]Dest) if dest := gotoDest(&action, m.bookmarks); !m.copyPage && (dest == nil || !m.pages[editorImportKey(dest.PageID)]) {
for _, bookmark := range m.doc.Bookmarks.Bookmark {
bookmarks[bookmark.Name] = bookmark.Dest
}
if dest := gotoDest(&action, bookmarks); !m.copyPage && (dest == nil || !m.pages[editorImportKey(dest.PageID)]) {
if m.strictLinks { if m.strictLinks {
return nil, fmt.Errorf("object link targets a page outside the imported selection") return nil, fmt.Errorf("object link targets a page outside the imported selection")
} }
@@ -1050,7 +1130,31 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
} else if id := m.ids[editorImportKey(value)]; id != "" { } else if id := m.ids[editorImportKey(value)]; id != "" {
value = id value = id
} }
case "Font", "ResourceID", "Substitution", "ImageMask", "Relative", "DrawParam", "ColorSpace", "Thumbnail", "TemplateID", "PageRef", "RefId": case "Font", "DrawParam", "ColorSpace":
value, err = m.resource(value, key)
case "Relative":
value, err = m.resource(value, "DrawParam")
case "Substitution", "ImageMask", "Thumbnail":
value, err = m.resource(value, "Image")
case "ResourceID":
kind := ""
switch name {
case "CompositeObject":
kind = "CompositeGraphicUnit"
case "ImageObject":
kind = "Image"
case "Sound":
kind = "Audio"
case "Movie":
kind = "Video"
}
value, err = m.resource(value, kind)
case "TemplateID":
if _, exists := m.templates[editorImportKey(value)]; !exists {
return nil, fmt.Errorf("template %q not found", value)
}
value, err = m.reference(value)
case "PageRef", "RefId":
value, err = m.reference(value) value, err = m.reference(value)
case "BaseLoc", "FileRef": case "BaseLoc", "FileRef":
loc, resolveErr := m.resolve(entry.name, "", value) loc, resolveErr := m.resolve(entry.name, "", value)
@@ -1147,11 +1251,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
continue continue
} }
if name == "Goto" && child.name.Local == "Bookmark" { if name == "Goto" && child.name.Local == "Bookmark" {
for _, bookmark := range m.doc.Bookmarks.Bookmark { if dest, ok := m.bookmarks[child.attr("Name")]; ok {
if bookmark.Name != child.attr("Name") {
continue
}
dest := bookmark.Dest
encoded, err := xml.Marshal(dest) encoded, err := xml.Marshal(dest)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1174,6 +1274,9 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
} }
content = append(content, encoded...) content = append(content, encoded...)
} }
if name == "Actions" && len(content) == 0 {
return nil, nil
}
if len(node.children) == 0 { if len(node.children) == 0 {
value := editorImportText(entry.data, node) value := editorImportText(entry.data, node)
var err error var err error
@@ -1216,8 +1319,8 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
candidate := path.Join(m.directory, "Annotations", "Page_"+m.id(node.parent.attr("PageID"))+".xml") candidate := path.Join(m.directory, "Annotations", "Page_"+m.id(node.parent.attr("PageID"))+".xml")
value, err = m.copyXML(loc, candidate, false) value, err = m.copyXML(loc, candidate, false)
} }
case "Font", "Substitution", "Thumbnail": case "Substitution", "Thumbnail":
value, err = m.reference(strings.TrimSpace(value)) value, err = m.resource(strings.TrimSpace(value), "Image")
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1265,39 +1368,58 @@ func (m *editorPageImport) annotations() ([]byte, error) {
// 入参: id 来源附件标识 // 入参: id 来源附件标识
// 返回: string 新标识, error 错误信息 // 返回: string 新标识, error 错误信息
func (m *editorPageImport) attachment(id string) (string, error) { func (m *editorPageImport) attachment(id string) (string, error) {
if err := m.ctx.Err(); err != nil {
return "", err
}
if m.copyPage { if m.copyPage {
return id, nil return id, nil
} }
if _, exists := m.attachments[id]; exists { if _, exists := m.attachments[id]; exists {
return m.attachmentID(id) return m.attachmentID(id)
} }
name := m.reader.documentRoot if m.attachmentMap == nil {
if m.doc.Attachments.Path != "" { name, data := m.reader.documentRoot, m.documentXML
name = m.reader.ResPath(m.doc.Attachments.Path) root := m.documentRoot.child("Attachments")
} if location := strings.TrimSpace(m.doc.Attachments.Path); location != "" {
data, err := m.readFile(name) name = m.reader.ResPath(location)
if err != nil { var err error
return "", err data, err = m.readFile(name)
} if err != nil {
root, err := parseEditorXMLContext(m.ctx, data) return "", err
if err != nil { }
return "", err root, err = parseEditorXMLContext(m.ctx, data)
} if err != nil {
if m.doc.Attachments.Path == "" { return "", err
root = root.child("Attachments")
}
if root != nil {
for _, node := range root.children {
if node.name.Local == "Attachment" && node.attr("ID") == id {
m.attachments[id], err = m.encode(editorImportEntry{name: name, data: data}, node)
if err != nil {
return "", err
}
return m.attachmentID(id)
} }
} }
entries := make(map[string]editorImportEntry)
if root != nil {
for _, node := range root.children {
if err := m.ctx.Err(); err != nil {
return "", err
}
if !node.matchesOFD("Attachment") {
continue
}
key := node.attr("ID")
if _, exists := entries[key]; exists {
return "", fmt.Errorf("duplicate attachment ID: %s", key)
}
entries[key] = editorImportEntry{name: name, data: data, node: node}
}
}
m.attachmentMap = entries
} }
return "", fmt.Errorf("attachment %q not found", id) entry, exists := m.attachmentMap[id]
if !exists {
return "", fmt.Errorf("attachment %q not found", id)
}
data, err := m.encode(entry, entry.node)
if err != nil {
return "", err
}
m.attachments[id] = data
return m.attachmentID(id)
} }
// attachmentID 为来源附件分配独立的文本标识,避免与数值型资源标识混淆 // attachmentID 为来源附件分配独立的文本标识,避免与数值型资源标识混淆
@@ -1313,10 +1435,17 @@ func (m *editorPageImport) attachmentID(id string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
m.attachmentIDs = make(map[string]bool, len(attachments)) used := make(map[string]bool, len(attachments))
for _, attachment := range attachments { for _, attachment := range attachments {
m.attachmentIDs[attachment.ID] = true if err := m.ctx.Err(); err != nil {
return "", err
}
if used[attachment.ID] {
return "", fmt.Errorf("duplicate attachment ID: %s", attachment.ID)
}
used[attachment.ID] = true
} }
m.attachmentIDs = used
} }
mapped, maximum := nextAttachmentID(m.attachmentIDs, m.maximum) mapped, maximum := nextAttachmentID(m.attachmentIDs, m.maximum)
m.maximum, m.ids[key] = maximum, mapped m.maximum, m.ids[key] = maximum, mapped
@@ -1353,15 +1482,11 @@ func (m *editorPageImport) signatureID(source, kind, id string) (string, error)
sequence.observe(list.MaxSignID) sequence.observe(list.MaxSignID)
for _, ref := range list.List { for _, ref := range list.List {
sequence.observe(ref.ID) sequence.observe(ref.ID)
data, err := m.target.readFileViewContext(m.ctx, signatureRefPath(name, ref.BaseLoc)) stamps, err := m.signatureStamps(m.target, signatureRefPath(name, ref.BaseLoc))
if err != nil { if err != nil {
return "", err return "", err
} }
file, err := parseSignatureFile(data) for _, stamp := range stamps {
if err != nil {
return "", err
}
for _, stamp := range file.SignedInfo.StampAnnot {
sequence.observe(stamp.ID) sequence.observe(stamp.ID)
} }
} }
@@ -1376,6 +1501,46 @@ func (m *editorPageImport) signatureID(source, kind, id string) (string, error)
return mapped, nil return mapped, nil
} }
// signatureStamps 流式读取签章位置,跳过本次迁移无需解析的摘要和提供者信息
// 入参: reader 来源或目标阅读器, name 签名文件路径
// 返回: []SignatureStamp 签章位置, error 读取、解析或取消错误
func (m *editorPageImport) signatureStamps(reader *Reader, name string) ([]SignatureStamp, error) {
input, err := reader.openFile(name)
if err != nil {
return nil, err
}
defer input.Close()
var file struct {
XMLName xml.Name `xml:"Signature"`
Stamps []SignatureStamp `xml:"SignedInfo>StampAnnot"`
}
progress := m.progress.withContext(m.ctx)
decoder := xml.NewDecoder(&editorProgressReader{Reader: input, progress: progress, stage: "resources"})
if err := decoder.Decode(&file); err != nil {
return nil, err
}
for {
if err := m.ctx.Err(); err != nil {
return nil, err
}
token, err := decoder.Token()
if err == io.EOF {
return file.Stamps, nil
}
if err != nil {
return nil, err
}
switch value := token.(type) {
case xml.StartElement:
return nil, fmt.Errorf("multiple XML roots in signature %s", name)
case xml.CharData:
if len(bytes.TrimSpace(value)) != 0 {
return nil, fmt.Errorf("unexpected text after signature %s", name)
}
}
}
}
// signatures 复制所选页面的签章及原始验证凭据,过滤其他页面的签章位置 // signatures 复制所选页面的签章及原始验证凭据,过滤其他页面的签章位置
// 返回: []byte 签名索引条目, error 错误信息 // 返回: []byte 签名索引条目, error 错误信息
func (m *editorPageImport) signatures() ([]byte, error) { func (m *editorPageImport) signatures() ([]byte, error) {
@@ -1400,16 +1565,12 @@ func (m *editorPageImport) signatures() ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
value, err := m.readFile(loc) stamps, err := m.signatureStamps(m.reader, loc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var signature SignatureFile
if err := xml.Unmarshal(value, &signature); err != nil {
return nil, err
}
matched := false matched := false
for _, stamp := range signature.SignedInfo.StampAnnot { for _, stamp := range stamps {
matched = matched || m.pages[editorImportKey(stamp.PageRef)] matched = matched || m.pages[editorImportKey(stamp.PageRef)]
} }
if !matched { if !matched {
@@ -1461,7 +1622,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
} }
var content []byte var content []byte
if len(m.resourceXML) != 0 { if len(m.resourceXML) != 0 {
name, added, err := mergePackageResources(base.reader, files, base.document, m.directory, m.resourceXML) name, added, err := mergePackageResources(m.ctx, base.reader, files, base.document, m.directory, m.resourceXML)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+20 -2
View File
@@ -43,6 +43,7 @@ type editorSource struct {
annotationStates map[string]map[string]editorCompositeState annotationStates map[string]map[string]editorCompositeState
annotationPages map[string]bool annotationPages map[string]bool
retiredAttachments map[string]bool retiredAttachments map[string]bool
removedAttachments map[string]bool
} }
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用 // editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用
@@ -540,7 +541,10 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
all.Stretch = object.Type == "ImageObject" all.Stretch = object.Type == "ImageObject"
all.ReplaceImage = object.Type == "ImageObject" all.ReplaceImage = object.Type == "ImageObject"
all.CropImage = all.ReplaceImage all.CropImage = all.ReplaceImage
all.FitImage = all.ReplaceImage && axisAlignedMatrix(NewMatrix(object.ImageObject.CTM)) if all.ReplaceImage {
_, err := imageFitBasis(object.ImageObject.CTM)
all.FitImage = err == nil
}
return all return all
} }
node := origin.node node := origin.node
@@ -578,7 +582,10 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
all.Paint = all.Paint && e.editorPaintable(object) all.Paint = all.Paint && e.editorPaintable(object)
all.ReplaceImage = all.Update && object.Type == "ImageObject" all.ReplaceImage = all.Update && object.Type == "ImageObject"
all.CropImage = all.ReplaceImage all.CropImage = all.ReplaceImage
all.FitImage = all.ReplaceImage && axisAlignedMatrix(NewMatrix(object.ImageObject.CTM)) if all.ReplaceImage {
_, err := imageFitBasis(object.ImageObject.CTM)
all.FitImage = err == nil
}
if object.Type == "TextObject" { if object.Type == "TextObject" {
all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0 && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0) all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0 && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
all.Reflow = all.Reflow && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0) all.Reflow = all.Reflow && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
@@ -949,6 +956,17 @@ func validateEditorGeometry(object GraphicObject) error {
return err return err
} }
} }
if clips := *editorObjectClips(&object); clips != nil {
for _, clip := range clips.Clip {
for _, area := range clip.Area {
if area.CTM != "" {
if _, err := creationNumbers(area.CTM, 6); err != nil {
return fmt.Errorf("invalid clip transform: %w", err)
}
}
}
}
}
var alpha *int var alpha *int
switch object.Type { switch object.Type {
case "TextObject": case "TextObject":
+84 -54
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"bytes" "bytes"
"context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"slices" "slices"
@@ -146,7 +147,11 @@ func (e *Editor) UpdateOutline(path []int, title string, page int) error {
} }
content = append(content, standalone...) content = append(content, standalone...)
} }
patches = append(patches, editorXMLContent(data, actions, content)) if page < 0 && len(removals) == len(actions.children) {
patches = append(patches, editorXMLPatch{actions.start, actions.end, nil})
} else {
patches = append(patches, editorXMLContent(data, actions, content))
}
} else if page >= 0 { } else if page >= 0 {
content, err := editorXMLGenerated(e.outlineAction(page), node.name.Space) content, err := editorXMLGenerated(e.outlineAction(page), node.name.Space)
if err != nil { if err != nil {
@@ -167,11 +172,11 @@ func (e *Editor) UpdateOutline(path []int, title string, page int) error {
if err != nil { if err != nil {
return err return err
} }
value, err := editorXMLAttribute(updated, node, "Title", title) patch, err := editorXMLAttributePatch(updated, node, "Title", title)
if err != nil { if err != nil {
return err return err
} }
return e.setOutlineXML(editorPatchXML(updated, []editorXMLPatch{{node.start, node.end, value}})) return e.setOutlineXML(editorPatchXML(updated, []editorXMLPatch{patch}))
} }
// SetOutlineActions 替换目录节点的动作序列,空列表清除动作,不改变子目录 // SetOutlineActions 替换目录节点的动作序列,空列表清除动作,不改变子目录
@@ -391,50 +396,34 @@ func (e *Editor) setOutlineXML(data []byte) error {
return err return err
} }
var patches []editorXMLPatch var patches []editorXMLPatch
var leaves func(*editorXML) int var walk func(*editorXML) (int, error)
leaves = func(node *editorXML) int { walk = func(node *editorXML) (int, error) {
children := editorOutlineChildren(node)
if len(children) == 0 {
return 1
}
count := 0 count := 0
for _, child := range children { for _, child := range node.children {
count += leaves(child) if !child.matchesOFD("OutlineElem") {
} continue
return count
}
var walk func(*editorXML) error
walk = func(node *editorXML) error {
if node.attr("Count") != "" {
count := 0
for _, child := range editorOutlineChildren(node) {
count += leaves(child)
} }
value, err := editorXMLAttribute(data, node, "Count", strconv.Itoa(count)) leaves, err := walk(child)
if err != nil { if err != nil {
return err return 0, err
} }
updated, err := parseEditorXML(value) count += leaves
}
if value := strconv.Itoa(count); node.attr("Count") != "" && node.attr("Count") != value {
patch, err := editorXMLAttributePatch(data, node, "Count", value)
if err != nil { if err != nil {
return err return 0, err
}
if node.open != node.end {
patches = append(patches, editorXMLPatch{node.start, node.open, value[:updated.open]})
} else {
patches = append(patches, editorXMLPatch{node.start, node.end, value})
} }
patches = append(patches, patch)
} }
for _, child := range editorOutlineChildren(node) { return max(1, count), nil
if err := walk(child); err != nil {
return err
}
}
return nil
} }
if err := walk(root); err != nil { if _, err := walk(root); err != nil {
return err return err
} }
data = editorPatchXML(data, patches) if len(patches) > 0 {
data = editorPatchXML(data, patches)
}
current, err := e.outlineXML() current, err := e.outlineXML()
if err != nil { if err != nil {
return err return err
@@ -453,52 +442,93 @@ func (e *Editor) setOutlineXML(data []byte) error {
// withOutlines 将修改后的目录写入文档,清除指向已删除页面的新增跳转 // withOutlines 将修改后的目录写入文档,清除指向已删除页面的新增跳转
// 入参: data 文档原文 // 入参: data 文档原文
// 返回: []byte 更新文档, error 错误信息 // 返回: []byte 更新文档, error 解析或取消错误
func (e *Editor) withOutlines(data []byte) ([]byte, error) { func (e *Editor) withOutlines(data []byte) ([]byte, error) {
if e.outlines == nil { if e.outlines == nil {
return data, nil return data, nil
} }
outline, err := parseEditorXML(e.outlines) ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
outline, err := parseEditorXMLContext(ctx, e.outlines)
if err != nil { if err != nil {
return nil, err return nil, err
} }
valid := make(map[string]int, len(e.pages)) valid := make(map[string]bool, len(e.pages))
for i, page := range e.pages { for _, page := range e.pages {
valid[page.ID] = i if err := ctx.Err(); err != nil {
return nil, err
}
if id := editorResourceID(page.ID); id != "" {
valid[id] = true
}
} }
var removals []editorXMLPatch var removals []editorXMLPatch
var walk func(*editorXML) var walk func(*editorXML) error
walk = func(node *editorXML) { walk = func(node *editorXML) error {
if err := ctx.Err(); err != nil {
return err
}
if actions := node.child("Actions"); actions != nil { if actions := node.child("Actions"); actions != nil {
start := len(removals)
for _, action := range actions.children { for _, action := range actions.children {
if err := ctx.Err(); err != nil {
return err
}
if !action.matchesOFD("Action") { if !action.matchesOFD("Action") {
continue continue
} }
if target := action.child("Goto"); target != nil { if target := action.child("Goto"); target != nil {
if dest := target.child("Dest"); dest != nil { if dest := target.child("Dest"); dest != nil {
if _, ok := resourceValue(valid, dest.attr("PageID")); !ok { if !valid[editorResourceID(dest.attr("PageID"))] {
removals = append(removals, editorXMLPatch{action.start, action.end, nil}) removals = append(removals, editorXMLPatch{action.start, action.end, nil})
} }
} }
} }
} }
if len(removals) > start && len(removals)-start == len(actions.children) {
removals = append(removals[:start], editorXMLPatch{actions.start, actions.end, nil})
}
} }
for _, child := range editorOutlineChildren(node) { for _, child := range editorOutlineChildren(node) {
walk(child) if err := walk(child); err != nil {
return err
}
} }
return nil
} }
walk(outline) if err := walk(outline); err != nil {
value := editorPatchXML(e.outlines, removals) return nil, err
root, err := parseEditorXML(data) }
value := e.outlines
if len(outline.children) == 0 {
value = nil
} else if len(removals) > 0 {
value = editorPatchXML(value, removals)
}
root, err := parseEditorXMLContext(ctx, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var patch editorXMLPatch
if node := root.child("Outlines"); node != nil { if node := root.child("Outlines"); node != nil {
return editorPatchXML(data, []editorXMLPatch{{node.start, node.end, value}}), nil patch = editorXMLPatch{node.start, node.end, value}
} else if len(value) > 0 {
pages := root.child("Pages")
if pages == nil {
return nil, fmt.Errorf("document has no Pages")
}
patch = editorXMLPatch{pages.end, pages.end, value}
} else {
if err := ctx.Err(); err != nil {
return nil, err
}
return data, nil
} }
pages := root.child("Pages") data = editorPatchXML(data, []editorXMLPatch{patch})
if pages == nil { if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("document has no Pages") return nil, err
} }
return editorPatchXML(data, []editorXMLPatch{{pages.end, pages.end, value}}), nil return data, nil
} }
+8 -4
View File
@@ -28,7 +28,7 @@ import (
"strings" "strings"
) )
// sourceParts 生成编辑快照的XML和新增资源,删页时同步清理标准引用 // sourceParts 生成编辑快照的XML和新增资源,同步清理已删除页面及附件的引用
// 入参: ctx 取消上下文, progress 保存进度回调,预览时为nil // 入参: ctx 取消上下文, progress 保存进度回调,预览时为nil
// 返回: map[string][]byte 替换及新增条目, error 错误信息 // 返回: map[string][]byte 替换及新增条目, error 错误信息
func (e *Editor) sourceParts(ctx context.Context, progress editorProgress) (map[string][]byte, error) { func (e *Editor) sourceParts(ctx context.Context, progress editorProgress) (map[string][]byte, error) {
@@ -135,7 +135,11 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
if err != nil { if err != nil {
return nil, err return nil, err
} }
resourcePath, added, err := mergePackageResources(reader, parts, source.document, source.directory, data) ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
resourcePath, added, err := mergePackageResources(ctx, reader, parts, source.document, source.directory, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -263,8 +267,8 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
} }
parts[name] = updated parts[name] = updated
} }
if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 { if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 || len(source.removedAttachments) != 0 {
if err := e.prunePageReferences(parts, generated); err != nil { if err := e.pruneDocumentReferences(parts, generated); err != nil {
return nil, err return nil, err
} }
} }
+85 -41
View File
@@ -15,18 +15,21 @@
package ofdgo package ofdgo
import ( import (
"bytes"
"context"
"encoding/xml"
"fmt" "fmt"
"io" "io"
"maps" "maps"
"strings" "strings"
) )
// prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档 // pruneDocumentReferences 在输出快照中清理已删除页面和附件的引用,不修改历史快照或其他文档
// 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据 // 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
// 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据 // 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据
// 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil // 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil
// 返回: error 读取或解析错误 // 返回: error 读取、解析或取消错误
func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error { func (e *Editor) pruneDocumentReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
removed := maps.Clone(e.removedPages) removed := maps.Clone(e.removedPages)
if removed == nil { if removed == nil {
removed = make(map[string]bool) removed = make(map[string]bool)
@@ -41,9 +44,13 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
delete(removed, editorResourceID(page.ID)) delete(removed, editorResourceID(page.ID))
} }
delete(removed, "") delete(removed, "")
if len(removed) == 0 { if len(removed) == 0 && len(e.source.removedAttachments) == 0 {
return nil return nil
} }
ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
reader := e.source.reader reader := e.source.reader
bookmarks := make(map[string]bool) bookmarks := make(map[string]bool)
for _, bookmark := range e.source.document.Bookmarks.Bookmark { for _, bookmark := range e.source.document.Bookmarks.Bookmark {
@@ -52,15 +59,13 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
} }
} }
queue := []string{reader.documentRoot} queue := []string{reader.documentRoot}
if name := e.source.document.Signatures; name != "" && !reader.OFD.DocBody[reader.documentIndex].versioned { if name := e.source.document.Signatures; len(removed) != 0 && 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)
for len(queue) > 0 { for len(queue) > 0 {
if e.output != nil { if err := ctx.Err(); err != nil {
if err := e.output.ctx.Err(); err != nil { return err
return err
}
} }
name := queue[0] name := queue[0]
queue = queue[1:] queue = queue[1:]
@@ -71,15 +76,12 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
data, ok := parts[name] data, ok := parts[name]
if !ok { if !ok {
var err error var err error
data, err = reader.readFile(name) data, err = reader.readFileViewContext(ctx, name)
if err != nil { if err != nil {
return err return err
} }
} else if known := generated[name]; data == nil && known.staged != nil { } else if known := generated[name]; data == nil && known.staged != nil {
input := known.staged.open() input := imageInput{ReadCloser: known.staged.open(), context: ctx}
if e.output != nil {
input = imageInput{ReadCloser: input, context: e.output.ctx}
}
var err error var err error
data, err = io.ReadAll(input) data, err = io.ReadAll(input)
input.Close() input.Close()
@@ -87,17 +89,15 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
return err return err
} }
} }
root, err := parseEditorXML(data) root, err := parseEditorXMLContext(ctx, data)
if err != nil { if err != nil {
return err return err
} }
var patches []editorXMLPatch var patches []editorXMLPatch
var walk func(*editorXML) error var walk func(*editorXML) error
walk = func(node *editorXML) error { walk = func(node *editorXML) error {
if e.output != nil { if err := ctx.Err(); err != nil {
if err := e.output.ctx.Err(); err != nil { return err
return err
}
} }
if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown { if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown {
return nil return nil
@@ -111,6 +111,8 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
} else if bookmark := target.child("Bookmark"); bookmark != nil { } else if bookmark := target.child("Bookmark"); bookmark != nil {
remove = bookmarks[bookmark.attr("Name")] remove = bookmarks[bookmark.attr("Name")]
} }
} else if attachment := node.child("GotoA"); attachment != nil {
remove = e.source.removedAttachments[attachment.attr("AttachID")]
} }
case "Bookmark": case "Bookmark":
if node.parent != nil && node.parent.name.Local == "Bookmarks" { if node.parent != nil && node.parent.name.Local == "Bookmarks" {
@@ -180,37 +182,79 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
delete(generated, name) delete(generated, name)
} }
} }
return nil return ctx.Err()
}
// pageReferenceNode 记录跳转引用扫描中的父节点及删除范围
type pageReferenceNode struct {
name string
start, patch int
children, removed int
standard, drop bool
} }
// pruneCreatedPageReferences 清理创作文档中被明确删除页面的直接跳转,不检查其他目标是否可达 // pruneCreatedPageReferences 清理创作文档中被明确删除页面的直接跳转,不检查其他目标是否可达
// 入参: data 创作XML, removed 已删除页面标识 // 入参: ctx 取消上下文, data 创作XML, removed 已删除页面标识
// 返回: []byte 更新后的XML, error 解析错误 // 返回: []byte 更新后的XML, error 解析或取消错误
func pruneCreatedPageReferences(data []byte, removed map[string]bool) ([]byte, error) { func pruneCreatedPageReferences(ctx context.Context, data []byte, removed map[string]bool) ([]byte, error) {
root, err := parseEditorXML(data) d := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err != nil { var stack []pageReferenceNode
return nil, err
}
var patches []editorXMLPatch var patches []editorXMLPatch
var walk func(*editorXML) root := false
walk = func(node *editorXML) { for {
if node.name.Space != ofdNamespace2016 { if err := ctx.Err(); err != nil {
return return nil, err
} }
if node.name.Local == "Action" { start := int(d.InputOffset())
if target := node.child("Goto"); target != nil { token, err := d.Token()
if dest := target.child("Dest"); dest != nil && removed[editorResourceID(dest.attr("PageID"))] { if err == io.EOF {
patches = append(patches, editorXMLPatch{start: node.start, end: node.end}) break
return }
if err != nil {
return nil, err
}
switch token := token.(type) {
case xml.StartElement:
node := pageReferenceNode{name: token.Name.Local, start: start, patch: len(patches), standard: token.Name.Space == ofdNamespace2016}
if len(stack) == 0 {
if root {
return nil, fmt.Errorf("multiple XML roots")
}
root = true
} else {
parent := &stack[len(stack)-1]
parent.children++
node.standard = node.standard && parent.standard
}
if node.standard && node.name == "Dest" && len(stack) >= 2 && stack[len(stack)-1].name == "Goto" && stack[len(stack)-2].name == "Action" {
for _, attr := range token.Attr {
if attr.Name.Space == "" && attr.Name.Local == "PageID" && removed[editorResourceID(attr.Value)] {
stack[len(stack)-2].drop = true
}
}
}
stack = append(stack, node)
case xml.EndElement:
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if node.drop || node.standard && node.name == "Actions" && node.removed > 0 && node.children == node.removed {
patches = append(patches[:node.patch], editorXMLPatch{start: node.start, end: int(d.InputOffset())})
if len(stack) > 0 {
stack[len(stack)-1].removed++
} }
} }
} }
for _, child := range node.children {
walk(child)
}
} }
walk(root) if !root {
return editorPatchXML(data, patches), nil return nil, fmt.Errorf("empty XML document")
}
if len(patches) > 0 {
data = editorPatchXML(data, patches)
}
if err := ctx.Err(); err != nil {
return nil, err
}
return data, nil
} }
// editorPageLocation 按所在XML解析引用,绝对路径以包根目录为基准 // editorPageLocation 按所在XML解析引用,绝对路径以包根目录为基准
+39 -13
View File
@@ -20,7 +20,13 @@ import (
"slices" "slices"
) )
// resourceReferenceScopes 复用结构依赖归集文档引用,同文档索引共用只读结果,跨文档索引独立合并 // editorResourceScope 记录资源索引的文档归属链,引用集合只读
type editorResourceScope struct {
parent int
refs *editorResourceRefs
}
// resourceReferenceScopes 按文档归集引用,归属相同的资源索引复用只读结果
// 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 文件引用, selected 限定的文档入口,空值统计全部文档 // 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 文件引用, selected 限定的文档入口,空值统计全部文档
// 返回: map[string]*editorResourceRefs 各资源索引的引用,依赖不完整或引用无法归属时为空, error 取消错误 // 返回: map[string]*editorResourceRefs 各资源索引的引用,依赖不完整或引用无法归属时为空, error 取消错误
func resourceReferenceScopes(ctx context.Context, documents map[string]bool, resources []string, references map[string]*editorResourceRefs, selected string) (map[string]*editorResourceRefs, error) { func resourceReferenceScopes(ctx context.Context, documents map[string]bool, resources []string, references map[string]*editorResourceRefs, selected string) (map[string]*editorResourceRefs, error) {
@@ -29,8 +35,9 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
indexes[name] = true indexes[name] = true
} }
used := make(map[string]bool) used := make(map[string]bool)
result := make(map[string]*editorResourceRefs) owners := make(map[string]int)
shared := make(map[string]bool) groups := []editorResourceScope{{}}
groupIDs := make(map[editorResourceScope]int)
for _, entry := range slices.Sorted(maps.Keys(documents)) { for _, entry := range slices.Sorted(maps.Keys(documents)) {
scope := map[string]bool{entry: true} scope := map[string]bool{entry: true}
queue := []string{entry} queue := []string{entry}
@@ -89,17 +96,14 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
if !indexes[name] { if !indexes[name] {
continue continue
} }
if result[name] == nil { key := editorResourceScope{parent: owners[name], refs: refs}
result[name] = refs id := groupIDs[key]
continue if id == 0 {
id = len(groups)
groups = append(groups, key)
groupIDs[key] = id
} }
if !shared[name] { owners[name] = id
merged := &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
merged.merge(result[name])
result[name] = merged
shared[name] = true
}
result[name].merge(refs)
} }
} }
for name, refs := range references { for name, refs := range references {
@@ -107,5 +111,27 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
return nil, nil return nil, nil
} }
} }
result := make(map[string]*editorResourceRefs, len(owners))
merged := make(map[int]*editorResourceRefs)
for name, id := range owners {
if err := ctx.Err(); err != nil {
return nil, err
}
refs := merged[id]
if refs == nil {
refs = groups[id].refs
if groups[id].parent != 0 {
refs = &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
for next := id; next != 0; next = groups[next].parent {
if err := ctx.Err(); err != nil {
return nil, err
}
refs.merge(groups[next].refs)
}
}
merged[id] = refs
}
result[name] = refs
}
return result, ctx.Err() return result, ctx.Err()
} }
+15 -4
View File
@@ -72,6 +72,7 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
if e.output != nil { if e.output != nil {
ctx = e.output.ctx ctx = e.output.ctx
} }
progress = progress.withContext(ctx)
scope, err := reader.versionResourceFiles(ctx, parts, generated) scope, err := reader.versionResourceFiles(ctx, parts, generated)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -172,14 +173,14 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
data, ok := parts[name] data, ok := parts[name]
if !ok { if !ok {
var err error var err error
data, err = reader.readFile(name) data, err = reader.readFileViewContext(ctx, name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
root, err := parseEditorXML(data) root, err := parseEditorXMLContext(ctx, data)
if err != nil { if err != nil {
return nil, nil return nil, ctx.Err()
} }
usageRefs := refs usageRefs := refs
if multiple || prune { if multiple || prune {
@@ -190,6 +191,9 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
for _, group := range root.children { for _, group := range root.children {
var members []editorXMLPatch var members []editorXMLPatch
for _, node := range group.children { for _, node := range group.children {
if err := ctx.Err(); err != nil {
return nil, err
}
field := "" field := ""
if group.name.Local == "Fonts" && node.name.Local == "Font" { if group.name.Local == "Fonts" && node.name.Local == "Font" {
field = "FontFile" field = "FontFile"
@@ -264,6 +268,7 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
for name := range usedFiles { for name := range usedFiles {
delete(removed, name) delete(removed, name)
} }
var fontCache editorSourceFontCache
for i, name := range ordered { for i, name := range ordered {
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
@@ -277,10 +282,16 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
if err != nil { if err != nil {
return nil, err return nil, err
} }
subset, err := e.backends.FontResources.SubsetSourceFont(data, FontUsage{Characters: slices.Sorted(maps.Keys(usage.chars)), Glyphs: slices.Sorted(maps.Keys(usage.glyphs)), Unsafe: usage.unsafe}) if err := ctx.Err(); err != nil {
return nil, err
}
subset, err := fontCache.subset(ctx, e.backends.FontResources, data, usage)
if err != nil { if err != nil {
return nil, fmt.Errorf("subset font %s: %w", name, err) return nil, fmt.Errorf("subset font %s: %w", name, err)
} }
if err := ctx.Err(); err != nil {
return nil, err
}
if subset != nil { if subset != nil {
updates[name] = subset updates[name] = subset
} }
+1 -1
View File
@@ -42,7 +42,7 @@ func (e *Editor) SelectionBounds(page int, objects, annotations []string) (Box,
// 入参: page 页面索引, objects 正文标识, annotations 注解标识, matrix 有限可逆变换 // 入参: page 页面索引, objects 正文标识, annotations 注解标识, matrix 有限可逆变换
// 返回: error 标识、变换或编辑错误 // 返回: error 标识、变换或编辑错误
func (e *Editor) TransformSelection(page int, objects, annotations []string, matrix Matrix) error { func (e *Editor) TransformSelection(page int, objects, annotations []string, matrix Matrix) error {
if _, ok := matrix.Invert(); !ok || !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) || !finite(matrix.e) || !finite(matrix.f) { if _, ok := matrix.Invert(); !ok {
return fmt.Errorf("selection transform must be finite and invertible") return fmt.Errorf("selection transform must be finite and invertible")
} }
if matrix == IdentityMatrix { if matrix == IdentityMatrix {
+60 -24
View File
@@ -26,7 +26,7 @@ import (
// 入参: page 页面索引, ids 对象标识, matrix 页面变换 // 入参: page 页面索引, ids 对象标识, matrix 页面变换
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) TransformObjectsMatrix(page int, ids []string, matrix Matrix) error { func (e *Editor) TransformObjectsMatrix(page int, ids []string, matrix Matrix) error {
if _, ok := matrix.Invert(); !ok || !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) || !finite(matrix.e) || !finite(matrix.f) { if _, ok := matrix.Invert(); !ok {
return fmt.Errorf("object transform must be finite and invertible") return fmt.Errorf("object transform must be finite and invertible")
} }
objects, _, err := e.selectedObjects(page, ids) objects, _, err := e.selectedObjects(page, ids)
@@ -149,7 +149,7 @@ func (e *Editor) CropImage(page int, id string, box Box) error {
return e.UpdateObject(page, id, object) return e.UpdateObject(page, id, object)
} }
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留直角旋转及镜像方向 // FitImage 按原始像素比例居中适应或填充当前Boundary,保留旋转、镜像及斜切方向
// 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局 // 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局
// 入参: page 页面索引, id 图片对象标识, mode 为contain(完整显示)或cover(填满裁剪) // 入参: page 页面索引, id 图片对象标识, mode 为contain(完整显示)或cover(填满裁剪)
// 返回: error 错误信息 // 返回: error 错误信息
@@ -185,7 +185,11 @@ func (obj TextObject) TextFrame() (Box, error) {
if !ok { if !ok {
return Box{}, fmt.Errorf("text transform is not invertible") return Box{}, fmt.Errorf("text transform is not invertible")
} }
return m.TransformBox(Box{W: box.W, H: box.H}), nil frame := m.TransformBox(Box{W: box.W, H: box.H})
if !finite(frame.X) || !finite(frame.Y) || !finite(frame.W) || !finite(frame.H) || frame.W <= 0 || frame.H <= 0 {
return Box{}, fmt.Errorf("text frame exceeds finite range")
}
return frame, nil
} }
// ResizeTextFrame 调整文字本地坐标的左侧偏移及宽度,保留页面方向;重新排版后通过UpdateObject提交 // ResizeTextFrame 调整文字本地坐标的左侧偏移及宽度,保留页面方向;重新排版后通过UpdateObject提交
@@ -208,7 +212,13 @@ func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error)
frame.X += offset frame.X += offset
frame.W = width frame.W = width
box := m.TransformBox(frame) box := m.TransformBox(frame)
if !finite(box.X) || !finite(box.Y) || !finite(box.W) || !finite(box.H) || box.W <= 0 || box.H <= 0 {
return TextObject{}, fmt.Errorf("resized text frame exceeds finite range")
}
obj.CTM = TranslationMatrix(-box.X, -box.Y).Multiply(m).Multiply(TranslationMatrix(offset, 0)).String() obj.CTM = TranslationMatrix(-box.X, -box.Y).Multiply(m).Multiply(TranslationMatrix(offset, 0)).String()
if _, err := creationNumbers(obj.CTM, 6); err != nil {
return TextObject{}, err
}
obj.Boundary = editorBoxString(box) obj.Boundary = editorBoxString(box)
return obj, nil return obj, nil
} }
@@ -536,7 +546,29 @@ func cropImageObject(image *ImageObject, box Box) error {
return nil return nil
} }
// fitImage 将原始像素比例应用到当前图片框,适应时保留空白,填充时裁掉超出部分 // imageFitBasis 提取图片变换的单位轴方向,保留旋转、镜像和斜切夹角
// 入参: ctm 原始变换,空值使用默认方向
// 返回: Matrix 单位轴矩阵, error 无效或退化变换
func imageFitBasis(ctm string) (Matrix, error) {
if ctm != "" {
if _, err := creationNumbers(ctm, 6); err != nil {
return Matrix{}, err
}
}
m := NewMatrix(ctm)
x, y := math.Hypot(m.a, m.b), math.Hypot(m.c, m.d)
if x == 0 || y == 0 || !finite(x) || !finite(y) {
return Matrix{}, fmt.Errorf("image fitting requires nonzero finite axes")
}
m.a, m.b, m.c, m.d = m.a/x, m.b/x, m.c/y, m.d/y
m.e, m.f = 0, 0
if _, ok := m.Invert(); !ok {
return Matrix{}, fmt.Errorf("image fitting requires an invertible transform")
}
return m, nil
}
// fitImage 按原像素比例和轴方向布局,适应使用变换后边界,填充使用逆变换后的框范围
// 入参: obj 图片对象副本, mode 为contain或cover // 入参: obj 图片对象副本, mode 为contain或cover
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) fitImage(obj *ImageObject, mode string) error { func (e *Editor) fitImage(obj *ImageObject, mode string) error {
@@ -547,34 +579,38 @@ func (e *Editor) fitImage(obj *ImageObject, mode string) error {
if err != nil { if err != nil {
return err return err
} }
m := NewMatrix(obj.CTM) m, err := imageFitBasis(obj.CTM)
if obj.CTM == "" { if err != nil {
m = Matrix{a: box.W, d: box.H} return err
}
if !axisAlignedMatrix(m) {
return fmt.Errorf("image fitting requires an axis-aligned transform")
} }
size, err := e.editorImage(obj.ResourceID) size, err := e.editorImage(obj.ResourceID)
if err != nil { if err != nil {
return err return err
} }
w, h := float64(size.X), float64(size.Y) w, h := float64(size.X), float64(size.Y)
if m.a == 0 { m.a, m.b, m.c, m.d = m.a*w, m.b*w, m.c*h, m.d*h
w, h = h, w
}
scale := math.Min(box.W/w, box.H/h)
if mode == "cover" {
scale = math.Max(box.W/w, box.H/h)
}
if m.a == 0 {
m.b, m.c = math.Copysign(float64(size.X)*scale, m.b), math.Copysign(float64(size.Y)*scale, m.c)
} else {
m.a, m.d = math.Copysign(w*scale, m.a), math.Copysign(h*scale, m.d)
}
m.e, m.f = 0, 0
full := m.TransformBox(Box{W: 1, H: 1}) full := m.TransformBox(Box{W: 1, H: 1})
scale := math.Min(box.W/full.W, box.H/full.H)
if mode == "cover" {
inverse, _ := m.Invert()
frame := inverse.TransformBox(Box{W: box.W, H: box.H})
scale = math.Max(frame.W, frame.H)
}
m.a, m.b, m.c, m.d = m.a*scale, m.b*scale, m.c*scale, m.d*scale
full = m.TransformBox(Box{W: 1, H: 1})
m.e, m.f = (box.W-full.W)/2-full.X, (box.H-full.H)/2-full.Y m.e, m.f = (box.W-full.W)/2-full.X, (box.H-full.H)/2-full.Y
obj.CTM, obj.Clips = m.String(), nil values, original := m.Values(), NewMatrix(obj.CTM).Values()
for _, value := range values {
if !finite(value) {
return fmt.Errorf("image fit exceeds finite range")
}
}
if obj.CTM == "" || !slices.EqualFunc(values[:], original[:], func(a, b float64) bool {
return math.Abs(a-b) <= 1e-12*max(box.W, box.H, math.Abs(a), math.Abs(b))
}) {
obj.CTM = m.String()
}
obj.Clips = nil
if mode == "cover" { if mode == "cover" {
return cropImageObject(obj, box) return cropImageObject(obj, box)
} }
+48
View File
@@ -697,6 +697,54 @@ func editorXMLEncodedFragment(data []byte, node *editorXML, namespace string) ([
return editorXMLGenerated(fragment, namespace) return editorXMLGenerated(fragment, namespace)
} }
// editorXMLAttributePatch 生成标准属性的起始标签补丁,不复制子树
// 入参: data 原文, node 节点, name 属性名, value 新值
// 返回: editorXMLPatch 标签补丁, error 编码错误
func editorXMLAttributePatch(data []byte, node *editorXML, name, value string) (editorXMLPatch, error) {
patch := editorXMLPatch{start: node.start, end: node.open, data: data[node.start:node.open]}
if node.hasAttr(name) && node.attr(name) == value {
return patch, nil
}
d := xml.NewDecoder(bytes.NewReader(patch.data))
token, err := d.RawToken()
if err != nil {
return editorXMLPatch{}, err
}
start := token.(xml.StartElement)
found := false
for i := range start.Attr {
if attr := &start.Attr[i]; attr.Name.Space == "" && attr.Name.Local == name {
attr.Value, found = value, true
}
}
if !found {
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: name}, Value: value})
}
if start.Name.Space != "" {
start.Name.Local = start.Name.Space + ":" + start.Name.Local
start.Name.Space = ""
}
for i := range start.Attr {
if attr := &start.Attr[i]; attr.Name.Space != "" {
attr.Name.Local = attr.Name.Space + ":" + attr.Name.Local
attr.Name.Space = ""
}
}
var output bytes.Buffer
encoder := xml.NewEncoder(&output)
if err := encoder.EncodeToken(start); err != nil {
return editorXMLPatch{}, err
}
if err := encoder.Flush(); err != nil {
return editorXMLPatch{}, err
}
patch.data = output.Bytes()
if node.open == node.end {
patch.data = append(patch.data[:len(patch.data)-1], '/', '>')
}
return patch, nil
}
// editorXMLAttribute 修改单个标准属性,保留节点其余原文 // editorXMLAttribute 修改单个标准属性,保留节点其余原文
// 入参: data 原文, node 节点, name 属性名, value 新值 // 入参: data 原文, node 节点, name 属性名, value 新值
// 返回: []byte 修改后的节点, error 错误信息 // 返回: []byte 修改后的节点, error 错误信息
+69 -12
View File
@@ -133,21 +133,78 @@ func (m Matrix) String() string {
return fmt.Sprintf("%s %s %s %s %s %s", ofdNumber(m.a), ofdNumber(m.b), ofdNumber(m.c), ofdNumber(m.d), ofdNumber(m.e), ofdNumber(m.f)) return fmt.Sprintf("%s %s %s %s %s %s", ofdNumber(m.a), ofdNumber(m.b), ofdNumber(m.c), ofdNumber(m.d), ofdNumber(m.e), ofdNumber(m.f))
} }
// Invert 求逆矩阵 // Invert 求仿射矩阵的逆变换
// 返回: Matrix 逆矩阵, bool 是否可逆 // 返回: Matrix 逆矩阵,失败时返回单位矩阵, bool 是否存在有限逆矩阵
func (m Matrix) Invert() (Matrix, bool) { func (m Matrix) Invert() (Matrix, bool) {
det := m.a*m.d - m.b*m.c for _, value := range m.Values() {
if math.Abs(det) < 1e-9 { if !finite(value) {
return IdentityMatrix, false
}
}
det, ds := matrixProductDifference(m.a, m.d, m.b, m.c)
x, xs := matrixProductDifference(m.c, m.f, m.d, m.e)
y, ys := matrixProductDifference(m.b, m.e, m.a, m.f)
if det != 0 && ds && xs && ys {
n := Matrix{a: m.d / det, b: -m.b / det, c: -m.c / det, d: m.a / det, e: x / det, f: y / det}
valid := true
for _, value := range n.Values() {
valid = valid && finite(value)
}
if valid {
return n, true
}
}
determinant, exponent := matrixProductSum(m.a, m.d, -m.b, m.c)
if determinant == 0 {
return IdentityMatrix, false return IdentityMatrix, false
} }
return Matrix{ divide := func(value float64, power int) float64 {
a: m.d / det, mantissa, shift := math.Frexp(value)
b: -m.b / det, return math.Ldexp(mantissa/determinant, power+shift-exponent)
c: -m.c / det, }
d: m.a / det, x, xe := matrixProductSum(m.c, m.f, -m.d, m.e)
e: (m.c*m.f - m.d*m.e) / det, y, ye := matrixProductSum(m.b, m.e, -m.a, m.f)
f: (m.b*m.e - m.a*m.f) / det, n := Matrix{a: divide(m.d, 0), b: divide(-m.b, 0), c: divide(-m.c, 0), d: divide(m.a, 0), e: divide(x, xe), f: divide(y, ye)}
}, true for _, value := range n.Values() {
if !finite(value) {
return IdentityMatrix, false
}
}
return n, true
}
// matrixProductDifference 计算两个乘积之差,检查溢出、下溢及相消造成的精度损失
// 入参: a 第一个乘数, b 第二个乘数, c 第三个乘数, d 第四个乘数
// 返回: float64 乘积之差, bool 是否可直接用于求逆
func matrixProductDifference(a, b, c, d float64) (float64, bool) {
x, y := a*b, c*d
value := x - y
if !finite(value) || x == 0 && a != 0 && b != 0 || y == 0 && c != 0 && d != 0 {
return value, false
}
return value, x == 0 && y == 0 || math.Abs(value) >= 0x1p-1022 && math.Abs(value) >= 0x1p-48*math.Max(math.Abs(x), math.Abs(y))
}
// matrixProductSum 以尾数及指数累加两个乘积,保留乘法舍入残差
// 入参: a 第一个乘数, b 第二个乘数, c 第三个乘数, d 第四个乘数
// 返回: float64 结果尾数, int 二进制指数
func matrixProductSum(a, b, c, d float64) (float64, int) {
a, ae := math.Frexp(a)
b, be := math.Frexp(b)
c, ce := math.Frexp(c)
d, de := math.Frexp(d)
x, y := a*b, c*d
xe, ye := ae+be, ce+de
power := max(xe, ye)
if x == 0 {
power = ye
} else if y == 0 {
power = xe
}
value := math.Ldexp(x, xe-power) + math.Ldexp(y, ye-power)
value += math.Ldexp(math.FMA(a, b, -x), xe-power) + math.Ldexp(math.FMA(c, d, -y), ye-power)
value, shift := math.Frexp(value)
return value, power + shift
} }
// TranslationMatrix 构造平移矩阵 // TranslationMatrix 构造平移矩阵
+31 -12
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"bytes" "bytes"
"context"
"path" "path"
"slices" "slices"
"strconv" "strconv"
@@ -78,9 +79,12 @@ func packageAvailableName(reader *Reader, files map[string][]byte, name string)
} }
// mergePackageResources 合并新增资源定义,保留原索引、路径基准与未知字段 // mergePackageResources 合并新增资源定义,保留原索引、路径基准与未知字段
// 入参: reader 原包, parts 待写入条目, doc 文档, directory 文档目录, added 新增资源XML,文件引用使用绝对包路径 // 入参: ctx 取消上下文, reader 原包, parts 待写入条目, doc 文档, directory 文档目录, added 新增资源XML,文件引用使用绝对包路径
// 返回: string 资源索引路径, bool 是否需要新增文档引用, error 错误信息 // 返回: string 资源索引路径, bool 是否需要新增文档引用, error 读取、解析或取消错误
func mergePackageResources(reader *Reader, parts map[string][]byte, doc *Document, directory string, added []byte) (string, bool, error) { func mergePackageResources(ctx context.Context, reader *Reader, parts map[string][]byte, doc *Document, directory string, added []byte) (string, bool, error) {
if err := ctx.Err(); err != nil {
return "", false, err
}
if len(doc.CommonData.DocumentRes) == 0 { if len(doc.CommonData.DocumentRes) == 0 {
name := packageAvailableName(reader, parts, path.Join(directory, "DocumentRes.xml")) name := packageAvailableName(reader, parts, path.Join(directory, "DocumentRes.xml"))
parts[name] = added parts[name] = added
@@ -93,42 +97,54 @@ func mergePackageResources(reader *Reader, parts map[string][]byte, doc *Documen
data, ok := parts[name] data, ok := parts[name]
if !ok { if !ok {
var err error var err error
data, err = reader.readFile(name) data, err = reader.readFileViewContext(ctx, name)
if err != nil { if err != nil {
return "", false, err return "", false, err
} }
} }
addition, err := parseEditorXML(added) addition, err := parseEditorXMLContext(ctx, added)
if err != nil { if err != nil {
return "", false, err return "", false, err
} }
order := []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"} order := []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"}
for _, group := range addition.children { for _, group := range addition.children {
root, err := parseEditorXML(data) root, err := parseEditorXMLContext(ctx, data)
if err != nil { if err != nil {
return "", false, err return "", false, err
} }
target := root.child(group.name.Local) target := root.child(group.name.Local)
context := root scope := root
used := make(map[[2]string]bool)
if target != nil { if target != nil {
context = target scope = target
for _, old := range target.children {
if err := ctx.Err(); err != nil {
return "", false, err
}
if old.matchesOFD(old.name.Local) {
used[[2]string{old.name.Local, editorImportKey(old.attr("ID"))}] = true
}
}
} }
var content []byte var content []byte
for _, node := range group.children { for _, node := range group.children {
if target != nil && slices.ContainsFunc(target.children, func(old *editorXML) bool { if err := ctx.Err(); err != nil {
return old.matchesOFD(node.name.Local) && old.attr("ID") == node.attr("ID") return "", false, err
}) { }
key := [2]string{node.name.Local, editorImportKey(node.attr("ID"))}
if used[key] {
continue continue
} }
entry, err := editorXMLStandalone(added[node.start:node.end], node) entry, err := editorXMLStandalone(added[node.start:node.end], node)
if err != nil { if err != nil {
return "", false, err return "", false, err
} }
entry, err = editorXMLGenerated(entry, context.name.Space) entry, err = editorXMLGenerated(entry, scope.name.Space)
if err != nil { if err != nil {
return "", false, err return "", false, err
} }
content = append(content, entry...) content = append(content, entry...)
used[key] = true
} }
if len(content) == 0 { if len(content) == 0 {
continue continue
@@ -155,6 +171,9 @@ func mergePackageResources(reader *Reader, parts map[string][]byte, doc *Documen
data = editorPatchXML(data, []editorXMLPatch{{position, position, content}}) data = editorPatchXML(data, []editorXMLPatch{{position, position, content}})
} }
} }
if err := ctx.Err(); err != nil {
return "", false, err
}
parts[name] = data parts[name] = data
return name, false, nil return name, false, nil
} }
+5 -1
View File
@@ -346,11 +346,15 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
// 返回: error 写入错误 // 返回: error 写入错误
func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, progress editorProgress, includePages bool) error { func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, progress editorProgress, includePages bool) error {
if len(e.removedPages) != 0 { if len(e.removedPages) != 0 {
ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
output := write output := write
write = func(name string, data []byte, binary bool) error { write = func(name string, data []byte, binary bool) error {
if !binary && strings.EqualFold(path.Ext(name), ".xml") { if !binary && strings.EqualFold(path.Ext(name), ".xml") {
var err error var err error
data, err = pruneCreatedPageReferences(data, e.removedPages) data, err = pruneCreatedPageReferences(ctx, data, e.removedPages)
if err != nil { if err != nil {
return err return err
} }