From dcf1ae8b3a92786f8686452b7698b949f0ae2eb6 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Tue, 15 Sep 2026 11:45:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=86=99=E5=85=A5=E5=8A=9F=E8=83=BD):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A1=B5=E9=9D=A2=E5=A4=8D=E5=88=B6=E4=B8=8E?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E6=8C=89=E9=9C=80=E5=86=99=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_editor.go | 44 +++++++++++++++++++++++++++++-- ofdgo_writer.go | 69 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/ofdgo_editor.go b/ofdgo_editor.go index 3a6ad75..36e45cc 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -127,6 +127,46 @@ func (e *Editor) AddPage(width, height float64) (int, error) { return len(e.pages) - 1, nil } +// CopyPage 复制页面并追加到文档末尾,分配新标识,复用字体和图片资源 +// 副本可独立修改,位置通过MovePage调整 +// 入参: index 原页面索引 +// 返回: int 副本页面索引, error 错误信息 +func (e *Editor) CopyPage(index int) (int, error) { + source, err := e.page(index) + if err != nil { + return 0, err + } + page := PageContent{Area: source.Area, Content: Content{Layer: make([]Layer, len(source.Content.Layer))}} + for i, layer := range source.Content.Layer { + page.Content.Layer[i] = Layer{Type: layer.Type, Objects: make([]GraphicObject, len(layer.Objects))} + for j, object := range layer.Objects { + page.Content.Layer[i].Objects[j], err = cloneEditorObject(object) + if err != nil { + return 0, err + } + } + } + page.ID = e.nextID() + for i := range page.Content.Layer { + layer := &page.Content.Layer[i] + layer.ID = e.nextID() + for j := range layer.Objects { + object := &layer.Objects[j] + id := e.nextID() + switch object.Type { + case "TextObject": + object.TextObject.ID = id + case "PathObject": + object.PathObject.ID = id + case "ImageObject": + object.ImageObject.ID = id + } + } + } + e.pages = append(e.pages, page) + return len(e.pages) - 1, nil +} + // DeletePage 删除页面,不回收资源或复用标识,删除后页面索引随之变化 // 入参: index 页面索引 // 返回: error 错误信息 @@ -180,7 +220,7 @@ func (e *Editor) nextID() string { return strconv.Itoa(e.maxID) } -// AddFont 嵌入OpenType字体,集合字体按索引提取,重复资源复用标识 +// AddFont 注册OpenType字体,集合字体按索引提取,重复资源复用标识,引用后写入文档 // 入参: file 字体文件, index 集合内字体索引,单字体为0 // 返回: string 字体资源标识, error 错误信息 func (e *Editor) AddFont(file FontFile, index int) (string, error) { @@ -250,7 +290,7 @@ func editorFontName(sfnt *font.SFNT, names ...font.NameID) string { return "" } -// AddImage 嵌入PNG或JPEG图片,保留原始编码,重复资源复用标识 +// AddImage 注册PNG或JPEG图片,保留原始编码,重复资源复用标识,引用后写入文档 // 入参: data 图片数据 // 返回: string 图片资源标识, error 错误信息 func (e *Editor) AddImage(data []byte) (string, error) { diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 2f45e26..84ab4ac 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -27,7 +27,7 @@ import ( const ofdNamespace = "http://www.ofdspec.org/2016" -// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出 +// WriteTo 逐个条目写出OFD,仅包含实际引用的资源,不关闭调用方输出流,出错时应丢弃本次输出 // 入参: writer 输出流 // 返回: int64 已写入字节数, error 错误信息 func (e *Editor) WriteTo(writer io.Writer) (int64, error) { @@ -55,7 +55,7 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) { } // Reader 获取当前文档的独立内存快照,不进行ZIP压缩,后续修改不影响已有快照 -// 返回的Reader可用于现有渲染、搜索和导出接口,二进制资源内部共享只读数据 +// 返回的Reader可用于现有渲染、搜索和导出接口,仅包含实际引用的资源,二进制数据内部共享只读 // 返回: *Reader 阅读器, error 错误信息 func (e *Editor) Reader() (*Reader, error) { if err := e.validate(); err != nil { @@ -100,6 +100,7 @@ func (e *Editor) validate() error { // 入参: write 条目写入方法 // 返回: error 错误信息 func (e *Editor) writeParts(write func(string, []byte, bool) error) error { + fonts, images := e.usedResources() writeXML := func(name string, encode func(*ofdXML)) error { data, err := encodeOFDXML(encode) if err != nil { @@ -145,7 +146,7 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error) error { x.start("PageArea", nil) x.text("PhysicalBox", e.pages[0].Area.PhysicalBox) x.end("PageArea") - if len(e.resources) != 0 { + if len(fonts)+len(images) != 0 { x.text("DocumentRes", "DocumentRes.xml") } x.end("CommonData") @@ -162,8 +163,8 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error) error { }); err != nil { return err } - if len(e.resources) != 0 { - if err := writeXML("Doc_0/DocumentRes.xml", e.writeResources); err != nil { + if len(fonts)+len(images) != 0 { + if err := writeXML("Doc_0/DocumentRes.xml", func(x *ofdXML) { x.resources(fonts, images) }); err != nil { return err } } @@ -174,14 +175,46 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error) error { return err } } - for _, resource := range e.resources { - if err := write(resource.name, resource.data, resource.image != nil); err != nil { - return err + for _, resources := range [][]editorResource{fonts, images} { + for _, resource := range resources { + if err := write(resource.name, resource.data, resource.image != nil); err != nil { + return err + } } } return nil } +// usedResources 按注册顺序筛选当前页面引用的字体和图片,不修改资源池 +// 返回: []editorResource 字体资源, []editorResource 图片资源 +func (e *Editor) usedResources() (fonts, images []editorResource) { + used := make(map[string]bool) + for _, page := range e.pages { + for _, layer := range page.Content.Layer { + for _, object := range layer.Objects { + switch object.Type { + case "TextObject": + used[object.TextObject.Font] = true + case "ImageObject": + used[object.ImageObject.ResourceID] = true + if object.ImageObject.ImageMask != "" { + used[object.ImageObject.ImageMask] = true + } + } + } + } + } + for _, resource := range e.resources { + if resource.font != nil && used[resource.font.ID] { + fonts = append(fonts, resource) + } + if resource.image != nil && used[resource.image.ID] { + images = append(images, resource) + } + } + return fonts, images +} + // page 写出页面尺寸、图层和对象 // 入参: page 页面内容 func (x *ofdXML) page(page PageContent) { @@ -204,16 +237,13 @@ func (x *ofdXML) page(page PageContent) { x.end("Page") } -// writeResources 写出文档资源索引 -// 入参: x XML编码器 -func (e *Editor) writeResources(x *ofdXML) { +// resources 写出文档资源索引 +// 入参: fonts 字体资源, images 图片资源 +func (x *ofdXML) resources(fonts, images []editorResource) { x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "Res"}}) - if len(e.fonts) != 0 { + if len(fonts) != 0 { x.start("Fonts", nil) - for _, resource := range e.resources { - if resource.font == nil { - continue - } + for _, resource := range fonts { font := resource.font var attrs ofdAttrs attrs.add("ID", font.ID) @@ -235,12 +265,9 @@ func (e *Editor) writeResources(x *ofdXML) { } x.end("Fonts") } - if len(e.images) != 0 { + if len(images) != 0 { x.start("MultiMedias", nil) - for _, resource := range e.resources { - if resource.image == nil { - continue - } + for _, resource := range images { image := resource.image var attrs ofdAttrs attrs.add("ID", image.ID)