From c4b81155a913fe440da8f54c24e0721e4e0b90aa Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 12 Oct 2026 11:20:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4):=20?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=A3=B0=E6=98=8E=E9=A1=BA=E5=BA=8F=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/webui/wasm.go | 16 ++--- ofdgo_action.go | 17 +++++ ofdgo_doc.go | 120 +++++++++++++++++++++++++++++++---- ofdgo_editor_font_binding.go | 6 +- ofdgo_editor_namespace.go | 8 ++- ofdgo_editor_open.go | 7 +- ofdgo_editor_pages.go | 16 ++--- ofdgo_editor_reference.go | 44 ++++++------- ofdgo_editor_xml.go | 5 +- ofdgo_font_info.go | 6 +- ofdgo_image_bmp.go | 14 ++-- ofdgo_image_tiff.go | 6 +- ofdgo_meta.go | 63 ++++++++++++------ ofdgo_object.go | 50 ++++----------- ofdgo_page.go | 58 ++++++++++++++++- ofdgo_reader.go | 43 +++++++++++-- ofdgo_renderer.go | 6 +- ofdgo_u3d_mode.go | 6 +- ofdgo_xml.go | 61 +++++++++++++++++- 19 files changed, 410 insertions(+), 142 deletions(-) diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 03ae12d..80a0998 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -126,6 +126,14 @@ type annotationOptions struct { Area *ofdgo.Box } +// outputContext 定期让出浏览器线程,将取消请求传入库层长操作 +type outputContext struct { + context.Context + cancel context.CancelFunc + checkpoint func() error + checked time.Time +} + // Write 将数据块交给浏览器保存 // 入参: data 导出数据 // 返回: int 写入长度, error 错误信息 @@ -4626,14 +4634,6 @@ func compressionFromJS(value js.Value, options *ofdgo.CompressionOptions) error return options.Validate() } -// outputContext 定期让出浏览器线程,将取消请求传入库层长操作 -type outputContext struct { - context.Context - cancel context.CancelFunc - checkpoint func() error - checked time.Time -} - // newOutputContext 创建单次串行导出的取消上下文 // 入参: checkpoint 浏览器取消检查回调 // 返回: context.Context 导出上下文 diff --git a/ofdgo_action.go b/ofdgo_action.go index 751dde6..01bb0b9 100644 --- a/ofdgo_action.go +++ b/ofdgo_action.go @@ -139,6 +139,23 @@ type actionSource struct { Graphic bool } +// decodeOFDActions 读取标准动作列表,不将外部同名节点作为交互动作 +// 入参: d XML解码器, start 列表起始节点, actions 动作接收列表 +// 返回: error 解析错误 +func decodeOFDActions(d *xml.Decoder, start xml.StartElement, actions *[]Action) error { + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + if node.Name.Local != "Action" { + return d.Skip() + } + var action Action + if err := d.DecodeElement(&action, &node); err != nil { + return err + } + *actions = append(*actions, action) + return nil + }) +} + // UnmarshalXML 解析跳转目标 // 入参: d XML解码器, start 起始节点 // 返回: error 错误信息 diff --git a/ofdgo_doc.go b/ofdgo_doc.go index 53cebfd..eb628da 100644 --- a/ofdgo_doc.go +++ b/ofdgo_doc.go @@ -16,6 +16,7 @@ package ofdgo import ( "encoding/xml" + "fmt" "path" "strings" ) @@ -202,6 +203,112 @@ type print struct { Copies *int `xml:"Copies,attr"` } +// UnmarshalXML 按命名空间读取文档结构,扩展数据仍由原解码器保留原文 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (doc *Document) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + if start.Name.Local != "Document" { + return fmt.Errorf("expected Document root, got %s", start.Name.Local) + } + *doc = Document{XMLName: start.Name, Permissions: defaultPermissions()} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + var target any + switch node.Name.Local { + case "CommonData": + target = &doc.CommonData + case "Pages": + target = &doc.Pages + case "Outlines": + target = &doc.Outlines + case "Permissions": + target = &doc.Permissions + case "Actions": + return decodeOFDActions(d, node, &doc.Actions) + case "Bookmarks": + target = &doc.Bookmarks + case "Annotations": + target = &doc.Annotations + case "Attachments": + target = &doc.Attachments + case "CustomTags": + target = &doc.CustomTags + case "Extensions": + target = &doc.Extensions + default: + return d.Skip() + } + return d.DecodeElement(target, &node) + }) +} + +// UnmarshalXML 读取文档公共区域、模板与资源引用,忽略外部同名节点 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (common *CommonData) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *common = CommonData{} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + var target any + switch node.Name.Local { + case "MaxUnitID": + target = &common.MaxUnitID + case "PageArea": + target = &common.PageArea + case "PublicRes": + target = &common.PublicRes + case "DocumentRes": + target = &common.DocumentRes + case "TemplatePage": + target = &common.TemplatePage + case "DefaultCS": + target = &common.DefaultCS + default: + return d.Skip() + } + return d.DecodeElement(target, &node) + }) +} + +// UnmarshalXML 读取页面区域,防止外部同名字段覆盖标准区域 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (area *PageArea) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *area = PageArea{} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + var target *string + switch node.Name.Local { + case "PhysicalBox": + target = &area.PhysicalBox + case "ApplicationBox": + target = &area.ApplicationBox + case "ContentBox": + target = &area.ContentBox + case "BleedBox": + target = &area.BleedBox + default: + return d.Skip() + } + return d.DecodeElement(target, &node) + }) +} + +// UnmarshalXML 读取标准页面引用,忽略外部扩展中的同名条目 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (pages *Pages) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *pages = Pages{} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + if node.Name.Local != "Page" { + return d.Skip() + } + var page Page + if err := d.DecodeElement(&page, &node); err != nil { + return err + } + pages.Page = append(pages.Page, page) + return nil + }) +} + // PageIndex 按标识查找页面,优先匹配原文和标准十进制写法,等价编号有歧义时不匹配 // 入参: id 页面标识 // 返回: int 从0开始的页面索引,未找到时为-1 @@ -249,19 +356,6 @@ func (doc *Document) OutlineInfos() []OutlineInfo { return outlineInfos(doc.Outlines.OutlineElem, bookmarks, pages) } -// UnmarshalXML 解析文档并应用权限默认值 -// 入参: d XML解码器, start 起始节点 -// 返回: error 错误信息 -func (doc *Document) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - type document Document - value := document{Permissions: defaultPermissions()} - if err := d.DecodeElement(&value, &start); err != nil { - return err - } - *doc = Document(value) - return nil -} - // FileName 获取另存文件名,优先使用带扩展名的显示名称,剔除目录及文件名非法字符 // 返回: string 文件名 func (a Attachment) FileName() string { diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index 61c553a..033c536 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -28,9 +28,6 @@ import ( "strings" ) -// FontMode 指定字体资源的保存方式,不改变文字位置或压缩选项 -type FontMode uint8 - const ( // FontKeep 保留原有嵌入方式 FontKeep FontMode = iota @@ -40,6 +37,9 @@ const ( FontExternal ) +// FontMode 指定字体资源的保存方式,不改变文字位置或压缩选项 +type FontMode uint8 + // FontChange 返回单个字体的处理结果,Document为零基文档索引,Reason非空表示保持原样 type FontChange struct { Document int `json:"document"` diff --git a/ofdgo_editor_namespace.go b/ofdgo_editor_namespace.go index 0b04f32..95eedee 100644 --- a/ofdgo_editor_namespace.go +++ b/ofdgo_editor_namespace.go @@ -24,7 +24,13 @@ import ( // 入参: name 节点名称 // 返回: bool 是否匹配 func (n *editorXML) matchesOFD(name string) bool { - return n.name.Local == name && (classifyOFDNamespace(n.name.Space) != ofdXMLUnknown || n.parent != nil && n.name.Space == n.parent.name.Space) + if n.name.Local != name { + return false + } + if n.parent == nil { + return classifyOFDNamespace(n.name.Space) != ofdXMLUnknown + } + return ofdXMLChild(n.parent.name, n.name) } // textXML 按所在节点的命名空间编码文本节点 diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 81820b5..4fede67 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -380,6 +380,9 @@ func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bo var visit func(*editorXML) error visit = func(parent *editorXML) error { for _, node := range parent.children { + if !ofdXMLChild(parent.name, node.name) { + continue + } id := node.attr("ID") if id != "" && seen[id] { switch node.name.Local { @@ -401,7 +404,7 @@ func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bo } if content := root.child("Content"); content != nil { for _, layer := range content.children { - if layer.name.Local == "Layer" { + if layer.matchesOFD("Layer") { if err := visit(layer); err != nil { return nil, false, err } @@ -508,7 +511,7 @@ func (e *Editor) loadSourcePage(index int) error { nodes := make(map[string]*editorXML) if content := root.child("Content"); content != nil { for _, layer := range content.children { - if layer.name.Local != "Layer" { + if !layer.matchesOFD("Layer") { continue } if err := editorXMLObjects(layer, nodes); err != nil { diff --git a/ofdgo_editor_pages.go b/ofdgo_editor_pages.go index c37ecd2..122d164 100644 --- a/ofdgo_editor_pages.go +++ b/ofdgo_editor_pages.go @@ -24,6 +24,14 @@ import ( "strings" ) +// pageReferenceNode 记录跳转引用扫描中的父节点及删除范围 +type pageReferenceNode struct { + name string + start, patch int + children, removed int + standard, drop bool +} + // pruneDocumentReferences 在输出快照中清理已删除页面和附件的引用,不修改历史快照或其他文档 // 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据 // 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据 @@ -185,14 +193,6 @@ func (e *Editor) pruneDocumentReferences(parts map[string][]byte, generated map[ return ctx.Err() } -// pageReferenceNode 记录跳转引用扫描中的父节点及删除范围 -type pageReferenceNode struct { - name string - start, patch int - children, removed int - standard, drop bool -} - // pruneCreatedPageReferences 清理创作文档中被明确删除页面的直接跳转,不检查其他目标是否可达 // 入参: ctx 取消上下文, data 创作XML, removed 已删除页面标识 // 返回: []byte 更新后的XML, error 解析或取消错误 diff --git a/ofdgo_editor_reference.go b/ofdgo_editor_reference.go index dc9af6c..eeb99e5 100644 --- a/ofdgo_editor_reference.go +++ b/ofdgo_editor_reference.go @@ -33,6 +33,28 @@ type editorGeneratedReferences struct { staged *editorStagedPage } +// editorVectorUsage 保存自产矢量资源的引用快照,内容和路径变化时失效 +type editorVectorUsage struct { + name string + digest [32]byte + refs *editorResourceRefs +} + +// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描 +type editorReferenceScan struct { + refs *editorResourceRefs + objects map[string]bool + name string + base string + stack []string + fonts []*editorFontUsage + text strings.Builder + resource bool + capture bool + safe bool + seen bool +} + // matches 判断条目仍为本次编码结果,空条目与暂存占位不混用 // 入参: data 当前条目数据 // 返回: bool 是否可以复用本次引用 @@ -50,13 +72,6 @@ func (r editorGeneratedReferences) stagedLeaf(data []byte) bool { return r.staged != nil && data == nil && r.refs != nil && len(r.refs.files) == 0 } -// editorVectorUsage 保存自产矢量资源的引用快照,内容和路径变化时失效 -type editorVectorUsage struct { - name string - digest [32]byte - refs *editorResourceRefs -} - // matches 核对资源路径和完整内容摘要,不以切片地址判断内容 // 入参: name 当前资源路径, data 当前资源内容 // 返回: bool 是否仍可使用编码时的引用 @@ -89,21 +104,6 @@ func (e *Editor) generatedVectorReferences(parts map[string][]byte, generated ma return generated } -// editorReferenceScan 按XML事件收集资源和字体用字,未知内容保持完整扫描 -type editorReferenceScan struct { - refs *editorResourceRefs - objects map[string]bool - name string - base string - stack []string - fonts []*editorFontUsage - text strings.Builder - resource bool - capture bool - safe bool - seen bool -} - // vectorUsage 保留完整且限额内的自产矢量引用,超限时保存流程重新扫描 // 入参: data 本次编码内容 // 返回: *editorVectorUsage 引用快照,无法复用时为空 diff --git a/ofdgo_editor_xml.go b/ofdgo_editor_xml.go index 1fea173..f4ed54e 100644 --- a/ofdgo_editor_xml.go +++ b/ofdgo_editor_xml.go @@ -273,11 +273,14 @@ func editorXMLTextValue(data []byte, node *editorXML, before, after string) ([]b return append(entry, data[patch.end:node.end]...), nil } -// editorXMLObjects 递归登记图层和页块内的对象位置,保留原容器层级 +// editorXMLObjects 递归登记图层和页块内的OFD对象位置,不进入外部扩展 // 入参: container 图层或页块, nodes 标识与节点映射 // 返回: error 错误信息 func editorXMLObjects(container *editorXML, nodes map[string]*editorXML) error { for _, node := range container.children { + if !ofdXMLChild(container.name, node.name) { + continue + } if id := node.attr("ID"); id != "" { if nodes[id] != nil { return fmt.Errorf("duplicate object ID %q", id) diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index f4e4825..a179c51 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -220,11 +220,11 @@ func (p *fontUsagePage) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err if start.Name.Local != "Page" { return fmt.Errorf("expected element type but have <%s>", start.Name.Local) } - return decodeObjectContainer(d, start, func(d *xml.Decoder, node xml.StartElement) error { + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { if node.Name.Local != "Content" { return d.Skip() } - return decodeObjectContainer(d, node, func(d *xml.Decoder, layer xml.StartElement) error { + return decodeOFDChildren(d, node, func(d *xml.Decoder, layer xml.StartElement) error { if layer.Name.Local != "Layer" { return d.Skip() } @@ -434,7 +434,7 @@ func (r *Renderer) readPageFontUsage(page Page) (map[string]int, error) { // 入参: d XML解码器, start 容器起始节点, defaults 默认绘制参数 // 返回: error 错误信息 func (p *fontUsagePage) decodeObjects(d *xml.Decoder, start xml.StartElement, defaults *DrawParam) error { - return decodeObjectContainer(d, start, func(d *xml.Decoder, node xml.StartElement) error { + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { switch node.Name.Local { case "PageBlock": return p.decodeObjects(d, node, defaults) diff --git a/ofdgo_image_bmp.go b/ofdgo_image_bmp.go index a0d345c..c76557c 100644 --- a/ofdgo_image_bmp.go +++ b/ofdgo_image_bmp.go @@ -23,6 +23,13 @@ import ( "math/bits" ) +// bmpColorMask 保存一个BMP颜色分量的位域位置和取值范围 +type bmpColorMask struct { + mask uint32 + shift uint + max uint32 +} + // decodeImageConfig 读取图片尺寸,BMP和TIFF只读取布局信息,不解码像素 // 入参: input 图片输入流 // 返回: image.Config 图片配置, string 图片格式, error 头部错误 @@ -57,13 +64,6 @@ func decodeImageConfig(input io.Reader) (image.Config, string, error) { return image.DecodeConfig(buffer) } -// bmpColorMask 保存一个BMP颜色分量的位域位置和取值范围 -type bmpColorMask struct { - mask uint32 - shift uint - max uint32 -} - // sample 将位域采样值映射到八位通道,缺省透明度为不透明 // 入参: value 编码像素 // 返回: uint8 通道值 diff --git a/ofdgo_image_tiff.go b/ofdgo_image_tiff.go index a26b492..4ed04cf 100644 --- a/ofdgo_image_tiff.go +++ b/ofdgo_image_tiff.go @@ -25,6 +25,9 @@ import ( "io" ) +// errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源 +var errTIFFRandomAccess = errors.New("TIFF directory requires random access") + // tiffImageLayout 保存单页TIFF的尺寸、方向、采样及压缩参数 type tiffImageLayout struct { width, height int @@ -39,9 +42,6 @@ type tiffImageLayout struct { t4, t6 uint32 } -// errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源 -var errTIFFRandomAccess = errors.New("TIFF directory requires random access") - // tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源 type tiffDirectoryInput struct { reader *bytes.Reader diff --git a/ofdgo_meta.go b/ofdgo_meta.go index 6e96948..7cf62e9 100644 --- a/ofdgo_meta.go +++ b/ofdgo_meta.go @@ -14,7 +14,10 @@ package ofdgo -import "encoding/xml" +import ( + "encoding/xml" + "fmt" +) // ofdCreator 标识本库生成的文档 const ofdCreator = "xiaoqidun/ofdgo" @@ -37,23 +40,6 @@ type DocBody struct { versioned bool } -// UnmarshalXML 读取文档入口并记录版本列表是否存在,版本描述在选择时读取 -// 入参: decoder XML解码器, start 起始节点 -// 返回: error 解析错误 -func (body *DocBody) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { - type docBody DocBody - var value struct { - docBody - Versions *struct{} `xml:"Versions"` - } - if err := decoder.DecodeElement(&value, &start); err != nil { - return err - } - *body = DocBody(value.docBody) - body.versioned = value.Versions != nil - return nil -} - // DocInfo 文档元数据 type DocInfo struct { DocID string `xml:"DocID"` @@ -79,3 +65,44 @@ type CustomData struct { Value string `xml:",chardata"` sourceIndex int } + +// UnmarshalXML 读取主入口,区分标准文档条目与扩展中的同名节点 +// 入参: decoder XML解码器, start 起始节点 +// 返回: error 解析错误 +func (ofd *OFD) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { + if start.Name.Local != "OFD" { + return fmt.Errorf("expected OFD root, got %s", start.Name.Local) + } + *ofd = OFD{XMLName: start.Name, Version: attrValue(start, "Version"), DocType: attrValue(start, "DocType")} + return decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error { + if node.Name.Local != "DocBody" { + return d.Skip() + } + var body DocBody + if err := d.DecodeElement(&body, &node); err != nil { + return err + } + ofd.DocBody = append(ofd.DocBody, body) + return nil + }) +} + +// UnmarshalXML 读取文档入口并记录版本列表是否存在,版本描述在选择时读取 +// 入参: decoder XML解码器, start 起始节点 +// 返回: error 解析错误 +func (body *DocBody) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { + *body = DocBody{} + return decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error { + switch node.Name.Local { + case "DocInfo": + return d.DecodeElement(&body.DocInfo, &node) + case "DocRoot": + return d.DecodeElement(&body.DocRoot, &node) + case "Signatures": + return d.DecodeElement(&body.Signatures, &node) + case "Versions": + body.versioned = true + } + return d.Skip() + }) +} diff --git a/ofdgo_object.go b/ofdgo_object.go index a5b0c78..6466e6b 100644 --- a/ofdgo_object.go +++ b/ofdgo_object.go @@ -162,7 +162,7 @@ func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { l.ID = attrValue(start, "ID") l.Type = attrValue(start, "Type") l.DrawParam = attrValue(start, "DrawParam") - return decodeObjectContainer(d, start, l.decodeObject) + return decodeOFDChildren(d, start, l.decodeObject) } // UnmarshalXML 解析复合图元并保留对象顺序 @@ -199,7 +199,7 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme c.Visible = &visible } } - return decodeObjectContainer(d, start, c.decodeObject) + return decodeOFDChildren(d, start, c.decodeObject) } // UnmarshalXML 解析注释外观并保留对象顺序 @@ -208,7 +208,7 @@ func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartEleme func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { *a = Appearance{} a.Boundary = attrValue(start, "Boundary") - return decodeObjectContainer(d, start, a.decodeObject) + return decodeOFDChildren(d, start, a.decodeObject) } // UnmarshalXML 解析图案单元内容并保留对象顺序 @@ -216,7 +216,7 @@ func (a *Appearance) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error // 返回: error 错误信息 func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { *p = PatternContent{Thumbnail: attrValue(start, "Thumbnail")} - return decodeObjectContainer(d, start, p.decodeObject) + return decodeOFDChildren(d, start, p.decodeObject) } // objects 按绘制顺序访问底纹单元,Objects优先于分类数组 @@ -314,28 +314,6 @@ func decodeGraphicObject(d *xml.Decoder, start xml.StartElement, target graphicO return true, nil } -// decodeObjectContainer 解析图形对象容器 -// 入参: d XML解码器, start 起始节点, decode 对象解码函数 -// 返回: error 错误信息 -func decodeObjectContainer(d *xml.Decoder, start xml.StartElement, decode func(*xml.Decoder, xml.StartElement) error) error { - for { - tok, err := d.Token() - if err != nil { - return err - } - switch node := tok.(type) { - case xml.StartElement: - if err := decode(d, node); err != nil { - return err - } - case xml.EndElement: - if node.Name.Local == start.Name.Local { - return nil - } - } - } -} - // decodeObject 解析图层子对象 // 入参: d XML解码器, start 起始节点 // 返回: error 错误信息 @@ -351,7 +329,7 @@ func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error { return err } if start.Name.Local == "PageBlock" { - return decodeObjectContainer(d, start, l.decodeObject) + return decodeOFDChildren(d, start, l.decodeObject) } return d.Skip() } @@ -388,15 +366,9 @@ func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartEleme } c.Clips = &clips case "Actions": - var actions struct { - Action []Action `xml:"Action"` - } - if err := d.DecodeElement(&actions, &start); err != nil { - return err - } - c.Actions = actions.Action + return decodeOFDActions(d, start, &c.Actions) case "Content", "PageBlock": - return decodeObjectContainer(d, start, c.decodeObject) + return decodeOFDChildren(d, start, c.decodeObject) default: return d.Skip() } @@ -418,7 +390,7 @@ func (a *Appearance) decodeObject(d *xml.Decoder, start xml.StartElement) error return err } if start.Name.Local == "PageBlock" { - return decodeObjectContainer(d, start, a.decodeObject) + return decodeOFDChildren(d, start, a.decodeObject) } return d.Skip() } @@ -438,17 +410,17 @@ func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) er return err } if start.Name.Local == "PageBlock" { - return decodeObjectContainer(d, start, p.decodeObject) + return decodeOFDChildren(d, start, p.decodeObject) } return d.Skip() } -// attrValue 获取XML属性值 +// attrValue 获取无命名空间的XML属性值 // 入参: start 起始节点, name 属性名 // 返回: string 属性值 func attrValue(start xml.StartElement, name string) string { for _, attr := range start.Attr { - if attr.Name.Local == name { + if attr.Name.Space == "" && attr.Name.Local == name { return attr.Value } } diff --git a/ofdgo_page.go b/ofdgo_page.go index 72b6b20..ce9094e 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -14,7 +14,10 @@ package ofdgo -import "encoding/xml" +import ( + "encoding/xml" + "fmt" +) // PageContent 页面内容 type PageContent struct { @@ -270,3 +273,56 @@ type ImageBorder struct { DashPattern string `xml:"DashPattern,attr"` BorderColor *StrokeColor `xml:"BorderColor"` } + +// UnmarshalXML 读取页面直接子节点,扩展内容不作为页面区域、资源或图层使用 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (page *PageContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + if start.Name.Local != "Page" { + return fmt.Errorf("expected Page root, got %s", start.Name.Local) + } + *page = PageContent{XMLName: start.Name} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + switch node.Name.Local { + case "Area": + return d.DecodeElement(&page.Area, &node) + case "Template": + var template Template + if err := d.DecodeElement(&template, &node); err != nil { + return err + } + page.Template = append(page.Template, template) + case "PageRes": + var location string + if err := d.DecodeElement(&location, &node); err != nil { + return err + } + page.PageRes = append(page.PageRes, location) + case "Content": + return d.DecodeElement(&page.Content, &node) + case "Actions": + return decodeOFDActions(d, node, &page.Actions) + default: + return d.Skip() + } + return nil + }) +} + +// UnmarshalXML 读取页面图层,不将外部命名空间中的Layer作为图层 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (content *Content) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *content = Content{} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + if node.Name.Local != "Layer" { + return d.Skip() + } + var layer Layer + if err := d.DecodeElement(&layer, &node); err != nil { + return err + } + content.Layer = append(content.Layer, layer) + return nil + }) +} diff --git a/ofdgo_reader.go b/ofdgo_reader.go index e0a5dc6..ac9ad6b 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -62,6 +62,27 @@ type Reader struct { encryption *encryptionState } +// documentPageList 仅读取文档中的页面索引,统计页数时不构建其他结构 +type documentPageList struct { + Pages Pages +} + +// UnmarshalXML 按文档命名空间读取页面列表,跳过其他文档结构和扩展 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (list *documentPageList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + if start.Name.Local != "Document" { + return fmt.Errorf("expected Document root, got %s", start.Name.Local) + } + *list = documentPageList{} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + if node.Name.Local == "Pages" { + return d.DecodeElement(&list.Pages, &node) + } + return d.Skip() + }) +} + // Close 关闭阅读器持有的文件,重复调用不再关闭底层文件 // 返回: error 错误信息 func (r *Reader) Close() error { @@ -117,10 +138,7 @@ func (r *Reader) DocumentPageCountContext(ctx context.Context, index int) (int, if err != nil { return 0, err } - var doc struct { - XMLName xml.Name `xml:"Document"` - Pages Pages `xml:"Pages"` - } + var doc documentPageList decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) if err := decoder.Decode(&doc); err != nil { return 0, fmt.Errorf("failed to unmarshal document.xml: %w", err) @@ -1003,6 +1021,7 @@ func (r *Reader) readPageHeaderContext(ctx context.Context, page Page) (PageCont defer f.Close() var content PageContent d := xml.NewDecoder(imageInput{ReadCloser: f, context: ctx}) + var root xml.Name scan: for { if err := ctx.Err(); err != nil { @@ -1016,9 +1035,21 @@ scan: return PageContent{}, err } if start, ok := token.(xml.StartElement); ok { - switch start.Name.Local { - case "Page": + if root.Local == "" { + if start.Name.Local != "Page" { + return PageContent{}, fmt.Errorf("expected Page root, got %s", start.Name.Local) + } + root = start.Name continue + } + if !ofdXMLChild(root, start.Name) { + if err := d.Skip(); err != nil { + return PageContent{}, err + } + continue + } + start = ofdXMLStart(start) + switch start.Name.Local { case "Template": var template Template if err := d.DecodeElement(&template, &start); err != nil { diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 2c5a1d0..8ad1790 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -61,6 +61,9 @@ type Renderer struct { TransparentBackground bool } +// RendererOption 渲染器配置选项 +type RendererOption func(*Renderer) + // deviceLineWidth 按设备分辨率计算描边宽度,零线宽为一像素,正线宽至少为两像素 // 入参: width 页面空间线宽,单位为毫米 // 返回: float64 设备描边宽度,单位为毫米 @@ -75,9 +78,6 @@ func (r *Renderer) deviceLineWidth(width float64) float64 { return width } -// RendererOption 渲染器配置选项 -type RendererOption func(*Renderer) - // ClearCache 释放派生字体、图片、模板与后端缓存,不改变文档和渲染配置 func (r *Renderer) ClearCache() { r.backendStates = make(map[any]any) diff --git a/ofdgo_u3d_mode.go b/ofdgo_u3d_mode.go index e9179d3..de34e87 100644 --- a/ofdgo_u3d_mode.go +++ b/ofdgo_u3d_mode.go @@ -19,9 +19,6 @@ import ( "math" ) -// U3DRenderMode 定义静态几何的呈现方式,边线和顶点使用单像素覆盖 -type U3DRenderMode uint8 - const ( U3DRenderSolid U3DRenderMode = iota // 实体 U3DRenderSolidWireframe // 实体线框 @@ -38,6 +35,9 @@ const ( U3DRenderShadedIllustration // 着色插图 ) +// U3DRenderMode 定义静态几何的呈现方式,边线和顶点使用单像素覆盖 +type U3DRenderMode uint8 + // u3dPrimitiveKey 区分共享点线的颜色及混合状态,避免透明边点重复叠加 type u3dPrimitiveKey struct { a, b [7]float64 diff --git a/ofdgo_xml.go b/ofdgo_xml.go index a66580a..42f8cb5 100644 --- a/ofdgo_xml.go +++ b/ofdgo_xml.go @@ -14,7 +14,10 @@ package ofdgo -import "unicode/utf8" +import ( + "encoding/xml" + "unicode/utf8" +) // 旧版与2016版OFD命名空间 const ( @@ -33,6 +36,62 @@ const ( // ofdXMLNamespace 标识XML命名空间,不代表文档版本或功能支持范围 type ofdXMLNamespace uint8 +// ofdXMLChild 判断子节点是否属于可识别的OFD命名空间或父节点所在的未知命名空间 +// 入参: parent 父节点名称, child 子节点名称 +// 返回: bool 是否属于同一OFD结构 +func ofdXMLChild(parent, child xml.Name) bool { + return classifyOFDNamespace(child.Space) != ofdXMLUnknown || child.Space == parent.Space +} + +// ofdXMLStart 保留OFD节点的无命名空间属性,不修改解码器返回的属性切片 +// 入参: start 起始节点 +// 返回: xml.StartElement 供标准结构解码使用的节点 +func ofdXMLStart(start xml.StartElement) xml.StartElement { + for i, attr := range start.Attr { + if attr.Name.Space == "" { + continue + } + attrs := make([]xml.Attr, i, len(start.Attr)-1) + copy(attrs, start.Attr[:i]) + for _, attr := range start.Attr[i+1:] { + if attr.Name.Space == "" { + attrs = append(attrs, attr) + } + } + start.Attr = attrs + break + } + return start +} + +// decodeOFDChildren 按命名空间解析直接子节点,跳过外部扩展而不提升其内部节点 +// 入参: d XML解码器, start 容器起始节点, decode 子节点解码函数 +// 返回: error 解析错误 +func decodeOFDChildren(d *xml.Decoder, start xml.StartElement, decode func(*xml.Decoder, xml.StartElement) error) error { + for { + token, err := d.Token() + if err != nil { + return err + } + switch node := token.(type) { + case xml.StartElement: + if !ofdXMLChild(start.Name, node.Name) { + if err := d.Skip(); err != nil { + return err + } + continue + } + if err := decode(d, ofdXMLStart(node)); err != nil { + return err + } + case xml.EndElement: + if node.Name == start.Name { + return nil + } + } + } +} + // classifyOFDNamespace 识别OFD命名空间,未声明与未知版本分别处理 // 入参: namespace XML命名空间 // 返回: ofdXMLNamespace 命名空间类别