feat(压缩编辑): 优化字体与压缩,规范文档解析
build.yaml / build (push) Successful in 52s

This commit is contained in:
xiaoqidun committed 2026-10-12 18:35:50 +08:00
1 parent 778d36d373
commit 4b9dbd6447
6 files changed
+508 -152

No files matched your search

+102
View File
@@ -18,6 +18,8 @@ import (
"context" "context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"strconv"
"strings"
) )
// Annotations 注释集合 // Annotations 注释集合
@@ -93,6 +95,106 @@ type Appearance struct {
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"` 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 按页面顺序获取注释信息,包含不可见注释 // AnnotationInfos 按页面顺序获取注释信息,包含不可见注释
// 返回: []AnnotationInfo 注释信息, error 错误信息 // 返回: []AnnotationInfo 注释信息, error 错误信息
func (r *Reader) AnnotationInfos() ([]AnnotationInfo, error) { func (r *Reader) AnnotationInfos() ([]AnnotationInfo, error) {
+7 -1
View File
@@ -167,7 +167,7 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo,
return result, ctx.Err() return result, ctx.Err()
} }
// DrawObject 汇总图片、替换图及文字和路径底纹的尺寸,不使用裁剪后的局部面积 // DrawObject 汇总图片、替换图及文字、路径和图像边框底纹的尺寸,不使用裁剪后的局部面积
// 入参: object 图形对象, state 继承状态 // 入参: object 图形对象, state 继承状态
// 返回: error 取消或几何错误 // 返回: error 取消或几何错误
func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState) 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 { if err != nil {
return err 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) local := NewMatrix(img.CTM)
if img.CTM == "" { if img.CTM == "" {
local = Matrix{a: box.W, d: box.H} local = Matrix{a: box.W, d: box.H}
+187 -78
View File
@@ -23,6 +23,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"maps" "maps"
"path"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -57,6 +58,22 @@ type FontProcessOptions struct {
OnProgress func(stage string, completed, total int) error 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 嵌入当前文档指定字体,保留资源标识及全部使用位置,成功后可撤销 // EmbedFont 嵌入当前文档指定字体,保留资源标识及全部使用位置,成功后可撤销
// file为空时从编辑器字体来源精确匹配;显式文件表示调用方确认替换,仍校验字符和字形 // file为空时从编辑器字体来源精确匹配;显式文件表示调用方确认替换,仍校验字符和字形
// 入参: ctx 取消上下文, id 字体标识, file 字体文件, index 集合索引 // 入参: ctx 取消上下文, id 字体标识, file 字体文件, index 集合索引
@@ -149,6 +166,7 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces
if err != nil { if err != nil {
return nil, err return nil, err
} }
pending := &editorFontChange{patches: make(map[string][]editorXMLPatch), files: make(map[string][]byte)}
ids := slices.Sorted(maps.Keys(scan.fonts)) ids := slices.Sorted(maps.Keys(scan.fonts))
ids = slices.DeleteFunc(ids, func(id string) bool { ids = slices.DeleteFunc(ids, func(id string) bool {
return (mode == FontEmbed) == (scan.fonts[id].location != "") return (mode == FontEmbed) == (scan.fonts[id].location != "")
@@ -166,13 +184,20 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces
return nil, err return nil, err
} }
item := FontChange{Document: document, ID: id, Name: declaration.font.FontName} 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 err != nil {
if ctx.Err() != nil { if ctx.Err() != nil {
return nil, ctx.Err() return nil, ctx.Err()
} }
item.Reason = err.Error() 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) report = append(report, item)
if err := progress("fonts", len(report), total); err != nil { if err := progress("fonts", len(report), total); err != nil {
return nil, err return nil, err
@@ -182,13 +207,12 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces
if mode == FontExternal { if mode == FontExternal {
externalFiles = append(externalFiles, declaration.location) externalFiles = append(externalFiles, declaration.location)
} }
scan, err = scanEditorFonts(ctx, reader, document) }
if err != nil { }
if err := pending.apply(ctx, scan); err != nil {
return nil, err return nil, err
} }
} }
}
}
if err := progress("fonts", len(report), len(report)); err != nil { if err := progress("fonts", len(report), len(report)); err != nil {
return nil, err return nil, err
} }
@@ -208,26 +232,40 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces
// 入参: ctx 取消上下文, scan 文档引用, id 字体标识, mode 保存方式, file 字体文件, index 集合索引 // 入参: ctx 取消上下文, scan 文档引用, id 字体标识, mode 保存方式, file 字体文件, index 集合索引
// 返回: bool 是否修改, error 字体或结构错误 // 返回: bool 是否修改, error 字体或结构错误
func (e *Editor) changeFontSnapshot(ctx context.Context, scan *editorFontDocument, id string, mode FontMode, file FontFile, index int) (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)] declaration := scan.fonts[editorResourceID(id)]
if declaration == nil { 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 != "") { if (mode == FontEmbed) == (declaration.location != "") {
return false, nil return nil, nil
} }
if scan.unsafe { 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 { if e.backends.FontResources == nil {
return false, ErrBackendUnavailable return nil, ErrBackendUnavailable
} }
var data []byte var data []byte
var err error var err error
explicit := mode == FontEmbed && len(file.Data) != 0 explicit := mode == FontEmbed && len(file.Data) != 0
if mode == FontExternal { if mode == FontExternal {
data, err = scan.reader.readFile(declaration.location) data, err = scan.fontCache.read(ctx, scan.reader, declaration.location)
if err != nil { if err != nil {
return false, err return nil, err
} }
file = FontFile{Data: data} file = FontFile{Data: data}
if bytes.HasPrefix(data, []byte("ttcf")) { 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 { } else if len(file.Data) == 0 {
file, err = e.resolveBindingFont(scan, declaration) file, err = e.resolveBindingFont(scan, declaration)
if err != nil { if err != nil {
return false, err return nil, err
} }
index = 0 index = 0
} }
parsed, err := e.backends.FontResources.OpenFontResource(file, index) parsed, err := scan.fontCache.open(ctx, e.backends.FontResources, file, index)
if err != nil { if err != nil {
return false, err return nil, err
} }
if mode == FontEmbed { if mode == FontEmbed {
if err := validateFontEmbedding(parsed.Data); err != nil { 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 verifiedGlyphs := false
for _, name := range slices.Sorted(maps.Keys(scan.files)) { for _, text := range scan.texts[editorResourceID(id)] {
if err := ctx.Err(); err != nil { 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 var object TextObject
if err := xml.Unmarshal(file.data[node.start:node.end], &object); err != nil { if err := xml.Unmarshal(file.data[node.start:node.end], &object); err != nil {
return false, err return nil, err
} }
if err := checkFontBinding(object, parsed, mode); err != nil { if err := checkFontBinding(object, parsed, mode); err != nil {
return false, err return nil, err
} }
if explicit && len(object.CGTransform) != 0 && !verifiedGlyphs { if explicit && len(object.CGTransform) != 0 && !verifiedGlyphs {
source, err := e.resolveBindingFont(scan, declaration) source, err := e.resolveBindingFont(scan, declaration)
if err != nil || !sameBindingFont(source.Data, parsed.Data) { if err != nil || !sameBindingFont(source.Data, parsed.Data) {
return false, fmt.Errorf("font glyph mapping requires the original font") return nil, fmt.Errorf("font glyph mapping requires the original font")
} }
verifiedGlyphs = true verifiedGlyphs = true
} }
if mode == FontExternal { if mode == FontExternal {
for _, child := range node.children { for _, child := range node.children {
if child.matchesOFD("CGTransform") { if child.matchesOFD("CGTransform") {
patches = append(patches, editorXMLPatch{start: child.start, end: child.end}) 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 data = declaration.file.data
node := declaration.node 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") fontFile := node.child("FontFile")
var fragment []byte
if mode == FontEmbed { if mode == FontEmbed {
digest := sha256.Sum256(parsed.Data) digest := sha256.Sum256(parsed.Data)
name := fmt.Sprintf("Fonts/%x%s", digest, parsed.Extension) name, reused, err := fontBindingFile(ctx, scan.reader, pending, fmt.Sprintf("Fonts/%x%s", digest, parsed.Extension), parsed.Data)
if existing, err := scan.reader.readFile(name); err != nil || !bytes.Equal(existing, parsed.Data) { if err != nil {
name = packageAvailableName(scan.reader, changes, name) return nil, err
changes[name] = bytes.Clone(parsed.Data) }
if !reused {
change.files[name] = bytes.Clone(parsed.Data)
} }
content := node.textXML("FontFile", "/"+name) content := node.textXML("FontFile", "/"+name)
if fontFile != nil { 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 { } 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 { } 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 { } else {
if fontFile == nil { 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"} { for _, field := range []string{"FontName", "FamilyName"} {
root, err := parseEditorXML(fragment) root, err := parseEditorXML(fragment)
if err != nil { if err != nil {
return false, err return nil, err
} }
value := root.attr(field) value := root.attr(field)
if stripped := externalFontName(value); stripped != value { if stripped := externalFontName(value); stripped != value {
fragment, err = editorXMLAttribute(fragment, root, field, stripped) fragment, err = editorXMLAttribute(fragment, root, field, stripped)
if err != nil { if err != nil {
return false, err return nil, err
} }
} }
} }
data = editorPatchXML(data, []editorXMLPatch{{node.start, node.end, fragment}})
} }
if explicit { if explicit {
for _, field := range []struct{ name, value string }{{"FontName", parsed.Font.FontName}, {"FamilyName", parsed.Font.FamilyName}, {"Charset", "unicode"}} { 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
}
fragment, err := editorXMLAttribute(data, node, field.name, field.value)
if err != nil {
return false, 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
}
return true, nil
}
// fontBindingNode 在修改后的资源XML中重新定位字体声明
// 入参: data 资源XML, id 字体标识
// 返回: *editorXML 字体节点, error 解析或查找错误
func fontBindingNode(data []byte, id string) (*editorXML, error) {
root, err := parseEditorXML(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for node := range editorResourceNodes(root, "Fonts", "Font") { fragment, err = editorXMLAttribute(fragment, root, field.name, field.value)
if editorResourceID(node.attr("ID")) == editorResourceID(id) { if err != nil {
return node, nil return nil, err
}
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
change.patches[declaration.file.name] = append(change.patches[declaration.file.name], editorXMLPatch{node.start, node.end, fragment})
return change, nil
}
// 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
}
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 比较独立字体的全部数据表,忽略重新封装产生的校验和差异 // sameBindingFont 比较独立字体的全部数据表,忽略重新封装产生的校验和差异
+13 -6
View File
@@ -30,6 +30,8 @@ type editorFontDocument struct {
index int index int
files map[string]*editorFontXML files map[string]*editorFontXML
fonts map[string]*editorFontDeclaration fonts map[string]*editorFontDeclaration
texts map[string][]editorFontText
fontCache editorBindingFontCache
unsafe bool unsafe bool
} }
@@ -37,8 +39,12 @@ type editorFontDocument struct {
type editorFontXML struct { type editorFontXML struct {
name string name string
data []byte data []byte
root *editorXML }
texts []*editorXML
// editorFontText 关联文字节点及其原始文件,按字体标识归集
type editorFontText struct {
file *editorFontXML
node *editorXML
} }
// editorFontDeclaration 关联字体声明、所属资源文件及嵌入数据路径 // editorFontDeclaration 关联字体声明、所属资源文件及嵌入数据路径
@@ -53,7 +59,7 @@ type editorFontDeclaration struct {
// 入参: ctx 取消上下文, reader 包快照, index 文档索引 // 入参: ctx 取消上下文, reader 包快照, index 文档索引
// 返回: *editorFontDocument 字体引用, error 读取或结构错误 // 返回: *editorFontDocument 字体引用, error 读取或结构错误
func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*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) entry, err := reader.documentEntryContext(ctx, index)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -76,7 +82,7 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon
if err != nil { if err != nil {
return nil, err return nil, err
} }
file := &editorFontXML{name: name, data: data, root: root} file := &editorFontXML{name: name, data: data}
scan.files[name] = file scan.files[name] = file
base := "" base := ""
if root.matchesOFD("Res") { 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} 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") { if node.matchesOFD("TextObject") || node.matchesOFD("Text") && node.parent != nil && node.parent.matchesOFD("Area") {
file.texts = append(file.texts, node) id := editorResourceID(node.attr("Font"))
if node.attr("Font") == "" { scan.texts[id] = append(scan.texts[id], editorFontText{file: file, node: node})
if id == "" {
scan.unsafe = true scan.unsafe = true
} }
} }
+126 -21
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io"
"iter" "iter"
"strconv" "strconv"
"strings" "strings"
@@ -31,6 +32,32 @@ type graphicObjectTarget struct {
composite *[]CompositeGraphicUnit 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 返回对象动作的独立副本,不修改原对象 // Actions 返回对象动作的独立副本,不修改原对象
// 返回: []Action 动作列表 // 返回: []Action 动作列表
func (o GraphicObject) Actions() []Action { func (o GraphicObject) Actions() []Action {
@@ -57,17 +84,45 @@ func (clips *Clips) usesObjectMatrix() bool {
// 入参: d XML解码器, start 起始节点 // 入参: d XML解码器, start 起始节点
// 返回: error 解码错误 // 返回: error 解码错误
func (clips *Clips) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (clips *Clips) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type plain Clips *clips = Clips{}
var value plain
if classifyOFDNamespace(start.Name.Space) == ofdXMLLegacy { if classifyOFDNamespace(start.Name.Space) == ofdXMLLegacy {
transform := true transform := true
value.TransFlag = &transform clips.TransFlag = &transform
} }
if err := d.DecodeElement(&value, &start); err != nil { 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 return err
} }
*clips = Clips(value) clips.TransFlag = &transform
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 解析路径并区分省略样式、显式实线与零值描边参数 // UnmarshalXML 解析路径并区分省略样式、显式实线与零值描边参数
@@ -75,27 +130,77 @@ func (clips *Clips) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// 返回: error 错误信息 // 返回: error 错误信息
func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { func (p *PathObject) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type plain PathObject type plain PathObject
var value struct { var value plain
plain if err := decodeGraphicAttributes(start, &value); err != nil {
DashPattern *string `xml:"DashPattern,attr"`
}
if err := d.DecodeElement(&value, &start); err != nil {
return err return err
} }
*p = PathObject(value.plain) *p = PathObject(value)
for _, attr := range start.Attr { for _, attr := range ofdXMLStart(start).Attr {
if attr.Name.Local == "LineWidth" { switch attr.Name.Local {
case "LineWidth":
p.LineWidthSet = p.LineWidth == 0 p.LineWidthSet = p.LineWidth == 0
} case "MiterLimit":
if attr.Name.Local == "MiterLimit" {
p.MiterLimitSet = p.MiterLimit == 0 p.MiterLimitSet = p.MiterLimit == 0
} case "DashPattern":
}
if value.DashPattern != nil {
p.DashPattern = *value.DashPattern
p.dashPatternSet = p.DashPattern == "" p.dashPatternSet = p.DashPattern == ""
} }
return nil }
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
}
*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 解析绘制参数并保留显式实线和零值参数对基础参数的覆盖 // UnmarshalXML 解析绘制参数并保留显式实线和零值参数对基础参数的覆盖
+73 -46
View File
@@ -14,74 +14,101 @@
package ofdgo package ofdgo
import "encoding/xml" import (
"encoding/xml"
// textObjectTokens 记录字形变换所属TextCode,供解码后转换为对象内索引 "strconv"
type textObjectTokens struct { "strings"
decoder *xml.Decoder )
start *xml.StartElement
depth, code int
transforms []int
}
// UnmarshalXML 解析字形变换,省略的字符数和字形数均取1 // UnmarshalXML 解析字形变换,省略的字符数和字形数均取1
// 入参: decoder XML解码器, start 变换起始节点 // 入参: decoder XML解码器, start 变换起始节点
// 返回: error 解码错误 // 返回: error 解码错误
func (transform *CGTransform) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { func (transform *CGTransform) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
type plain CGTransform *transform = CGTransform{CodeCount: 1, GlyphCount: 1}
value := plain{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 { if err := decoder.DecodeElement(&value, &start); err != nil {
return err return err
} }
*transform = CGTransform(value) *code = TextCode(value)
return nil return nil
} }
// Token 转发XML令牌并记录直接子节点顺序 // UnmarshalXML 解析标准文字节点,将各TextCode前的局部字形位置统一为对象内索引
// 返回: 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前的局部字形位置统一为对象内索引
// 入参: decoder XML解码器, start 对象起始节点 // 入参: decoder XML解码器, start 对象起始节点
// 返回: error 解码错误 // 返回: error 解码错误
func (obj *TextObject) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { func (obj *TextObject) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error {
type plain TextObject type plain TextObject
stream := &textObjectTokens{decoder: decoder, start: &start}
var value plain var value plain
if err := xml.NewTokenDecoder(stream).Decode(&value); err != nil { if err := decodeGraphicAttributes(start, &value); err != nil {
return err return err
} }
offsets := make([]int, len(value.TextCode)) offset, pending := 0, 0
for i := 1; i < len(offsets); i++ { if err := decodeOFDChildren(decoder, start, func(d *xml.Decoder, node xml.StartElement) error {
offsets[i] = offsets[i-1] + len(textCodeRunes(value.TextCode[i-1].Value)) 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, code := range stream.transforms { for i := pending; i < len(value.CGTransform); i++ {
if code < len(offsets) { value.CGTransform[i].CodePosition += offset
value.CGTransform[i].CodePosition += offsets[code]
} }
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) *obj = TextObject(value)
for _, attr := range start.Attr { for _, attr := range ofdXMLStart(start).Attr {
if attr.Name.Local == "LineWidth" { if attr.Name.Local == "LineWidth" {
obj.LineWidthSet = obj.LineWidth == 0 obj.LineWidthSet = obj.LineWidth == 0
} }