diff --git a/ofdgo_annot.go b/ofdgo_annot.go index f897e16..5510280 100644 --- a/ofdgo_annot.go +++ b/ofdgo_annot.go @@ -18,6 +18,8 @@ import ( "context" "encoding/xml" "fmt" + "strconv" + "strings" ) // Annotations 注释集合 @@ -93,6 +95,106 @@ type Appearance struct { CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"` } +// UnmarshalXML 解析注释入口,按命名空间读取直接页面引用 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (a *Annotations) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + if start.Name.Local != "Annotations" { + return fmt.Errorf("expected Annotations root, got %s", start.Name.Local) + } + *a = Annotations{XMLName: start.Name} + return decodeOFDCollection(d, start, "Page", &a.Page) +} + +// UnmarshalXML 读取注释所属页面及文件路径,忽略扩展中的同名字段 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (page *AnnPage) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *page = AnnPage{PageID: attrValue(start, "PageID")} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + if node.Name.Local == "FileLoc" { + return d.DecodeElement(&page.FileLoc, &node) + } + return d.Skip() + }) +} + +// UnmarshalXML 解析分页注释文件,不将外部同名节点作为注释 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (page *PageAnnot) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + if start.Name.Local != "PageAnnot" { + return fmt.Errorf("expected PageAnnot root, got %s", start.Name.Local) + } + *page = PageAnnot{XMLName: start.Name} + return decodeOFDCollection(d, start, "Annot", &page.Annot) +} + +// UnmarshalXML 解析注释属性、说明、参数和外观,保留未指定可见性的默认语义 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (a *Annotation) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *a = Annotation{ + ID: attrValue(start, "ID"), Type: attrValue(start, "Type"), Subtype: attrValue(start, "Subtype"), + Creator: attrValue(start, "Creator"), LastModDate: attrValue(start, "LastModDate"), + } + for _, attr := range start.Attr { + if attr.Name.Space != "" { + continue + } + var target *bool + switch attr.Name.Local { + case "Visible": + a.Visible = new(bool) + target = a.Visible + case "NoZoom": + target = &a.NoZoom + case "NoRotate": + target = &a.NoRotate + default: + continue + } + value, err := strconv.ParseBool(strings.TrimSpace(attr.Value)) + if err != nil { + return err + } + *target = value + } + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + switch node.Name.Local { + case "Remark": + return d.DecodeElement(&a.Remark, &node) + case "Parameters": + return d.DecodeElement(&a.Parameters, &node) + case "Appearance": + return d.DecodeElement(&a.Appearance, &node) + } + return d.Skip() + }) +} + +// UnmarshalXML 读取标准注释参数,保留参数顺序和重复名称 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (p *AnnotationParameters) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *p = AnnotationParameters{} + return decodeOFDCollection(d, start, "Parameter", &p.Parameter) +} + +// UnmarshalXML 读取参数名称与文本,不采用带命名空间的名称属性 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解析错误 +func (p *AnnotationParameter) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type plain AnnotationParameter + var value plain + start = ofdXMLStart(start) + if err := d.DecodeElement(&value, &start); err != nil { + return err + } + *p = AnnotationParameter(value) + return nil +} + // AnnotationInfos 按页面顺序获取注释信息,包含不可见注释 // 返回: []AnnotationInfo 注释信息, error 错误信息 func (r *Reader) AnnotationInfos() ([]AnnotationInfo, error) { diff --git a/ofdgo_compression_images.go b/ofdgo_compression_images.go index 502493c..b023cf2 100644 --- a/ofdgo_compression_images.go +++ b/ofdgo_compression_images.go @@ -167,7 +167,7 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo, return result, ctx.Err() } -// DrawObject 汇总图片、替换图及文字和路径底纹的尺寸,不使用裁剪后的局部面积 +// DrawObject 汇总图片、替换图及文字、路径和图像边框底纹的尺寸,不使用裁剪后的局部面积 // 入参: object 图形对象, state 继承状态 // 返回: error 取消或几何错误 func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState) error { @@ -193,6 +193,12 @@ func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState) if err != nil { return err } + if border := img.Border; border != nil && border.BorderColor != nil && border.BorderColor.Pattern != nil { + matrix, _ := renderObjectMatrix(img.Boundary, IdentityMatrix, state) + if err := v.patternImages((*FillColor)(border.BorderColor), matrix); err != nil { + return err + } + } local := NewMatrix(img.CTM) if img.CTM == "" { local = Matrix{a: box.W, d: box.H} diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index 24554d5..308aa85 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "maps" + "path" "slices" "strconv" "strings" @@ -57,6 +58,22 @@ type FontProcessOptions struct { OnProgress func(stage string, completed, total int) error } +// editorFontChange 汇总同一文档的字体修改,XML偏移均基于修改前的文件 +type editorFontChange struct { + patches map[string][]editorXMLPatch + files map[string][]byte +} + +// editorBindingFontCache 仅保留当前文档最近读取的字体文件及最近解析结果,操作结束后释放 +type editorBindingFontCache struct { + source string + data []byte + checksum [32]byte + name string + index int + font *FontResource +} + // EmbedFont 嵌入当前文档指定字体,保留资源标识及全部使用位置,成功后可撤销 // file为空时从编辑器字体来源精确匹配;显式文件表示调用方确认替换,仍校验字符和字形 // 入参: ctx 取消上下文, id 字体标识, file 字体文件, index 集合索引 @@ -149,6 +166,7 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces if err != nil { return nil, err } + pending := &editorFontChange{patches: make(map[string][]editorXMLPatch), files: make(map[string][]byte)} ids := slices.Sorted(maps.Keys(scan.fonts)) ids = slices.DeleteFunc(ids, func(id string) bool { return (mode == FontEmbed) == (scan.fonts[id].location != "") @@ -166,13 +184,20 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces return nil, err } item := FontChange{Document: document, ID: id, Name: declaration.font.FontName} - item.Changed, err = e.changeFontSnapshot(ctx, scan, id, mode, FontFile{}, 0) + change, err := e.prepareFontBinding(ctx, scan, id, mode, FontFile{}, 0, pending.files) if err != nil { if ctx.Err() != nil { return nil, ctx.Err() } item.Reason = err.Error() } + item.Changed = change != nil + if change != nil { + for name, patches := range change.patches { + pending.patches[name] = append(pending.patches[name], patches...) + } + maps.Copy(pending.files, change.files) + } report = append(report, item) if err := progress("fonts", len(report), total); err != nil { return nil, err @@ -182,12 +207,11 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces if mode == FontExternal { externalFiles = append(externalFiles, declaration.location) } - scan, err = scanEditorFonts(ctx, reader, document) - if err != nil { - return nil, err - } } } + if err := pending.apply(ctx, scan); err != nil { + return nil, err + } } if err := progress("fonts", len(report), len(report)); err != nil { return nil, err @@ -208,26 +232,40 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces // 入参: ctx 取消上下文, scan 文档引用, id 字体标识, mode 保存方式, file 字体文件, index 集合索引 // 返回: bool 是否修改, error 字体或结构错误 func (e *Editor) changeFontSnapshot(ctx context.Context, scan *editorFontDocument, id string, mode FontMode, file FontFile, index int) (bool, error) { + change, err := e.prepareFontBinding(ctx, scan, id, mode, file, index, nil) + if err != nil || change == nil { + return false, err + } + if err := change.apply(ctx, scan); err != nil { + return false, err + } + return true, nil +} + +// prepareFontBinding 校验单个字体并生成独立修改集,不改变文档及已准备的资源 +// 入参: ctx 取消上下文, scan 文档引用, id 字体标识, mode 保存方式, file 字体文件, index 集合索引, pending 已准备的资源文件 +// 返回: *editorFontChange 字体修改,无需处理时为nil, error 字体或结构错误 +func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocument, id string, mode FontMode, file FontFile, index int, pending map[string][]byte) (*editorFontChange, error) { declaration := scan.fonts[editorResourceID(id)] if declaration == nil { - return false, fmt.Errorf("font not found: %s", id) + return nil, fmt.Errorf("font not found: %s", id) } if (mode == FontEmbed) == (declaration.location != "") { - return false, nil + return nil, nil } if scan.unsafe { - return false, fmt.Errorf("font references include unsupported content") + return nil, fmt.Errorf("font references include unsupported content") } if e.backends.FontResources == nil { - return false, ErrBackendUnavailable + return nil, ErrBackendUnavailable } var data []byte var err error explicit := mode == FontEmbed && len(file.Data) != 0 if mode == FontExternal { - data, err = scan.reader.readFile(declaration.location) + data, err = scan.fontCache.read(ctx, scan.reader, declaration.location) if err != nil { - return false, err + return nil, err } file = FontFile{Data: data} if bytes.HasPrefix(data, []byte("ttcf")) { @@ -236,136 +274,207 @@ func (e *Editor) changeFontSnapshot(ctx context.Context, scan *editorFontDocumen } else if len(file.Data) == 0 { file, err = e.resolveBindingFont(scan, declaration) if err != nil { - return false, err + return nil, err } index = 0 } - parsed, err := e.backends.FontResources.OpenFontResource(file, index) + parsed, err := scan.fontCache.open(ctx, e.backends.FontResources, file, index) if err != nil { - return false, err + return nil, err } if mode == FontEmbed { if err := validateFontEmbedding(parsed.Data); err != nil { - return false, err + return nil, err } } - changes := make(map[string][]byte) + change := &editorFontChange{patches: make(map[string][]editorXMLPatch), files: make(map[string][]byte)} verifiedGlyphs := false - for _, name := range slices.Sorted(maps.Keys(scan.files)) { + for _, text := range scan.texts[editorResourceID(id)] { if err := ctx.Err(); err != nil { - return false, err + return nil, err } - file := scan.files[name] - var patches []editorXMLPatch - for _, node := range file.texts { - if editorResourceID(node.attr("Font")) != editorResourceID(id) { - continue + file, node := text.file, text.node + var object TextObject + if err := xml.Unmarshal(file.data[node.start:node.end], &object); err != nil { + return nil, err + } + if err := checkFontBinding(object, parsed, mode); err != nil { + return nil, err + } + if explicit && len(object.CGTransform) != 0 && !verifiedGlyphs { + source, err := e.resolveBindingFont(scan, declaration) + if err != nil || !sameBindingFont(source.Data, parsed.Data) { + return nil, fmt.Errorf("font glyph mapping requires the original font") } - var object TextObject - if err := xml.Unmarshal(file.data[node.start:node.end], &object); err != nil { - return false, err - } - if err := checkFontBinding(object, parsed, mode); err != nil { - return false, err - } - if explicit && len(object.CGTransform) != 0 && !verifiedGlyphs { - source, err := e.resolveBindingFont(scan, declaration) - if err != nil || !sameBindingFont(source.Data, parsed.Data) { - return false, fmt.Errorf("font glyph mapping requires the original font") - } - verifiedGlyphs = true - } - if mode == FontExternal { - for _, child := range node.children { - if child.matchesOFD("CGTransform") { - patches = append(patches, editorXMLPatch{start: child.start, end: child.end}) - } + verifiedGlyphs = true + } + if mode == FontExternal { + for _, child := range node.children { + if child.matchesOFD("CGTransform") { + change.patches[file.name] = append(change.patches[file.name], editorXMLPatch{start: child.start, end: child.end}) } } } - if len(patches) != 0 { - changes[name] = editorPatchXML(file.data, patches) - } } data = declaration.file.data node := declaration.node - if modified, ok := changes[declaration.file.name]; ok { - data = modified - node, err = fontBindingNode(data, id) - if err != nil { - return false, err - } - } fontFile := node.child("FontFile") + var fragment []byte if mode == FontEmbed { digest := sha256.Sum256(parsed.Data) - name := fmt.Sprintf("Fonts/%x%s", digest, parsed.Extension) - if existing, err := scan.reader.readFile(name); err != nil || !bytes.Equal(existing, parsed.Data) { - name = packageAvailableName(scan.reader, changes, name) - changes[name] = bytes.Clone(parsed.Data) + name, reused, err := fontBindingFile(ctx, scan.reader, pending, fmt.Sprintf("Fonts/%x%s", digest, parsed.Extension), parsed.Data) + if err != nil { + return nil, err + } + if !reused { + change.files[name] = bytes.Clone(parsed.Data) } content := node.textXML("FontFile", "/"+name) if fontFile != nil { - data = editorPatchXML(data, []editorXMLPatch{{fontFile.start, fontFile.end, content}}) + fragment = editorPatchXML(data[node.start:node.end], []editorXMLPatch{{fontFile.start - node.start, fontFile.end - node.start, content}}) } else if node.open == node.end { - data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, content)}) + patch := editorXMLContent(data, node, content) + patch.start -= node.start + patch.end -= node.start + fragment = editorPatchXML(data[node.start:node.end], []editorXMLPatch{patch}) } else { - data = editorPatchXML(data, []editorXMLPatch{{node.close, node.close, content}}) + fragment = editorPatchXML(data[node.start:node.end], []editorXMLPatch{{node.close - node.start, node.close - node.start, content}}) } } else { if fontFile == nil { - return false, fmt.Errorf("font file declaration is missing") + return nil, fmt.Errorf("font file declaration is missing") } - fragment := editorPatchXML(data[node.start:node.end], []editorXMLPatch{{fontFile.start - node.start, fontFile.end - node.start, nil}}) + fragment = editorPatchXML(data[node.start:node.end], []editorXMLPatch{{fontFile.start - node.start, fontFile.end - node.start, nil}}) for _, field := range []string{"FontName", "FamilyName"} { root, err := parseEditorXML(fragment) if err != nil { - return false, err + return nil, err } value := root.attr(field) if stripped := externalFontName(value); stripped != value { fragment, err = editorXMLAttribute(fragment, root, field, stripped) if err != nil { - return false, err + return nil, err } } } - data = editorPatchXML(data, []editorXMLPatch{{node.start, node.end, fragment}}) } if explicit { for _, field := range []struct{ name, value string }{{"FontName", parsed.Font.FontName}, {"FamilyName", parsed.Font.FamilyName}, {"Charset", "unicode"}} { - node, err := fontBindingNode(data, id) + root, err := parseEditorXML(fragment) if err != nil { - return false, err + return nil, err } - fragment, err := editorXMLAttribute(data, node, field.name, field.value) + fragment, err = editorXMLAttribute(fragment, root, field.name, field.value) if err != nil { - return false, err + return nil, err } - data = editorPatchXML(data, []editorXMLPatch{{node.start, node.end, fragment}}) } } - changes[declaration.file.name] = data - if err := scan.apply(ctx, changes); err != nil { - return false, err + if err := ctx.Err(); err != nil { + return nil, err } - return true, nil + change.patches[declaration.file.name] = append(change.patches[declaration.file.name], editorXMLPatch{node.start, node.end, fragment}) + return change, nil } -// fontBindingNode 在修改后的资源XML中重新定位字体声明 -// 入参: data 资源XML, id 字体标识 -// 返回: *editorXML 字体节点, error 解析或查找错误 -func fontBindingNode(data []byte, id string) (*editorXML, error) { - root, err := parseEditorXML(data) +// apply 合并同一文档的XML修改,再统一隔离其他文档及版本使用的文件 +// 入参: ctx 取消上下文, scan 修改前的文档引用 +// 返回: error 取消、引用或结构错误 +func (c *editorFontChange) apply(ctx context.Context, scan *editorFontDocument) error { + if err := ctx.Err(); err != nil { + return err + } + if len(c.patches) == 0 && len(c.files) == 0 { + return nil + } + changes := maps.Clone(c.files) + for name, patches := range c.patches { + if err := ctx.Err(); err != nil { + return err + } + changes[name] = editorPatchXML(scan.files[name].data, patches) + } + return scan.apply(ctx, changes) +} + +// read 复用当前文档最近读取的字体,避免共享引用重复解压,不缓存错误或取消结果 +// 入参: ctx 取消上下文, reader 包快照, name 字体路径 +// 返回: []byte 只读字体数据, error 读取或取消错误 +func (c *editorBindingFontCache) read(ctx context.Context, reader *Reader, name string) ([]byte, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + name = cleanPackagePath(name) + if c.source == name && c.data != nil { + return c.data, nil + } + c.source, c.data = "", nil + data, err := reader.readFileViewContext(ctx, name) if err != nil { return nil, err } - for node := range editorResourceNodes(root, "Fonts", "Font") { - if editorResourceID(node.attr("ID")) == editorResourceID(id) { - return node, nil + if err := ctx.Err(); err != nil { + return nil, err + } + c.source, c.data = name, data + return data, nil +} + +// open 按字体内容、名称及集合索引复用最近的解析结果,不缓存错误或取消结果 +// 入参: ctx 取消上下文, backend 字体资源后端, file 字体文件, index 集合索引 +// 返回: *FontResource 只读字体资源, error 解析或取消错误 +func (c *editorBindingFontCache) open(ctx context.Context, backend FontResourceBackend, file FontFile, index int) (*FontResource, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + checksum := sha256.Sum256(file.Data) + if c.font != nil && c.checksum == checksum && c.name == file.Name && c.index == index { + return c.font, ctx.Err() + } + c.font = nil + font, err := backend.OpenFontResource(file, index) + if err != nil { + return nil, err + } + if err := ctx.Err(); err != nil { + return nil, err + } + c.checksum, c.name, c.index, c.font = checksum, file.Name, index, font + return font, nil +} + +// fontBindingFile 查找未占用的字体路径,同名内容一致时复用已有文件或本次待写资源 +// 入参: ctx 取消上下文, reader 包快照, pending 待写资源, name 期望路径, data 字体内容 +// 返回: string 包内路径, bool 是否复用, error 取消错误 +func fontBindingFile(ctx context.Context, reader *Reader, pending map[string][]byte, name string, data []byte) (string, bool, error) { + base, extension := strings.TrimSuffix(name, path.Ext(name)), path.Ext(name) + for i := 0; ; i++ { + if err := ctx.Err(); err != nil { + return "", false, err + } + candidate := name + if i != 0 { + candidate = base + "_" + strconv.Itoa(i) + extension + } + existing, occupied := pending[candidate] + if !occupied { + existing, occupied = reader.files[candidate] + if !occupied { + _, occupied = reader.packageFile(candidate) + if occupied { + var err error + existing, err = reader.readFileViewContext(ctx, candidate) + if err != nil { + continue + } + } + } + } + if !occupied || bytes.Equal(existing, data) { + return candidate, occupied, ctx.Err() } } - return nil, fmt.Errorf("font declaration is missing") } // sameBindingFont 比较独立字体的全部数据表,忽略重新封装产生的校验和差异 diff --git a/ofdgo_editor_font_scan.go b/ofdgo_editor_font_scan.go index c61cc7b..0060b61 100644 --- a/ofdgo_editor_font_scan.go +++ b/ofdgo_editor_font_scan.go @@ -26,19 +26,25 @@ import ( // editorFontDocument 保存一个文档的字体声明与完整内容引用,不进入附件和签名封装 type editorFontDocument struct { - reader *Reader - index int - files map[string]*editorFontXML - fonts map[string]*editorFontDeclaration - unsafe bool + reader *Reader + index int + files map[string]*editorFontXML + fonts map[string]*editorFontDeclaration + texts map[string][]editorFontText + fontCache editorBindingFontCache + unsafe bool } // editorFontXML 保存字体声明及文字引用所在的XML type editorFontXML struct { - name string - data []byte - root *editorXML - texts []*editorXML + name string + data []byte +} + +// editorFontText 关联文字节点及其原始文件,按字体标识归集 +type editorFontText struct { + file *editorFontXML + node *editorXML } // editorFontDeclaration 关联字体声明、所属资源文件及嵌入数据路径 @@ -53,7 +59,7 @@ type editorFontDeclaration struct { // 入参: ctx 取消上下文, reader 包快照, index 文档索引 // 返回: *editorFontDocument 字体引用, error 读取或结构错误 func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFontDocument, error) { - scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration)} + scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration), texts: make(map[string][]editorFontText)} entry, err := reader.documentEntryContext(ctx, index) if err != nil { return nil, err @@ -76,7 +82,7 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon if err != nil { return nil, err } - file := &editorFontXML{name: name, data: data, root: root} + file := &editorFontXML{name: name, data: data} scan.files[name] = file base := "" if root.matchesOFD("Res") { @@ -122,8 +128,9 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon scan.fonts[id] = &editorFontDeclaration{font: font, file: file, node: node, location: location} } if node.matchesOFD("TextObject") || node.matchesOFD("Text") && node.parent != nil && node.parent.matchesOFD("Area") { - file.texts = append(file.texts, node) - if node.attr("Font") == "" { + id := editorResourceID(node.attr("Font")) + scan.texts[id] = append(scan.texts[id], editorFontText{file: file, node: node}) + if id == "" { scan.unsafe = true } } diff --git a/ofdgo_object.go b/ofdgo_object.go index 7e4e918..85635a9 100644 --- a/ofdgo_object.go +++ b/ofdgo_object.go @@ -17,6 +17,7 @@ package ofdgo import ( "encoding/xml" "fmt" + "io" "iter" "strconv" "strings" @@ -31,6 +32,32 @@ type graphicObjectTarget struct { composite *[]CompositeGraphicUnit } +// graphicAttributeTokens 提供图形节点的起止令牌,仅用于解码无命名空间属性 +type graphicAttributeTokens struct { + start xml.StartElement + step int +} + +// Token 返回起始节点和结束节点,不读取图形子节点 +// 返回: xml.Token XML令牌, error 读取错误 +func (s *graphicAttributeTokens) Token() (xml.Token, error) { + s.step++ + switch s.step { + case 1: + return s.start, nil + case 2: + return s.start.End(), nil + } + return nil, io.EOF +} + +// decodeGraphicAttributes 按结构字段解析图形属性,忽略带命名空间的扩展属性 +// 入参: start 起始节点, value 不含自身解码方法的接收结构 +// 返回: error 解码错误 +func decodeGraphicAttributes(start xml.StartElement, value any) error { + return xml.NewTokenDecoder(&graphicAttributeTokens{start: ofdXMLStart(start)}).Decode(value) +} + // Actions 返回对象动作的独立副本,不修改原对象 // 返回: []Action 动作列表 func (o GraphicObject) Actions() []Action { @@ -57,17 +84,45 @@ func (clips *Clips) usesObjectMatrix() bool { // 入参: d XML解码器, start 起始节点 // 返回: error 解码错误 func (clips *Clips) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - type plain Clips - var value plain + *clips = Clips{} if classifyOFDNamespace(start.Name.Space) == ofdXMLLegacy { transform := true - value.TransFlag = &transform + clips.TransFlag = &transform } - if err := d.DecodeElement(&value, &start); err != nil { - return err + for _, attr := range start.Attr { + if attr.Name.Space == "" && attr.Name.Local == "TransFlag" { + transform, err := strconv.ParseBool(strings.TrimSpace(attr.Value)) + if err != nil { + return err + } + clips.TransFlag = &transform + } } - *clips = Clips(value) - return nil + return decodeOFDCollection(d, start, "Clip", &clips.Clip) +} + +// UnmarshalXML 解析裁剪分区,不将外部同名节点作为裁剪区域 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解码错误 +func (clip *Clip) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *clip = Clip{} + return decodeOFDCollection(d, start, "Area", &clip.Area) +} + +// UnmarshalXML 解析裁剪区域的变换和路径或文字内容 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解码错误 +func (area *ClipArea) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *area = ClipArea{DrawParam: attrValue(start, "DrawParam"), CTM: attrValue(start, "CTM")} + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + switch node.Name.Local { + case "Path": + return d.DecodeElement(&area.Path, &node) + case "Text": + return d.DecodeElement(&area.Text, &node) + } + return d.Skip() + }) } // UnmarshalXML 解析路径并区分省略样式、显式实线与零值描边参数 @@ -75,27 +130,77 @@ func (clips *Clips) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { // 返回: error 错误信息 func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { type plain PathObject - var value struct { - plain - DashPattern *string `xml:"DashPattern,attr"` - } - if err := d.DecodeElement(&value, &start); err != nil { + var value plain + if err := decodeGraphicAttributes(start, &value); err != nil { return err } - *p = PathObject(value.plain) - for _, attr := range start.Attr { - if attr.Name.Local == "LineWidth" { + *p = PathObject(value) + for _, attr := range ofdXMLStart(start).Attr { + switch attr.Name.Local { + case "LineWidth": p.LineWidthSet = p.LineWidth == 0 - } - if attr.Name.Local == "MiterLimit" { + case "MiterLimit": p.MiterLimitSet = p.MiterLimit == 0 + case "DashPattern": + p.dashPatternSet = p.DashPattern == "" } } - if value.DashPattern != nil { - p.DashPattern = *value.DashPattern - p.dashPatternSet = p.DashPattern == "" + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + switch node.Name.Local { + case "FillColor": + return d.DecodeElement(&p.FillColor, &node) + case "StrokeColor": + return d.DecodeElement(&p.StrokeColor, &node) + case "AbbreviatedData": + return d.DecodeElement(&p.AbbreviatedData, &node) + case "Clips": + return d.DecodeElement(&p.Clips, &node) + case "Actions": + return decodeOFDActions(d, node, &p.Actions) + } + return d.Skip() + }) +} + +// UnmarshalXML 解析图片及其边框、裁剪和动作,忽略外部同名内容 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解码错误 +func (image *ImageObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type plain ImageObject + var value plain + if err := decodeGraphicAttributes(start, &value); err != nil { + return err } - return nil + *image = ImageObject(value) + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + switch node.Name.Local { + case "Border": + return d.DecodeElement(&image.Border, &node) + case "Clips": + return d.DecodeElement(&image.Clips, &node) + case "Actions": + return decodeOFDActions(d, node, &image.Actions) + } + return d.Skip() + }) +} + +// UnmarshalXML 解析图片边框,保留省略线宽与显式零值的区别 +// 入参: d XML解码器, start 起始节点 +// 返回: error 解码错误 +func (border *ImageBorder) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type plain ImageBorder + var value plain + if err := decodeGraphicAttributes(start, &value); err != nil { + return err + } + *border = ImageBorder(value) + return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error { + if node.Name.Local == "BorderColor" { + return d.DecodeElement(&border.BorderColor, &node) + } + return d.Skip() + }) } // UnmarshalXML 解析绘制参数并保留显式实线和零值参数对基础参数的覆盖 diff --git a/ofdgo_text_xml.go b/ofdgo_text_xml.go index c883362..682e789 100644 --- a/ofdgo_text_xml.go +++ b/ofdgo_text_xml.go @@ -14,74 +14,101 @@ package ofdgo -import "encoding/xml" - -// textObjectTokens 记录字形变换所属TextCode,供解码后转换为对象内索引 -type textObjectTokens struct { - decoder *xml.Decoder - start *xml.StartElement - depth, code int - transforms []int -} +import ( + "encoding/xml" + "strconv" + "strings" +) // UnmarshalXML 解析字形变换,省略的字符数和字形数均取1 // 入参: decoder XML解码器, start 变换起始节点 // 返回: error 解码错误 func (transform *CGTransform) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { - type plain CGTransform - value := plain{CodeCount: 1, GlyphCount: 1} + *transform = CGTransform{CodeCount: 1, GlyphCount: 1} + for _, attr := range start.Attr { + if attr.Name.Space != "" { + continue + } + var target *int + switch attr.Name.Local { + case "CodePosition": + target = &transform.CodePosition + case "CodeCount": + target = &transform.CodeCount + case "GlyphCount": + target = &transform.GlyphCount + } + if target != nil { + value, err := strconv.Atoi(strings.TrimSpace(attr.Value)) + if err != nil { + return err + } + *target = value + } + } + return decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error { + if node.Name.Local == "Glyphs" { + return d.DecodeElement(&transform.Glyphs, &node) + } + return d.Skip() + }) +} + +// UnmarshalXML 解析文字定位及原文,不使用带外部命名空间的定位属性 +// 入参: decoder XML解码器, start 文字节点 +// 返回: error 解码错误 +func (code *TextCode) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { + type plain TextCode + var value plain + start = ofdXMLStart(start) if err := decoder.DecodeElement(&value, &start); err != nil { return err } - *transform = CGTransform(value) + *code = TextCode(value) return nil } -// Token 转发XML令牌并记录直接子节点顺序 -// 返回: xml.Token XML令牌, error 解码错误 -func (s *textObjectTokens) Token() (xml.Token, error) { - if s.start != nil { - start := *s.start - s.start = nil - return start, nil - } - token, err := s.decoder.Token() - switch value := token.(type) { - case xml.StartElement: - if s.depth == 0 && value.Name.Local == "CGTransform" { - s.transforms = append(s.transforms, s.code) - } - s.depth++ - case xml.EndElement: - s.depth-- - if s.depth == 0 && value.Name.Local == "TextCode" { - s.code++ - } - } - return token, err -} - -// UnmarshalXML 将各TextCode前的局部字形位置统一为对象内索引 +// UnmarshalXML 解析标准文字节点,将各TextCode前的局部字形位置统一为对象内索引 // 入参: decoder XML解码器, start 对象起始节点 // 返回: error 解码错误 func (obj *TextObject) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { type plain TextObject - stream := &textObjectTokens{decoder: decoder, start: &start} var value plain - if err := xml.NewTokenDecoder(stream).Decode(&value); err != nil { + if err := decodeGraphicAttributes(start, &value); err != nil { return err } - offsets := make([]int, len(value.TextCode)) - for i := 1; i < len(offsets); i++ { - offsets[i] = offsets[i-1] + len(textCodeRunes(value.TextCode[i-1].Value)) - } - for i, code := range stream.transforms { - if code < len(offsets) { - value.CGTransform[i].CodePosition += offsets[code] + offset, pending := 0, 0 + if err := decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error { + switch node.Name.Local { + case "FillColor": + return d.DecodeElement(&value.FillColor, &node) + case "StrokeColor": + return d.DecodeElement(&value.StrokeColor, &node) + case "Clips": + return d.DecodeElement(&value.Clips, &node) + case "Actions": + return decodeOFDActions(d, node, &value.Actions) + case "CGTransform": + return d.DecodeElement(&value.CGTransform, &node) + case "TextCode": + var code TextCode + if err := d.DecodeElement(&code, &node); err != nil { + return err + } + for i := pending; i < len(value.CGTransform); i++ { + value.CGTransform[i].CodePosition += offset + } + pending = len(value.CGTransform) + offset += len(textCodeRunes(code.Value)) + value.TextCode = append(value.TextCode, code) + return nil } + return d.Skip() + }); err != nil { + return err } *obj = TextObject(value) - for _, attr := range start.Attr { + for _, attr := range ofdXMLStart(start).Attr { if attr.Name.Local == "LineWidth" { obj.LineWidthSet = obj.LineWidth == 0 }