diff --git a/go.mod b/go.mod index c616a0a..e0311b0 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index eb297cd..f8a7427 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/ofdgo_compression_profiles.go b/ofdgo_compression_profiles.go index 1e6879b..7b92fd7 100644 --- a/ofdgo_compression_profiles.go +++ b/ofdgo_compression_profiles.go @@ -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) diff --git a/ofdgo_compression_resources.go b/ofdgo_compression_resources.go index 6f8aa31..31c6996 100644 --- a/ofdgo_compression_resources.go +++ b/ofdgo_compression_resources.go @@ -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) } } } diff --git a/ofdgo_editor_annot_ops.go b/ofdgo_editor_annot_ops.go index 028c044..df0b9c6 100644 --- a/ofdgo_editor_annot_ops.go +++ b/ofdgo_editor_annot_ops.go @@ -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 { diff --git a/ofdgo_editor_attachment.go b/ofdgo_editor_attachment.go index 111d16d..5a0f0ef 100644 --- a/ofdgo_editor_attachment.go +++ b/ofdgo_editor_attachment.go @@ -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 != "" { diff --git a/ofdgo_editor_composite.go b/ofdgo_editor_composite.go index 16f6352..afa68a1 100644 --- a/ofdgo_editor_composite.go +++ b/ofdgo_editor_composite.go @@ -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) diff --git a/ofdgo_editor_composite_image.go b/ofdgo_editor_composite_image.go index 991fb0a..40bb30d 100644 --- a/ofdgo_editor_composite_image.go +++ b/ofdgo_editor_composite_image.go @@ -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 diff --git a/ofdgo_editor_composite_ops.go b/ofdgo_editor_composite_ops.go index b89f0ac..3a46277 100644 --- a/ofdgo_editor_composite_ops.go +++ b/ofdgo_editor_composite_ops.go @@ -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 diff --git a/ofdgo_editor_font.go b/ofdgo_editor_font.go index 17577af..227fdf5 100644 --- a/ofdgo_editor_font.go +++ b/ofdgo_editor_font.go @@ -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 错误信息 diff --git a/ofdgo_editor_import.go b/ofdgo_editor_import.go index b3cfe04..04b97ea 100644 --- a/ofdgo_editor_import.go +++ b/ofdgo_editor_import.go @@ -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 } diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 32fe79d..56c0fd1 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -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": diff --git a/ofdgo_editor_outline.go b/ofdgo_editor_outline.go index fa04d70..5e7ab48 100644 --- a/ofdgo_editor_outline.go +++ b/ofdgo_editor_outline.go @@ -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 } diff --git a/ofdgo_editor_package.go b/ofdgo_editor_package.go index 319cf32..759501f 100644 --- a/ofdgo_editor_package.go +++ b/ofdgo_editor_package.go @@ -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 } } diff --git a/ofdgo_editor_pages.go b/ofdgo_editor_pages.go index 9e57cbf..c37ecd2 100644 --- a/ofdgo_editor_pages.go +++ b/ofdgo_editor_pages.go @@ -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解析引用,绝对路径以包根目录为基准 diff --git a/ofdgo_editor_resource_scope.go b/ofdgo_editor_resource_scope.go index 85e0e7b..608498b 100644 --- a/ofdgo_editor_resource_scope.go +++ b/ofdgo_editor_resource_scope.go @@ -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() } diff --git a/ofdgo_editor_resources.go b/ofdgo_editor_resources.go index 9f99481..9f42fe0 100644 --- a/ofdgo_editor_resources.go +++ b/ofdgo_editor_resources.go @@ -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 } diff --git a/ofdgo_editor_selection.go b/ofdgo_editor_selection.go index dbd9567..df515f5 100644 --- a/ofdgo_editor_selection.go +++ b/ofdgo_editor_selection.go @@ -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 { diff --git a/ofdgo_editor_transform.go b/ofdgo_editor_transform.go index 2c72a0c..9fa9676 100644 --- a/ofdgo_editor_transform.go +++ b/ofdgo_editor_transform.go @@ -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) } diff --git a/ofdgo_editor_xml.go b/ofdgo_editor_xml.go index 63aeaea..b638784 100644 --- a/ofdgo_editor_xml.go +++ b/ofdgo_editor_xml.go @@ -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 错误信息 diff --git a/ofdgo_geom.go b/ofdgo_geom.go index 1cb1879..5d07c02 100644 --- a/ofdgo_geom.go +++ b/ofdgo_geom.go @@ -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 构造平移矩阵 diff --git a/ofdgo_package.go b/ofdgo_package.go index 17be51f..432ec6b 100644 --- a/ofdgo_package.go +++ b/ofdgo_package.go @@ -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 } diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 23e2d38..b99cf9f 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -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 }