mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
778d36d373
commit
4b9dbd6447
6 files changed
+508
-152
No files matched your search
+102
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
+187
-78
@@ -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,13 +207,12 @@ 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 {
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
file := scan.files[name]
|
||||
var patches []editorXMLPatch
|
||||
for _, node := range file.texts {
|
||||
if editorResourceID(node.attr("Font")) != editorResourceID(id) {
|
||||
continue
|
||||
return nil, err
|
||||
}
|
||||
file, node := text.file, text.node
|
||||
var object TextObject
|
||||
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 {
|
||||
return false, err
|
||||
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 false, fmt.Errorf("font glyph mapping requires the original font")
|
||||
return nil, 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})
|
||||
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)
|
||||
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)
|
||||
root, err := parseEditorXML(fragment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for node := range editorResourceNodes(root, "Fonts", "Font") {
|
||||
if editorResourceID(node.attr("ID")) == editorResourceID(id) {
|
||||
return node, nil
|
||||
fragment, err = editorXMLAttribute(fragment, root, field.name, field.value)
|
||||
if err != 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 比较独立字体的全部数据表,忽略重新封装产生的校验和差异
|
||||
|
||||
@@ -30,6 +30,8 @@ type editorFontDocument struct {
|
||||
index int
|
||||
files map[string]*editorFontXML
|
||||
fonts map[string]*editorFontDeclaration
|
||||
texts map[string][]editorFontText
|
||||
fontCache editorBindingFontCache
|
||||
unsafe bool
|
||||
}
|
||||
|
||||
@@ -37,8 +39,12 @@ type editorFontDocument struct {
|
||||
type editorFontXML struct {
|
||||
name string
|
||||
data []byte
|
||||
root *editorXML
|
||||
texts []*editorXML
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
+126
-21
@@ -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 {
|
||||
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 = Clips(value)
|
||||
return nil
|
||||
clips.TransFlag = &transform
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if value.DashPattern != nil {
|
||||
p.DashPattern = *value.DashPattern
|
||||
case "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 解析绘制参数并保留显式实线和零值参数对基础参数的覆盖
|
||||
|
||||
+73
-46
@@ -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))
|
||||
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, code := range stream.transforms {
|
||||
if code < len(offsets) {
|
||||
value.CGTransform[i].CodePosition += offsets[code]
|
||||
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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user