mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
This commit is contained in:
1 parent
225f60919b
commit
19dc79e8e3
23 files changed
+803
-256
No files matched your search
@@ -9,7 +9,7 @@ require (
|
||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
|
||||
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/image v0.47.0
|
||||
golang.org/x/text v0.43.0
|
||||
|
||||
@@ -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/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
|
||||
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-20261011113302-5ef97798f3e9/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20261011164142-f98f64ee48bb h1:oxXcuj9fGKNLDYmSSiJjQXKkxSWQpmaNJwoF4SEtPZU=
|
||||
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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
|
||||
@@ -95,7 +95,7 @@ func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[stri
|
||||
return fmt.Errorf("file not found: %s", name)
|
||||
}
|
||||
var err error
|
||||
data, err = reader.readFile(name)
|
||||
data, err = reader.readFileViewContext(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -110,15 +110,18 @@ func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[stri
|
||||
continue
|
||||
}
|
||||
for _, node := range group.children {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
location := indexes[name][editorResourceID(node.attr("ID"))]
|
||||
if !node.matchesOFD("ColorSpace") || location == "" {
|
||||
continue
|
||||
}
|
||||
fragment, err := editorXMLAttribute(data, node, "Profile", "/"+location)
|
||||
patch, err := editorXMLAttributePatch(data, node, "Profile", "/"+location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patches = append(patches, editorXMLPatch{start: node.start, end: node.end, data: fragment})
|
||||
patches = append(patches, patch)
|
||||
}
|
||||
}
|
||||
updates[name] = editorPatchXML(data, patches)
|
||||
|
||||
@@ -72,10 +72,13 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
||||
names[name] = true
|
||||
}
|
||||
read := func(name string) ([]byte, error) {
|
||||
if err := e.output.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data, ok := parts[name]; ok {
|
||||
return data, nil
|
||||
}
|
||||
return reader.readFileView(name)
|
||||
return reader.readFileViewContext(e.output.ctx, name)
|
||||
}
|
||||
actual := make(map[string]string)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
root, err := parseEditorXMLContext(e.output.ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
preserved := refs.versions[cleanPackagePath(name)]
|
||||
for _, group := range root.children {
|
||||
for _, node := range group.children {
|
||||
if err := e.output.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
field := ""
|
||||
if group.name.Local == "Fonts" && node.name.Local == "Font" {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
var fontCache editorSourceFontCache
|
||||
for _, key := range ordered {
|
||||
if refs.files[key] || files[resourceFile{key, "FontFile"}] && files[resourceFile{key, "MediaFile"}] {
|
||||
continue
|
||||
@@ -197,7 +204,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
||||
if !files[resourceFile{key, role}] {
|
||||
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
|
||||
}
|
||||
source, prefetched := imageSources[key]
|
||||
@@ -214,10 +221,13 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
||||
source.key.hash = sha256.Sum256(data)
|
||||
}
|
||||
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 {
|
||||
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) {
|
||||
data = candidate
|
||||
parts[actual[key]] = data
|
||||
@@ -302,13 +312,16 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
root, err := parseEditorXMLContext(e.output.ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var patches []editorXMLPatch
|
||||
for _, group := range root.children {
|
||||
for _, node := range group.children {
|
||||
if err := e.output.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, field := range []string{"FontFile", "MediaFile"} {
|
||||
file := node.child(field)
|
||||
if file == nil {
|
||||
@@ -319,7 +332,6 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
||||
return err
|
||||
}
|
||||
key := editorResourceLocation(name, root.attr("BaseLoc"), value)
|
||||
fragment := data[node.start:node.end]
|
||||
location := locations[key]
|
||||
if first, ok := aliases[key]; ok {
|
||||
location = locations[first]
|
||||
@@ -327,23 +339,14 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
||||
if location != "" && location != actual[key] {
|
||||
var escaped bytes.Buffer
|
||||
xml.EscapeText(&escaped, []byte("/"+location))
|
||||
patch := editorXMLContent(data, file, escaped.Bytes())
|
||||
patch.start -= node.start
|
||||
patch.end -= node.start
|
||||
fragment = editorPatchXML(fragment, []editorXMLPatch{patch})
|
||||
patches = append(patches, editorXMLContent(data, file, escaped.Bytes()))
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
fragment, err = editorXMLAttribute(fragment, parsed, "Format", formats[key])
|
||||
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})
|
||||
patches = append(patches, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx,
|
||||
// 入参: page 页面索引, ids 注解标识, 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 e.Transaction(func(edit *Editor) error {
|
||||
@@ -210,6 +210,9 @@ func (e *Editor) TransformAnnotations(page int, ids []string, matrix Matrix) err
|
||||
return err
|
||||
}
|
||||
bounds := matrix.TransformBox(box)
|
||||
if !finite(bounds.X) || !finite(bounds.Y) || !finite(bounds.W) || !finite(bounds.H) || bounds.W < 0 || bounds.H < 0 {
|
||||
return fmt.Errorf("annotation transform exceeds finite range")
|
||||
}
|
||||
path := ObjectPath{Annotation: id}
|
||||
reader, _, _, members, err := edit.compositeScope(page, path)
|
||||
if err != nil {
|
||||
|
||||
@@ -84,7 +84,7 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
||||
}
|
||||
node := doc.child("Attachments")
|
||||
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)
|
||||
existing, err := base.reader.readFile(name)
|
||||
if err != nil {
|
||||
@@ -203,7 +203,7 @@ func (e *Editor) ReplaceAttachment(id string, data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAttachment 删除附件声明,保存时清理可确认无引用的内容,支持撤销和重做
|
||||
// DeleteAttachment 删除附件声明,输出时清理相关动作及可确认无引用的内容,支持撤销和重做
|
||||
// 入参: id 附件标识
|
||||
// 返回: 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 {
|
||||
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 {
|
||||
location := strings.TrimSpace(editorImportText(fragment, file))
|
||||
if location != "" {
|
||||
|
||||
@@ -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 {
|
||||
clips := node.object.ImageObject.Clips
|
||||
_, 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
|
||||
}
|
||||
_, ctm := editorGeometry(node.object)
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
// FitCompositeImage 按原像素比例适应或填充内部图片框,保留原始裁剪和蒙版
|
||||
// FitCompositeImage 按原像素比例适应或填充内部图片框,保留轴方向、原始裁剪和蒙版
|
||||
// 入参: page 页面索引, path 父路径, index 图片序号, mode 为contain或cover
|
||||
// 返回: 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 {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -174,6 +185,11 @@ func (n *editorCompositeNode) matrix(transform bool) Matrix {
|
||||
// 入参: node 对象节点, shape 裁剪路径, matrix 路径到裁剪坐标的变换, track 是否记录还原序号
|
||||
// 返回: 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}}}}
|
||||
if clips := *editorObjectClips(&node.object); clips != nil && slices.ContainsFunc(clips.Clip, func(previous Clip) bool { return reflect.DeepEqual(previous, clip) }) {
|
||||
return nil
|
||||
|
||||
@@ -254,6 +254,9 @@ func (e *Editor) copyCompositeNodes(renderer *Renderer, nodes []*editorComposite
|
||||
// 入参: object 修改后的对象
|
||||
// 返回: 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)
|
||||
if err != nil || bytes.Equal(data, n.data) {
|
||||
return err
|
||||
|
||||
@@ -16,7 +16,9 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
@@ -38,6 +40,56 @@ type editorFontSubset struct {
|
||||
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字体,集合字体按索引提取,重复资源复用标识,引用后写入文档
|
||||
// 入参: file 字体文件, index 集合内字体索引,单字体为0
|
||||
// 返回: string 字体资源标识, error 错误信息
|
||||
|
||||
+225
-64
@@ -43,24 +43,29 @@ type editorPageImport struct {
|
||||
doc *Document
|
||||
directory string
|
||||
target *Reader
|
||||
paths map[string]string
|
||||
xmlPaths map[string]string
|
||||
dataPaths map[string]string
|
||||
templatePaths map[string]string
|
||||
evidence map[string]string
|
||||
resourceXML []byte
|
||||
pageParts []byte
|
||||
documentXML []byte
|
||||
documentRoot *editorXML
|
||||
docRefs map[string]map[string]*editorXML
|
||||
maximum int
|
||||
ids map[string]string
|
||||
pages map[string]bool
|
||||
files map[string][]byte
|
||||
resources map[string]editorImportEntry
|
||||
resourceFiles map[string]bool
|
||||
used map[string][]byte
|
||||
templates map[string]TemplatePage
|
||||
templateParts map[string][]byte
|
||||
defaultCS string
|
||||
attachments map[string][]byte
|
||||
attachmentMap map[string]editorImportEntry
|
||||
attachmentIDs map[string]bool
|
||||
bookmarks map[string]Dest
|
||||
signatureIDs *signatureIDSequence
|
||||
copyPage 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,
|
||||
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),
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -556,7 +567,7 @@ func (m *editorPageImport) encodeResources() error {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -588,13 +599,12 @@ func (m *editorPageImport) outlines() ([]byte, error) {
|
||||
if root == 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 keep func(*editorXML) (bool, error)
|
||||
keep = func(node *editorXML) (bool, error) {
|
||||
if err := m.ctx.Err(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
start, selected := len(patches), false
|
||||
if actions := node.child("Actions"); actions != nil {
|
||||
var value struct {
|
||||
@@ -605,7 +615,7 @@ func (m *editorPageImport) outlines() ([]byte, error) {
|
||||
}
|
||||
for _, action := range value.Action {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -639,6 +649,9 @@ func (m *editorPageImport) outlines() ([]byte, error) {
|
||||
}
|
||||
var validate func(*editorXML) error
|
||||
validate = func(node *editorXML) error {
|
||||
if err := m.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, attr := range node.attrs {
|
||||
if attr.Name.Space == "" || attr.Name.Space == "xmlns" {
|
||||
continue
|
||||
@@ -765,10 +778,17 @@ func (m *editorPageImport) id(value string) string {
|
||||
return id
|
||||
}
|
||||
|
||||
// resourceIndex 登记资源定义,按引用延迟复制资源内容
|
||||
// resourceIndex 每个索引只登记一次,拒绝重复资源标识,按引用延迟复制资源内容
|
||||
// 入参: name 资源索引路径
|
||||
// 返回: 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -780,15 +800,54 @@ func (m *editorPageImport) resourceIndex(name string) error {
|
||||
if root.name.Local != "Res" {
|
||||
return fmt.Errorf("invalid resource index %q", name)
|
||||
}
|
||||
entries := make(map[string]editorImportEntry)
|
||||
for _, group := range root.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
|
||||
}
|
||||
|
||||
// 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 来源资源标识
|
||||
// 返回: 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 _, 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
|
||||
data, err := m.encode(entry, entry.node)
|
||||
if err != nil {
|
||||
@@ -832,11 +893,36 @@ func (m *editorPageImport) reference(value string) (string, error) {
|
||||
// 入参: group 父节点名称, kind 引用节点名称, source 原标识, id 新标识, loc 新路径
|
||||
// 返回: []byte 引用XML, error 错误信息
|
||||
func (m *editorPageImport) documentReference(group, kind, source, id, loc string) ([]byte, error) {
|
||||
if err := m.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 node.name.Local != kind || editorImportKey(node.attr("ID")) != editorImportKey(source) {
|
||||
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
|
||||
@@ -846,15 +932,13 @@ func (m *editorPageImport) documentReference(group, kind, source, id, loc string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err = editorXMLAttribute(data, root, attr[0], attr[1])
|
||||
patch, err := editorXMLAttributePatch(data, root, attr[0], attr[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = editorPatchXML(data, []editorXMLPatch{patch})
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("missing %s reference %q", kind, source)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// copyData 复制二进制资源或签名引用原文,凭据与当前引用使用独立映射
|
||||
// copyData 复制原始数据,资源内容、签名凭据与重写XML使用独立路径映射
|
||||
// 入参: name 来源路径, relative 文档内候选路径, evidence 是否原始签名凭据
|
||||
// 返回: string 新路径, error 错误信息
|
||||
func (m *editorPageImport) copyData(name, relative string, evidence bool) (string, error) {
|
||||
paths := m.paths
|
||||
paths := m.dataPaths
|
||||
if evidence {
|
||||
paths = m.evidence
|
||||
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 是否按模板页复制
|
||||
// 返回: string 新路径, error 错误信息
|
||||
func (m *editorPageImport) copyXML(name, candidate string, template bool) (string, error) {
|
||||
paths := m.paths
|
||||
paths := m.xmlPaths
|
||||
if template {
|
||||
if m.templatePaths == nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
bookmarks := make(map[string]Dest)
|
||||
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 dest := gotoDest(&action, m.bookmarks); !m.copyPage && (dest == nil || !m.pages[editorImportKey(dest.PageID)]) {
|
||||
if m.strictLinks {
|
||||
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 != "" {
|
||||
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)
|
||||
case "BaseLoc", "FileRef":
|
||||
loc, resolveErr := m.resolve(entry.name, "", value)
|
||||
@@ -1147,11 +1251,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
||||
continue
|
||||
}
|
||||
if name == "Goto" && child.name.Local == "Bookmark" {
|
||||
for _, bookmark := range m.doc.Bookmarks.Bookmark {
|
||||
if bookmark.Name != child.attr("Name") {
|
||||
continue
|
||||
}
|
||||
dest := bookmark.Dest
|
||||
if dest, ok := m.bookmarks[child.attr("Name")]; ok {
|
||||
encoded, err := xml.Marshal(dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1174,6 +1274,9 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
||||
}
|
||||
content = append(content, encoded...)
|
||||
}
|
||||
if name == "Actions" && len(content) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(node.children) == 0 {
|
||||
value := editorImportText(entry.data, node)
|
||||
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")
|
||||
value, err = m.copyXML(loc, candidate, false)
|
||||
}
|
||||
case "Font", "Substitution", "Thumbnail":
|
||||
value, err = m.reference(strings.TrimSpace(value))
|
||||
case "Substitution", "Thumbnail":
|
||||
value, err = m.resource(strings.TrimSpace(value), "Image")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1265,39 +1368,58 @@ func (m *editorPageImport) annotations() ([]byte, error) {
|
||||
// 入参: id 来源附件标识
|
||||
// 返回: string 新标识, error 错误信息
|
||||
func (m *editorPageImport) attachment(id string) (string, error) {
|
||||
if err := m.ctx.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if m.copyPage {
|
||||
return id, nil
|
||||
}
|
||||
if _, exists := m.attachments[id]; exists {
|
||||
return m.attachmentID(id)
|
||||
}
|
||||
name := m.reader.documentRoot
|
||||
if m.doc.Attachments.Path != "" {
|
||||
name = m.reader.ResPath(m.doc.Attachments.Path)
|
||||
}
|
||||
data, err := m.readFile(name)
|
||||
if m.attachmentMap == nil {
|
||||
name, data := m.reader.documentRoot, m.documentXML
|
||||
root := m.documentRoot.child("Attachments")
|
||||
if location := strings.TrimSpace(m.doc.Attachments.Path); location != "" {
|
||||
name = m.reader.ResPath(location)
|
||||
var err error
|
||||
data, err = m.readFile(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
root, err := parseEditorXMLContext(m.ctx, data)
|
||||
root, err = parseEditorXMLContext(m.ctx, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if m.doc.Attachments.Path == "" {
|
||||
root = root.child("Attachments")
|
||||
}
|
||||
entries := make(map[string]editorImportEntry)
|
||||
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 := 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("attachment %q not found", id)
|
||||
}
|
||||
|
||||
// attachmentID 为来源附件分配独立的文本标识,避免与数值型资源标识混淆
|
||||
@@ -1313,10 +1435,17 @@ func (m *editorPageImport) attachmentID(id string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
m.attachmentIDs = make(map[string]bool, len(attachments))
|
||||
used := make(map[string]bool, len(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)
|
||||
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)
|
||||
for _, ref := range list.List {
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
file, err := parseSignatureFile(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, stamp := range file.SignedInfo.StampAnnot {
|
||||
for _, stamp := range stamps {
|
||||
sequence.observe(stamp.ID)
|
||||
}
|
||||
}
|
||||
@@ -1376,6 +1501,46 @@ func (m *editorPageImport) signatureID(source, kind, id string) (string, error)
|
||||
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 复制所选页面的签章及原始验证凭据,过滤其他页面的签章位置
|
||||
// 返回: []byte 签名索引条目, error 错误信息
|
||||
func (m *editorPageImport) signatures() ([]byte, error) {
|
||||
@@ -1400,16 +1565,12 @@ func (m *editorPageImport) signatures() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value, err := m.readFile(loc)
|
||||
stamps, err := m.signatureStamps(m.reader, loc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var signature SignatureFile
|
||||
if err := xml.Unmarshal(value, &signature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matched := false
|
||||
for _, stamp := range signature.SignedInfo.StampAnnot {
|
||||
for _, stamp := range stamps {
|
||||
matched = matched || m.pages[editorImportKey(stamp.PageRef)]
|
||||
}
|
||||
if !matched {
|
||||
@@ -1461,7 +1622,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
||||
}
|
||||
var content []byte
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+20
-2
@@ -43,6 +43,7 @@ type editorSource struct {
|
||||
annotationStates map[string]map[string]editorCompositeState
|
||||
annotationPages map[string]bool
|
||||
retiredAttachments map[string]bool
|
||||
removedAttachments map[string]bool
|
||||
}
|
||||
|
||||
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用
|
||||
@@ -540,7 +541,10 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
||||
all.Stretch = object.Type == "ImageObject"
|
||||
all.ReplaceImage = object.Type == "ImageObject"
|
||||
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
|
||||
}
|
||||
node := origin.node
|
||||
@@ -578,7 +582,10 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
||||
all.Paint = all.Paint && e.editorPaintable(object)
|
||||
all.ReplaceImage = all.Update && object.Type == "ImageObject"
|
||||
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" {
|
||||
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)
|
||||
@@ -949,6 +956,17 @@ func validateEditorGeometry(object GraphicObject) error {
|
||||
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
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
|
||||
+83
-53
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"slices"
|
||||
@@ -146,7 +147,11 @@ func (e *Editor) UpdateOutline(path []int, title string, page int) error {
|
||||
}
|
||||
content = append(content, standalone...)
|
||||
}
|
||||
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 {
|
||||
content, err := editorXMLGenerated(e.outlineAction(page), node.name.Space)
|
||||
if err != nil {
|
||||
@@ -167,11 +172,11 @@ func (e *Editor) UpdateOutline(path []int, title string, page int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := editorXMLAttribute(updated, node, "Title", title)
|
||||
patch, err := editorXMLAttributePatch(updated, node, "Title", title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.setOutlineXML(editorPatchXML(updated, []editorXMLPatch{{node.start, node.end, value}}))
|
||||
return e.setOutlineXML(editorPatchXML(updated, []editorXMLPatch{patch}))
|
||||
}
|
||||
|
||||
// SetOutlineActions 替换目录节点的动作序列,空列表清除动作,不改变子目录
|
||||
@@ -391,50 +396,34 @@ func (e *Editor) setOutlineXML(data []byte) error {
|
||||
return err
|
||||
}
|
||||
var patches []editorXMLPatch
|
||||
var leaves func(*editorXML) int
|
||||
leaves = func(node *editorXML) int {
|
||||
children := editorOutlineChildren(node)
|
||||
if len(children) == 0 {
|
||||
return 1
|
||||
}
|
||||
var walk func(*editorXML) (int, error)
|
||||
walk = func(node *editorXML) (int, error) {
|
||||
count := 0
|
||||
for _, child := range children {
|
||||
count += leaves(child)
|
||||
for _, child := range node.children {
|
||||
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 {
|
||||
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 {
|
||||
return 0, err
|
||||
}
|
||||
patches = append(patches, patch)
|
||||
}
|
||||
return max(1, count), nil
|
||||
}
|
||||
if _, err := walk(root); err != nil {
|
||||
return 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})
|
||||
}
|
||||
}
|
||||
for _, child := range editorOutlineChildren(node) {
|
||||
if err := walk(child); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := walk(root); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(patches) > 0 {
|
||||
data = editorPatchXML(data, patches)
|
||||
}
|
||||
current, err := e.outlineXML()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -453,52 +442,93 @@ func (e *Editor) setOutlineXML(data []byte) error {
|
||||
|
||||
// withOutlines 将修改后的目录写入文档,清除指向已删除页面的新增跳转
|
||||
// 入参: data 文档原文
|
||||
// 返回: []byte 更新文档, error 错误信息
|
||||
// 返回: []byte 更新文档, error 解析或取消错误
|
||||
func (e *Editor) withOutlines(data []byte) ([]byte, error) {
|
||||
if e.outlines == 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 {
|
||||
return nil, err
|
||||
}
|
||||
valid := make(map[string]int, len(e.pages))
|
||||
for i, page := range e.pages {
|
||||
valid[page.ID] = i
|
||||
valid := make(map[string]bool, len(e.pages))
|
||||
for _, page := range e.pages {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if id := editorResourceID(page.ID); id != "" {
|
||||
valid[id] = true
|
||||
}
|
||||
}
|
||||
var removals []editorXMLPatch
|
||||
var walk func(*editorXML)
|
||||
walk = func(node *editorXML) {
|
||||
var walk func(*editorXML) error
|
||||
walk = func(node *editorXML) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if actions := node.child("Actions"); actions != nil {
|
||||
start := len(removals)
|
||||
for _, action := range actions.children {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !action.matchesOFD("Action") {
|
||||
continue
|
||||
}
|
||||
if target := action.child("Goto"); target != 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})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
walk(child)
|
||||
if err := walk(child); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
walk(outline)
|
||||
value := editorPatchXML(e.outlines, removals)
|
||||
root, err := parseEditorXML(data)
|
||||
return nil
|
||||
}
|
||||
if err := walk(outline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
var patch editorXMLPatch
|
||||
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")
|
||||
}
|
||||
return editorPatchXML(data, []editorXMLPatch{{pages.end, pages.end, value}}), nil
|
||||
patch = editorXMLPatch{pages.end, pages.end, value}
|
||||
} else {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
data = editorPatchXML(data, []editorXMLPatch{patch})
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sourceParts 生成编辑快照的XML和新增资源,删页时同步清理标准引用
|
||||
// sourceParts 生成编辑快照的XML和新增资源,同步清理已删除页面及附件的引用
|
||||
// 入参: ctx 取消上下文, progress 保存进度回调,预览时为nil
|
||||
// 返回: 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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -263,8 +267,8 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
||||
}
|
||||
parts[name] = updated
|
||||
}
|
||||
if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 {
|
||||
if err := e.prunePageReferences(parts, generated); err != nil {
|
||||
if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 || len(source.removedAttachments) != 0 {
|
||||
if err := e.pruneDocumentReferences(parts, generated); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
+80
-36
@@ -15,18 +15,21 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档
|
||||
// pruneDocumentReferences 在输出快照中清理已删除页面和附件的引用,不修改历史快照或其他文档
|
||||
// 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
|
||||
// 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据
|
||||
// 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil
|
||||
// 返回: error 读取或解析错误
|
||||
func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
|
||||
// 返回: error 读取、解析或取消错误
|
||||
func (e *Editor) pruneDocumentReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
|
||||
removed := maps.Clone(e.removedPages)
|
||||
if removed == nil {
|
||||
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, "")
|
||||
if len(removed) == 0 {
|
||||
if len(removed) == 0 && len(e.source.removedAttachments) == 0 {
|
||||
return nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
if e.output != nil {
|
||||
ctx = e.output.ctx
|
||||
}
|
||||
reader := e.source.reader
|
||||
bookmarks := make(map[string]bool)
|
||||
for _, bookmark := range e.source.document.Bookmarks.Bookmark {
|
||||
@@ -52,16 +59,14 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
for len(queue) > 0 {
|
||||
if e.output != nil {
|
||||
if err := e.output.ctx.Err(); err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
name := queue[0]
|
||||
queue = queue[1:]
|
||||
if seen[name] {
|
||||
@@ -71,15 +76,12 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
|
||||
data, ok := parts[name]
|
||||
if !ok {
|
||||
var err error
|
||||
data, err = reader.readFile(name)
|
||||
data, err = reader.readFileViewContext(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if known := generated[name]; data == nil && known.staged != nil {
|
||||
input := known.staged.open()
|
||||
if e.output != nil {
|
||||
input = imageInput{ReadCloser: input, context: e.output.ctx}
|
||||
}
|
||||
input := imageInput{ReadCloser: known.staged.open(), context: ctx}
|
||||
var err error
|
||||
data, err = io.ReadAll(input)
|
||||
input.Close()
|
||||
@@ -87,18 +89,16 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
|
||||
return err
|
||||
}
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
root, err := parseEditorXMLContext(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var patches []editorXMLPatch
|
||||
var walk func(*editorXML) error
|
||||
walk = func(node *editorXML) error {
|
||||
if e.output != nil {
|
||||
if err := e.output.ctx.Err(); err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown {
|
||||
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 {
|
||||
remove = bookmarks[bookmark.attr("Name")]
|
||||
}
|
||||
} else if attachment := node.child("GotoA"); attachment != nil {
|
||||
remove = e.source.removedAttachments[attachment.attr("AttachID")]
|
||||
}
|
||||
case "Bookmark":
|
||||
if node.parent != nil && node.parent.name.Local == "Bookmarks" {
|
||||
@@ -180,37 +182,79 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri
|
||||
delete(generated, name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// pageReferenceNode 记录跳转引用扫描中的父节点及删除范围
|
||||
type pageReferenceNode struct {
|
||||
name string
|
||||
start, patch int
|
||||
children, removed int
|
||||
standard, drop bool
|
||||
}
|
||||
|
||||
// pruneCreatedPageReferences 清理创作文档中被明确删除页面的直接跳转,不检查其他目标是否可达
|
||||
// 入参: data 创作XML, removed 已删除页面标识
|
||||
// 返回: []byte 更新后的XML, error 解析错误
|
||||
func pruneCreatedPageReferences(data []byte, removed map[string]bool) ([]byte, error) {
|
||||
root, err := parseEditorXML(data)
|
||||
// 入参: ctx 取消上下文, data 创作XML, removed 已删除页面标识
|
||||
// 返回: []byte 更新后的XML, error 解析或取消错误
|
||||
func pruneCreatedPageReferences(ctx context.Context, data []byte, removed map[string]bool) ([]byte, error) {
|
||||
d := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||
var stack []pageReferenceNode
|
||||
var patches []editorXMLPatch
|
||||
root := false
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := int(d.InputOffset())
|
||||
token, err := d.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var patches []editorXMLPatch
|
||||
var walk func(*editorXML)
|
||||
walk = func(node *editorXML) {
|
||||
if node.name.Space != ofdNamespace2016 {
|
||||
return
|
||||
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")
|
||||
}
|
||||
if node.name.Local == "Action" {
|
||||
if target := node.child("Goto"); target != nil {
|
||||
if dest := target.child("Dest"); dest != nil && removed[editorResourceID(dest.attr("PageID"))] {
|
||||
patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
|
||||
return
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, child := range node.children {
|
||||
walk(child)
|
||||
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++
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
return editorPatchXML(data, patches), nil
|
||||
}
|
||||
}
|
||||
if !root {
|
||||
return nil, fmt.Errorf("empty XML document")
|
||||
}
|
||||
if len(patches) > 0 {
|
||||
data = editorPatchXML(data, patches)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// editorPageLocation 按所在XML解析引用,绝对路径以包根目录为基准
|
||||
|
||||
@@ -20,7 +20,13 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
// resourceReferenceScopes 复用结构依赖归集文档引用,同文档索引共用只读结果,跨文档索引独立合并
|
||||
// editorResourceScope 记录资源索引的文档归属链,引用集合只读
|
||||
type editorResourceScope struct {
|
||||
parent int
|
||||
refs *editorResourceRefs
|
||||
}
|
||||
|
||||
// resourceReferenceScopes 按文档归集引用,归属相同的资源索引复用只读结果
|
||||
// 入参: ctx 取消上下文, documents 文档入口, resources 资源索引, references 文件引用, selected 限定的文档入口,空值统计全部文档
|
||||
// 返回: 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
|
||||
}
|
||||
used := make(map[string]bool)
|
||||
result := make(map[string]*editorResourceRefs)
|
||||
shared := make(map[string]bool)
|
||||
owners := make(map[string]int)
|
||||
groups := []editorResourceScope{{}}
|
||||
groupIDs := make(map[editorResourceScope]int)
|
||||
for _, entry := range slices.Sorted(maps.Keys(documents)) {
|
||||
scope := map[string]bool{entry: true}
|
||||
queue := []string{entry}
|
||||
@@ -89,17 +96,14 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
|
||||
if !indexes[name] {
|
||||
continue
|
||||
}
|
||||
if result[name] == nil {
|
||||
result[name] = refs
|
||||
continue
|
||||
key := editorResourceScope{parent: owners[name], refs: refs}
|
||||
id := groupIDs[key]
|
||||
if id == 0 {
|
||||
id = len(groups)
|
||||
groups = append(groups, key)
|
||||
groupIDs[key] = id
|
||||
}
|
||||
if !shared[name] {
|
||||
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)
|
||||
owners[name] = id
|
||||
}
|
||||
}
|
||||
for name, refs := range references {
|
||||
@@ -107,5 +111,27 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
|
||||
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()
|
||||
}
|
||||
@@ -72,6 +72,7 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
|
||||
if e.output != nil {
|
||||
ctx = e.output.ctx
|
||||
}
|
||||
progress = progress.withContext(ctx)
|
||||
scope, err := reader.versionResourceFiles(ctx, parts, generated)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -172,14 +173,14 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
|
||||
data, ok := parts[name]
|
||||
if !ok {
|
||||
var err error
|
||||
data, err = reader.readFile(name)
|
||||
data, err = reader.readFileViewContext(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
root, err := parseEditorXML(data)
|
||||
root, err := parseEditorXMLContext(ctx, data)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
usageRefs := refs
|
||||
if multiple || prune {
|
||||
@@ -190,6 +191,9 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
|
||||
for _, group := range root.children {
|
||||
var members []editorXMLPatch
|
||||
for _, node := range group.children {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
field := ""
|
||||
if group.name.Local == "Fonts" && node.name.Local == "Font" {
|
||||
field = "FontFile"
|
||||
@@ -264,6 +268,7 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
|
||||
for name := range usedFiles {
|
||||
delete(removed, name)
|
||||
}
|
||||
var fontCache editorSourceFontCache
|
||||
for i, name := range ordered {
|
||||
if err := progress.report("fonts", i, len(ordered)); err != nil {
|
||||
return nil, err
|
||||
@@ -277,10 +282,16 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, fmt.Errorf("subset font %s: %w", name, err)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if subset != nil {
|
||||
updates[name] = subset
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (e *Editor) SelectionBounds(page int, objects, annotations []string) (Box,
|
||||
// 入参: page 页面索引, objects 正文标识, annotations 注解标识, 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")
|
||||
}
|
||||
if matrix == IdentityMatrix {
|
||||
|
||||
+60
-24
@@ -26,7 +26,7 @@ import (
|
||||
// 入参: page 页面索引, ids 对象标识, 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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留直角旋转及镜像方向
|
||||
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留旋转、镜像及斜切方向
|
||||
// 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局
|
||||
// 入参: page 页面索引, id 图片对象标识, mode 为contain(完整显示)或cover(填满裁剪)
|
||||
// 返回: error 错误信息
|
||||
@@ -185,7 +185,11 @@ func (obj TextObject) TextFrame() (Box, error) {
|
||||
if !ok {
|
||||
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提交
|
||||
@@ -208,7 +212,13 @@ func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error)
|
||||
frame.X += offset
|
||||
frame.W = width
|
||||
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()
|
||||
if _, err := creationNumbers(obj.CTM, 6); err != nil {
|
||||
return TextObject{}, err
|
||||
}
|
||||
obj.Boundary = editorBoxString(box)
|
||||
return obj, nil
|
||||
}
|
||||
@@ -536,7 +546,29 @@ func cropImageObject(image *ImageObject, box Box) error {
|
||||
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
|
||||
// 返回: 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 {
|
||||
return err
|
||||
}
|
||||
m := NewMatrix(obj.CTM)
|
||||
if obj.CTM == "" {
|
||||
m = Matrix{a: box.W, d: box.H}
|
||||
}
|
||||
if !axisAlignedMatrix(m) {
|
||||
return fmt.Errorf("image fitting requires an axis-aligned transform")
|
||||
m, err := imageFitBasis(obj.CTM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size, err := e.editorImage(obj.ResourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, h := float64(size.X), float64(size.Y)
|
||||
if m.a == 0 {
|
||||
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
|
||||
m.a, m.b, m.c, m.d = m.a*w, m.b*w, m.c*h, m.d*h
|
||||
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
|
||||
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" {
|
||||
return cropImageObject(obj, box)
|
||||
}
|
||||
|
||||
@@ -697,6 +697,54 @@ func editorXMLEncodedFragment(data []byte, node *editorXML, namespace string) ([
|
||||
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 修改单个标准属性,保留节点其余原文
|
||||
// 入参: data 原文, node 节点, name 属性名, value 新值
|
||||
// 返回: []byte 修改后的节点, error 错误信息
|
||||
|
||||
+69
-12
@@ -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))
|
||||
}
|
||||
|
||||
// Invert 求逆矩阵
|
||||
// 返回: Matrix 逆矩阵, bool 是否可逆
|
||||
// Invert 求仿射矩阵的逆变换
|
||||
// 返回: Matrix 逆矩阵,失败时返回单位矩阵, bool 是否存在有限逆矩阵
|
||||
func (m Matrix) Invert() (Matrix, bool) {
|
||||
det := m.a*m.d - m.b*m.c
|
||||
if math.Abs(det) < 1e-9 {
|
||||
for _, value := range m.Values() {
|
||||
if !finite(value) {
|
||||
return IdentityMatrix, false
|
||||
}
|
||||
return Matrix{
|
||||
a: m.d / det,
|
||||
b: -m.b / det,
|
||||
c: -m.c / det,
|
||||
d: m.a / det,
|
||||
e: (m.c*m.f - m.d*m.e) / det,
|
||||
f: (m.b*m.e - m.a*m.f) / det,
|
||||
}, true
|
||||
}
|
||||
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
|
||||
}
|
||||
divide := func(value float64, power int) float64 {
|
||||
mantissa, shift := math.Frexp(value)
|
||||
return math.Ldexp(mantissa/determinant, power+shift-exponent)
|
||||
}
|
||||
x, xe := matrixProductSum(m.c, m.f, -m.d, m.e)
|
||||
y, ye := matrixProductSum(m.b, m.e, -m.a, m.f)
|
||||
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)}
|
||||
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 构造平移矩阵
|
||||
|
||||
+31
-12
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -78,9 +79,12 @@ func packageAvailableName(reader *Reader, files map[string][]byte, name string)
|
||||
}
|
||||
|
||||
// mergePackageResources 合并新增资源定义,保留原索引、路径基准与未知字段
|
||||
// 入参: reader 原包, parts 待写入条目, doc 文档, directory 文档目录, added 新增资源XML,文件引用使用绝对包路径
|
||||
// 返回: string 资源索引路径, bool 是否需要新增文档引用, error 错误信息
|
||||
func mergePackageResources(reader *Reader, parts map[string][]byte, doc *Document, directory string, added []byte) (string, bool, error) {
|
||||
// 入参: ctx 取消上下文, reader 原包, parts 待写入条目, doc 文档, directory 文档目录, added 新增资源XML,文件引用使用绝对包路径
|
||||
// 返回: 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 {
|
||||
name := packageAvailableName(reader, parts, path.Join(directory, "DocumentRes.xml"))
|
||||
parts[name] = added
|
||||
@@ -93,42 +97,54 @@ func mergePackageResources(reader *Reader, parts map[string][]byte, doc *Documen
|
||||
data, ok := parts[name]
|
||||
if !ok {
|
||||
var err error
|
||||
data, err = reader.readFile(name)
|
||||
data, err = reader.readFileViewContext(ctx, name)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
}
|
||||
addition, err := parseEditorXML(added)
|
||||
addition, err := parseEditorXMLContext(ctx, added)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
order := []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"}
|
||||
for _, group := range addition.children {
|
||||
root, err := parseEditorXML(data)
|
||||
root, err := parseEditorXMLContext(ctx, data)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
target := root.child(group.name.Local)
|
||||
context := root
|
||||
scope := root
|
||||
used := make(map[[2]string]bool)
|
||||
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
|
||||
for _, node := range group.children {
|
||||
if target != nil && slices.ContainsFunc(target.children, func(old *editorXML) bool {
|
||||
return old.matchesOFD(node.name.Local) && old.attr("ID") == node.attr("ID")
|
||||
}) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
key := [2]string{node.name.Local, editorImportKey(node.attr("ID"))}
|
||||
if used[key] {
|
||||
continue
|
||||
}
|
||||
entry, err := editorXMLStandalone(added[node.start:node.end], node)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
entry, err = editorXMLGenerated(entry, context.name.Space)
|
||||
entry, err = editorXMLGenerated(entry, scope.name.Space)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
content = append(content, entry...)
|
||||
used[key] = true
|
||||
}
|
||||
if len(content) == 0 {
|
||||
continue
|
||||
@@ -155,6 +171,9 @@ func mergePackageResources(reader *Reader, parts map[string][]byte, doc *Documen
|
||||
data = editorPatchXML(data, []editorXMLPatch{{position, position, content}})
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
parts[name] = data
|
||||
return name, false, nil
|
||||
}
|
||||
+5
-1
@@ -346,11 +346,15 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error, progress edi
|
||||
// 返回: error 写入错误
|
||||
func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, progress editorProgress, includePages bool) error {
|
||||
if len(e.removedPages) != 0 {
|
||||
ctx := context.Background()
|
||||
if e.output != nil {
|
||||
ctx = e.output.ctx
|
||||
}
|
||||
output := write
|
||||
write = func(name string, data []byte, binary bool) error {
|
||||
if !binary && strings.EqualFold(path.Ext(name), ".xml") {
|
||||
var err error
|
||||
data, err = pruneCreatedPageReferences(data, e.removedPages)
|
||||
data, err = pruneCreatedPageReferences(ctx, data, e.removedPages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user