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/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
+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/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=
+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)
}
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)
+21 -18
View File
@@ -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)
}
}
}
+4 -1
View File
@@ -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 {
+10 -2
View File
@@ -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 != "" {
+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 {
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)
+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})
}
// 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
+3
View File
@@ -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
+52
View File
@@ -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 错误信息
+252 -91
View File
@@ -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,29 +893,52 @@ 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 parent := m.documentRoot.child(group); parent != nil {
for _, node := range parent.children {
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
}
if err := m.ctx.Err(); err != nil {
return nil, err
}
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和资源基准目录解析包内路径
@@ -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 err != nil {
return "", err
}
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return "", err
}
if m.doc.Attachments.Path == "" {
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)
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)
if err != nil {
return "", err
}
}
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 为来源附件分配独立的文本标识,避免与数值型资源标识混淆
@@ -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
View File
@@ -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":
+84 -54
View File
@@ -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...)
}
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 {
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)
}
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)
for _, child := range node.children {
if !child.matchesOFD("OutlineElem") {
continue
}
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 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})
return 0, err
}
patches = append(patches, patch)
}
for _, child := range editorOutlineChildren(node) {
if err := walk(child); err != nil {
return err
}
}
return nil
return max(1, count), nil
}
if err := walk(root); err != nil {
if _, err := walk(root); err != nil {
return err
}
data = editorPatchXML(data, patches)
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
}
}
return nil
}
walk(outline)
value := editorPatchXML(e.outlines, removals)
root, err := parseEditorXML(data)
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")
}
patch = editorXMLPatch{pages.end, pages.end, value}
} else {
if err := ctx.Err(); err != nil {
return nil, err
}
return data, nil
}
pages := root.child("Pages")
if pages == nil {
return nil, fmt.Errorf("document has no Pages")
data = editorPatchXML(data, []editorXMLPatch{patch})
if err := ctx.Err(); err != nil {
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"
)
// 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
}
}
+85 -41
View File
@@ -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,15 +59,13 @@ 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 {
return err
}
if err := ctx.Err(); err != nil {
return err
}
name := queue[0]
queue = queue[1:]
@@ -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,17 +89,15 @@ 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 {
return err
}
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)
if err != nil {
return nil, err
}
// 入参: 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
var walk func(*editorXML)
walk = func(node *editorXML) {
if node.name.Space != ofdNamespace2016 {
return
root := false
for {
if err := ctx.Err(); err != nil {
return nil, err
}
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
start := int(d.InputOffset())
token, err := d.Token()
if err == io.EOF {
break
}
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)
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解析引用,绝对路径以包根目录为基准
+39 -13
View File
@@ -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()
}
+15 -4
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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)
}
+48
View File
@@ -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
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))
}
// 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
}
}
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 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
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
View File
@@ -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
View File
@@ -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
}