feat(编辑能力): 优化字体资源及图像处理链路
build.yaml / build (push) Successful in 49s

This commit is contained in:
xiaoqidun committed 2026-10-13 12:01:49 +08:00
1 parent 7e774f99df
commit 5a88a9f31f
8 files changed
+78 -22

No files matched your search

+8 -2
View File
@@ -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
+9 -1
View File
@@ -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)
+5 -1
View File
@@ -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)
})
+20 -1
View File
@@ -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 注册已解析的只读字体,各文档独立分配标识和包内路径
+3
View File
@@ -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
}
+20 -13
View File
@@ -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 {
+12 -3
View File
@@ -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)
+1 -1
View File
@@ -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
}