From 5a88a9f31fa7f0a9d6d92afca0ffaa4c507352f4 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Tue, 13 Oct 2026 12:01:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BC=96=E8=BE=91=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AD=97=E4=BD=93=E8=B5=84=E6=BA=90=E5=8F=8A?= =?UTF-8?q?=E5=9B=BE=E5=83=8F=E5=A4=84=E7=90=86=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_editor.go | 10 ++++++++-- ofdgo_editor_composite.go | 10 +++++++++- ofdgo_editor_composite_ops.go | 6 +++++- ofdgo_editor_font.go | 21 ++++++++++++++++++++- ofdgo_editor_history.go | 3 +++ ofdgo_editor_images.go | 33 ++++++++++++++++++++------------- ofdgo_editor_resources.go | 15 ++++++++++++--- ofdgo_font_resource_sfnt.go | 2 +- 8 files changed, 78 insertions(+), 22 deletions(-) diff --git a/ofdgo_editor.go b/ofdgo_editor.go index ed401ed..c18b728 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -49,6 +49,7 @@ type Editor struct { pages []PageContent resources []editorResource fonts map[string]*FontResource + fontRegistration editorFontRegistration fontDirs []string fontFS []fs.FS fontRenderer *Renderer @@ -663,7 +664,8 @@ func (e *Editor) TransformObject(page int, id string, dx, dy, scale float64) err } // ReplaceImage 替换图片资源,保留对象标识和绘制顺序,一次撤销恢复资源及布局 -// 入参: page 页面索引, id 图片对象标识, data PNG或JPEG数据, fit 为contain、cover或空,空值保留原变换和裁剪 +// 更换主图资源时清除旧替换图引用,保留蒙版及其他绘制属性 +// 入参: page 页面索引, id 图片对象标识, data AddImage支持的图片数据, fit 为contain、cover或空,空值保留原变换和裁剪 // 返回: error 错误信息 func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) error { object, err := e.Object(page, id) @@ -682,10 +684,14 @@ func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) erro return err } } - object.ImageObject.ResourceID, err = e.AddImage(data) + resource, err := e.AddImage(data) if err != nil { return err } + if !sameResourceID(object.ImageObject.ResourceID, resource) { + object.ImageObject.Substitution = "" + } + object.ImageObject.ResourceID = resource if fit != "" { if err := e.fitImage(&object.ImageObject, fit); err != nil { return err diff --git a/ofdgo_editor_composite.go b/ofdgo_editor_composite.go index 9adee9e..76e5e6e 100644 --- a/ofdgo_editor_composite.go +++ b/ofdgo_editor_composite.go @@ -973,6 +973,7 @@ func compositeBoundary(object GraphicObject, parent Matrix, inward bool) Graphic } // writeCompositeNode 保留原文逐级合并内部修改,仅复制发生变化的资源分支 +// 内容变化的矢量资源移除旧缩略图和替换图,不影响原资源及其引用者 // 入参: node 实例节点 // 返回: []byte 新片段, bool 是否变化, error 错误信息 func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, error) { @@ -993,8 +994,15 @@ func (e *Editor) writeCompositeNode(node *editorCompositeNode) ([]byte, bool, er maps.Copy(states, child.states) } } - data := editorPatchXML(node.data, patches) changed := node.changed || len(patches) != 0 + if changed && node.node.name.Local == "CompositeGraphicUnit" { + for _, name := range []string{"Thumbnail", "Substitution"} { + if child := node.node.child(name); child != nil { + patches = append(patches, editorXMLPatch{start: child.start, end: child.end}) + } + } + } + data := editorPatchXML(node.data, patches) node.states = states if node.ref != nil { fragment, dirty, err := e.writeCompositeNode(node.ref) diff --git a/ofdgo_editor_composite_ops.go b/ofdgo_editor_composite_ops.go index 19d2eff..85a4b3b 100644 --- a/ofdgo_editor_composite_ops.go +++ b/ofdgo_editor_composite_ops.go @@ -112,7 +112,8 @@ func (e *Editor) OrderCompositeObjects(page int, path ObjectPath, indexes []int, } // ReplaceCompositeImage 替换内部图片数据,保留变换、裁剪、蒙版、边框和透明度 -// 入参: page 页面索引, path 父复合路径, index 图片序号, data PNG或JPEG数据 +// 更换主图资源时清除旧替换图引用,不修改其他共享实例 +// 入参: page 页面索引, path 父复合路径, index 图片序号, data AddImage支持的图片数据 // 返回: error 错误信息 func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, data []byte) error { return e.editCompositeObjects(page, path, []int{index}, func(renderer *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error { @@ -138,6 +139,9 @@ func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, dat if err != nil { return err } + if !sameResourceID(object.ImageObject.ResourceID, id) { + object.ImageObject.Substitution = "" + } object.ImageObject.ResourceID = id return node.update(object) }) diff --git a/ofdgo_editor_font.go b/ofdgo_editor_font.go index ab3c746..80d1dc3 100644 --- a/ofdgo_editor_font.go +++ b/ofdgo_editor_font.go @@ -40,6 +40,14 @@ type editorFontSubset struct { mapped bool } +// editorFontRegistration 记录最近一次成功注册的输入与标识,不保留原始文件 +type editorFontRegistration struct { + checksum [32]byte + name string + index int + id string +} + // editorSourceFontKey 区分原字体内容及完整用字集合 type editorSourceFontKey struct { source [32]byte @@ -52,6 +60,7 @@ type editorSourceFontCache map[editorSourceFontKey][]byte // clearFontResourceCache 清除字体后端生成的解析与裁剪缓存,保留原始资源 func (e *Editor) clearFontResourceCache() { e.fonts = make(map[string]*FontResource) + e.fontRegistration = editorFontRegistration{} for i := range e.resources { e.resources[i].subset = nil } @@ -105,11 +114,21 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) { if e.backends.FontResources == nil { return "", fmt.Errorf("font resources: %w", ErrBackendUnavailable) } + checksum := sha256.Sum256(file.Data) + previous := e.fontRegistration + if previous.id != "" && previous.checksum == checksum && previous.name == file.Name && previous.index == index { + return previous.id, nil + } + e.fontRegistration = editorFontRegistration{} parsed, err := e.backends.FontResources.OpenFontResource(file, index) if err != nil { return "", err } - return e.addFontResource(parsed, sha256.Sum256(parsed.Data)) + id, err := e.addFontResource(parsed, sha256.Sum256(parsed.Data)) + if err == nil { + e.fontRegistration = editorFontRegistration{checksum: checksum, name: file.Name, index: index, id: id} + } + return id, err } // addFontResource 注册已解析的只读字体,各文档独立分配标识和包内路径 diff --git a/ofdgo_editor_history.go b/ofdgo_editor_history.go index ed461b4..e0988e4 100644 --- a/ofdgo_editor_history.go +++ b/ofdgo_editor_history.go @@ -202,6 +202,9 @@ func (e *Editor) retainDocumentResources(source *Editor) { maps.Copy(e.fonts, source.fonts) maps.Copy(e.images, source.images) maps.Copy(e.resourceID, source.resourceID) + if sameBackend(e.backends.FontResources, source.backends.FontResources) { + e.fontRegistration = source.fontRegistration + } e.maxID = max(e.maxID, source.maxID) e.fontRenderer, e.fontMetrics = source.fontRenderer, source.fontMetrics } diff --git a/ofdgo_editor_images.go b/ofdgo_editor_images.go index 0359eba..24bbb29 100644 --- a/ofdgo_editor_images.go +++ b/ofdgo_editor_images.go @@ -17,6 +17,7 @@ package ofdgo import ( "bytes" "context" + "crypto/sha256" "fmt" "image" "io" @@ -43,6 +44,7 @@ type ImagePageOptions struct { // ImportImages 将PNG、JPEG、BMP、单页TIFF或JBIG2逐张建页并插入指定位置,一次操作计入一条撤销记录 // 保留像素及透明度,方向通过对象变换实现,不重新采样;图片压缩沿用AddImage规则 +// 同一批次的相同图片复用解码校验结果,不额外保留像素数据 // 失败或取消时不修改文档,读取期间仍需容纳单张图片的解码数据 // 入参: ctx 取消上下文, sources 按页面顺序排列的图片, at 插入位置, options 建页选项 // 返回: error 错误信息 @@ -80,6 +82,7 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at } return e.Transaction(func(e *Editor) error { indexes := make([]int, 0, len(sources)) + verified := make(map[[32]byte]bool) for i, source := range sources { if err := ctx.Err(); err != nil { return err @@ -91,7 +94,7 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at if err := ctx.Err(); err != nil { return err } - page, err := e.addImagePage(ctx, data, options) + page, err := e.addImagePage(ctx, data, options, verified) if err != nil { return fmt.Errorf("image %q: %w", source.Name, err) } @@ -108,9 +111,9 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at } // addImagePage 保留原图并按方向、物理尺寸与纸张选项建页 -// 入参: ctx 取消上下文, data 图片数据, options 建页选项 +// 入参: ctx 取消上下文, data 图片数据, options 建页选项, verified 本批次已成功解码的图片摘要 // 返回: int 从0开始的页面索引, error 错误信息 -func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePageOptions) (int, error) { +func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePageOptions, verified map[[32]byte]bool) (int, error) { if err := ctx.Err(); err != nil { return 0, err } @@ -122,16 +125,20 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag return 0, fmt.Errorf("only PNG, JPEG, BMP, single-page TIFF and JBIG2 images are supported") } var decoded image.Image - if format == "bmp" || format == "tiff" { - decoded, _, err = decodeImageDataContext(ctx, data) - } else { - decoded, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) - } - if cancelErr := ctx.Err(); cancelErr != nil { - return 0, cancelErr - } - if err != nil { - return 0, err + checksum := sha256.Sum256(data) + if !verified[checksum] { + if format == "bmp" || format == "tiff" { + decoded, _, err = decodeImageDataContext(ctx, data) + } else { + decoded, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) + } + if cancelErr := ctx.Err(); cancelErr != nil { + return 0, cancelErr + } + if err != nil { + return 0, err + } + verified[checksum] = true } metadata, err := imagePageMetadataContext(ctx, data, format, options.DPI) if err != nil { diff --git a/ofdgo_editor_resources.go b/ofdgo_editor_resources.go index e09b135..b2482a8 100644 --- a/ofdgo_editor_resources.go +++ b/ofdgo_editor_resources.go @@ -48,7 +48,7 @@ type editorResourceRefs struct { // compactSourceReferences 合并自产页面引用,版本文档按当前版本范围扫描 // 按GB/T33190-2016附录A检查标识和路径引用,保留扫描范围内孤立XML中的引用 -// 多文档压缩按文档归集引用,未知扩展或无法解析的XML不清理资源 +// 多文档按文档归集引用,未知扩展或无法解析的XML不清理资源 // 入参: parts 输出条目, progress 保存进度回调, generated 自产页面及引用 // 返回: map[string]bool 可移除条目, error 读取或取消错误 func (e *Editor) compactSourceReferences(parts map[string][]byte, progress editorProgress, generated map[string]editorGeneratedReferences) (map[string]bool, error) { @@ -61,10 +61,10 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Reader, progress editorProgress, generated map[string]editorGeneratedReferences, retired map[string]bool) (map[string]bool, error) { multiple := reader != nil && reader.DocumentCount() > 1 prune := e.output != nil && e.output.options.Mode != CompressionUnchanged - if e.output != nil && e.output.protected || multiple && (e.output == nil || e.output.options.Mode == CompressionUnchanged) { + if e.output != nil && e.output.protected { return nil, nil } - if len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) { + if !multiple && len(parts) == 0 && e.revision == 0 && (e.output == nil || e.output.options.Mode == CompressionUnchanged) { return nil, nil } generated = e.generatedVectorReferences(parts, generated) @@ -72,9 +72,18 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read if e.output != nil { ctx = e.output.ctx } + if multiple { + protected, err := reader.hasOutputSignatures(ctx) + if err != nil || protected { + return nil, err + } + } progress = progress.withContext(ctx) _, scope, err := reader.versionResourceFiles(ctx, parts, generated) if err != nil { + if multiple && !prune { + return nil, ctx.Err() + } return nil, err } names := make(map[string]bool) diff --git a/ofdgo_font_resource_sfnt.go b/ofdgo_font_resource_sfnt.go index b66f5fa..170c180 100644 --- a/ofdgo_font_resource_sfnt.go +++ b/ofdgo_font_resource_sfnt.go @@ -224,7 +224,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { } for tag := range sfnt.Tables { switch tag { - case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "VORG", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "GSUB", "meta", "DSIG", "FFTM": + case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "VORG", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "GSUB", "meta", "DSIG", "FFTM", "PCLT": default: return nil }