mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
d3a713f504
commit
528162a276
16 files changed
+3998
-254
No files matched your search
@@ -29,7 +29,7 @@ func NormalizeOS2(data []byte) []byte {
|
||||
var size int
|
||||
switch binary.BigEndian.Uint16(data) {
|
||||
case 0:
|
||||
// 旧式68字节表保持原样,不将截断的78字节表推断为旧式表。
|
||||
// 旧式68字节表保持原样,不将截断的78字节表推断为旧式表
|
||||
size = 78
|
||||
case 1:
|
||||
size = 86
|
||||
|
||||
@@ -118,7 +118,7 @@ func (e *Editor) CopyObjectsContext(ctx context.Context, page int, objects []Gra
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// copyObjectsContext 复制标准对象及资源来源,由上层按需同步富文本标引。
|
||||
// copyObjectsContext 复制标准对象及资源来源,由上层按需同步富文本标引
|
||||
// 入参: ctx 取消上下文, page 目标页, objects 对象快照, dx 横向位移, dy 纵向位移
|
||||
// 返回: []string 新对象标识, error 校验或取消错误
|
||||
func (e *Editor) copyObjectsContext(ctx context.Context, page int, objects []GraphicObject, dx, dy float64) ([]string, error) {
|
||||
@@ -529,7 +529,7 @@ func (e *Editor) DeleteObjects(page int, ids []string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// deleteObjects 删除已选择图元,由上层同步必要的富文本标引。
|
||||
// deleteObjects 删除已选择图元,由上层同步必要的富文本标引
|
||||
// 入参: page 页面索引, ids 对象标识
|
||||
// 返回: error 校验或删除错误
|
||||
func (e *Editor) deleteObjects(page int, ids []string) error {
|
||||
|
||||
+41
-41
@@ -25,8 +25,8 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Bookmarks 按声明顺序读取当前文档版本的命名书签,返回独立副本,不读取跳转引用。
|
||||
// 返回: []Bookmark 书签列表,无书签时为nil;error 读取、结构或名称重复错误。
|
||||
// Bookmarks 按声明顺序读取当前文档版本的命名书签,返回独立副本,不读取跳转引用
|
||||
// 返回: []Bookmark 书签列表,无书签时为nil;error 读取、结构或名称重复错误
|
||||
func (e *Editor) Bookmarks() ([]Bookmark, error) {
|
||||
if e.source == nil {
|
||||
return nil, nil
|
||||
@@ -42,30 +42,30 @@ func (e *Editor) Bookmarks() ([]Bookmark, error) {
|
||||
return editorBookmarks(data, root)
|
||||
}
|
||||
|
||||
// AddBookmark 在当前文档版本末尾添加名称唯一的书签,保留扩展内容并记录一次撤销。
|
||||
// 入参: name 非空且区分大小写的名称,不自动裁剪空白;dest 目标,PageID为当前页面标识而非页序号。
|
||||
// 返回: error 名称、目标、页面或提交错误。
|
||||
// AddBookmark 在当前文档版本末尾添加名称唯一的书签,保留扩展内容并记录一次撤销
|
||||
// 入参: name 非空且区分大小写的名称,不自动裁剪空白;dest 目标,PageID为当前页面标识而非页序号
|
||||
// 返回: error 名称、目标、页面或提交错误
|
||||
func (e *Editor) AddBookmark(name string, dest Dest) error {
|
||||
return e.editBookmark("", &Bookmark{Name: name, Dest: dest}, true)
|
||||
}
|
||||
|
||||
// UpdateBookmark 按原名称更新书签,重命名同步当前文档全部标准跳转引用,整体可撤销。
|
||||
// 入参: name 原名称;next 完整的新书签,Dest.PageID为当前页面标识;名称不变时仅修改目标。
|
||||
// 返回: error 名称不存在、重名、目标无效、引用读取或提交错误;内容未变时不记录撤销。
|
||||
// UpdateBookmark 按原名称更新书签,重命名同步当前文档全部标准跳转引用,整体可撤销
|
||||
// 入参: name 原名称;next 完整的新书签,Dest.PageID为当前页面标识;名称不变时仅修改目标
|
||||
// 返回: error 名称不存在、重名、目标无效、引用读取或提交错误;内容未变时不记录撤销
|
||||
func (e *Editor) UpdateBookmark(name string, next Bookmark) error {
|
||||
return e.editBookmark(name, &next, false)
|
||||
}
|
||||
|
||||
// DeleteBookmark 删除当前文档版本的命名书签,存在标准跳转引用时拒绝删除。
|
||||
// 入参: name 书签名称;其他文档和历史版本的引用不阻止当前版本删除。
|
||||
// 返回: error 名称不存在、仍被引用、引用读取或提交错误。
|
||||
// DeleteBookmark 删除当前文档版本的命名书签,存在标准跳转引用时拒绝删除
|
||||
// 入参: name 书签名称;其他文档和历史版本的引用不阻止当前版本删除
|
||||
// 返回: error 名称不存在、仍被引用、引用读取或提交错误
|
||||
func (e *Editor) DeleteBookmark(name string) error {
|
||||
return e.editBookmark(name, nil, false)
|
||||
}
|
||||
|
||||
// editorBookmarks 读取标准书签定义,拒绝重复集合、重名及缺失或重复目标,不解析扩展子树。
|
||||
// 入参: data 文档原文;root 文档根节点。
|
||||
// 返回: []Bookmark 独立定义列表;error 结构或解码错误。
|
||||
// editorBookmarks 读取标准书签定义,拒绝重复集合、重名及缺失或重复目标,不解析扩展子树
|
||||
// 入参: data 文档原文;root 文档根节点
|
||||
// 返回: []Bookmark 独立定义列表;error 结构或解码错误
|
||||
func editorBookmarks(data []byte, root *editorXML) ([]Bookmark, error) {
|
||||
if root.childAt("Bookmarks", 1) != nil {
|
||||
return nil, fmt.Errorf("multiple bookmark collections")
|
||||
@@ -104,9 +104,9 @@ func editorBookmarks(data []byte, root *editorXML) ([]Bookmark, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// validateBookmarkName 校验非空名称及XML可无损表示的Unicode字符。
|
||||
// 入参: name 名称。
|
||||
// 返回: error 名称错误。
|
||||
// validateBookmarkName 校验非空名称及XML可无损表示的Unicode字符
|
||||
// 入参: name 名称
|
||||
// 返回: error 名称错误
|
||||
func validateBookmarkName(name string) error {
|
||||
if strings.TrimSpace(name) == "" || !utf8.ValidString(name) {
|
||||
return fmt.Errorf("bookmark name must be nonempty valid UTF-8")
|
||||
@@ -119,9 +119,9 @@ func validateBookmarkName(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateBookmark 校验书签名称、标准目标参数及当前编辑页树中的目标标识。
|
||||
// 入参: bookmark 待写入定义。
|
||||
// 返回: error 名称、目标或页面错误。
|
||||
// validateBookmark 校验书签名称、标准目标参数及当前编辑页树中的目标标识
|
||||
// 入参: bookmark 待写入定义
|
||||
// 返回: error 名称、目标或页面错误
|
||||
func (e *Editor) validateBookmark(bookmark Bookmark) error {
|
||||
if err := validateBookmarkName(bookmark.Name); err != nil {
|
||||
return err
|
||||
@@ -138,9 +138,9 @@ func (e *Editor) validateBookmark(bookmark Bookmark) error {
|
||||
return fmt.Errorf("bookmark destination page %q does not exist", bookmark.Dest.PageID)
|
||||
}
|
||||
|
||||
// editBookmark 校验定义后原子提交局部XML补丁,重命名使用包含未保存编辑的完整快照。
|
||||
// 入参: name 原名称;next 新定义,nil表示删除;add 是否新增。
|
||||
// 返回: error 校验、引用扫描或提交错误。
|
||||
// editBookmark 校验定义后原子提交局部XML补丁,重命名使用包含未保存编辑的完整快照
|
||||
// 入参: name 原名称;next 新定义,nil表示删除;add 是否新增
|
||||
// 返回: error 校验、引用扫描或提交错误
|
||||
func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error {
|
||||
if !add {
|
||||
if err := validateBookmarkName(name); err != nil {
|
||||
@@ -241,9 +241,9 @@ func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error {
|
||||
})
|
||||
}
|
||||
|
||||
// commitBookmarkSnapshot 接入重命名后的包,保留已加载对象的会话排版、裁剪及尚未使用的注册资源。
|
||||
// 入参: reader 已完成隔离和重命名的包快照。
|
||||
// 返回: error 快照重建或页面加载错误。
|
||||
// commitBookmarkSnapshot 接入重命名后的包,保留已加载对象的会话排版、裁剪及尚未使用的注册资源
|
||||
// 入参: reader 已完成隔离和重命名的包快照
|
||||
// 返回: error 快照重建或页面加载错误
|
||||
func (e *Editor) commitBookmarkSnapshot(reader *Reader) error {
|
||||
before := e.transactionSnapshot()
|
||||
if err := e.commitResourceSnapshot(context.Background(), reader); err != nil {
|
||||
@@ -282,9 +282,9 @@ func (e *Editor) commitBookmarkSnapshot(reader *Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitBookmarkParts 隔离修改后复用注解快照提交,保守复制路径不覆盖未知扩展可能引用的原文件。
|
||||
// 入参: base 原包快照;parts 当前入口修改集。
|
||||
// 返回: error 依赖解析、隔离或提交错误。
|
||||
// commitBookmarkParts 隔离修改后复用注解快照提交,保守复制路径不覆盖未知扩展可能引用的原文件
|
||||
// 入参: base 原包快照;parts 当前入口修改集
|
||||
// 返回: error 依赖解析、隔离或提交错误
|
||||
func (e *Editor) commitBookmarkParts(base *editorSource, parts map[string][]byte) error {
|
||||
changes, err := base.reader.versionChanges(context.Background(), parts)
|
||||
if err != nil {
|
||||
@@ -303,9 +303,9 @@ func (e *Editor) commitBookmarkParts(base *editorSource, parts map[string][]byte
|
||||
return e.commitAnnotationParts(&source, nil)
|
||||
}
|
||||
|
||||
// editorBookmarkIsolate 在无法完整分析其他入口时保护所有已有文件,复制修改文件及其标准引用祖先。
|
||||
// 入参: reader 原包;changes 当前入口修改集。
|
||||
// 返回: map[string][]byte 隔离后的修改集;error 当前入口依赖或版本清单错误。
|
||||
// editorBookmarkIsolate 在无法完整分析其他入口时保护所有已有文件,复制修改文件及其标准引用祖先
|
||||
// 入参: reader 原包;changes 当前入口修改集
|
||||
// 返回: map[string][]byte 隔离后的修改集;error 当前入口依赖或版本清单错误
|
||||
func editorBookmarkIsolate(reader *Reader, changes map[string][]byte) (map[string][]byte, error) {
|
||||
ctx := context.Background()
|
||||
entry, err := reader.DocumentRootContext(ctx)
|
||||
@@ -386,9 +386,9 @@ func editorBookmarkIsolate(reader *Reader, changes map[string][]byte) (map[strin
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
// editorBookmarkDefinition 修改指定书签的标准字段,保留其扩展属性、扩展节点及其他定义原文。
|
||||
// 入参: data 文档原文;name 原名称;next 新定义,nil表示删除;add 是否新增。
|
||||
// 返回: []byte 修改后的文档;error 结构或编码错误。
|
||||
// editorBookmarkDefinition 修改指定书签的标准字段,保留其扩展属性、扩展节点及其他定义原文
|
||||
// 入参: data 文档原文;name 原名称;next 新定义,nil表示删除;add 是否新增
|
||||
// 返回: []byte 修改后的文档;error 结构或编码错误
|
||||
func editorBookmarkDefinition(data []byte, name string, next *Bookmark, add bool) ([]byte, error) {
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
@@ -497,9 +497,9 @@ func editorBookmarkDefinition(data []byte, name string, next *Bookmark, add bool
|
||||
return nil, fmt.Errorf("bookmark %q not found", name)
|
||||
}
|
||||
|
||||
// editorBookmarkReferences 扫描当前入口依赖中的标准书签跳转,拒绝带引用删除或生成重命名补丁。
|
||||
// 入参: reader 包含未保存修改的快照;name 原名称;next 新定义,nil表示删除。
|
||||
// 返回: map[string][]byte 需要修改的结构文件;error 读取、解析或被引用错误。
|
||||
// editorBookmarkReferences 扫描当前入口依赖中的标准书签跳转,拒绝带引用删除或生成重命名补丁
|
||||
// 入参: reader 包含未保存修改的快照;name 原名称;next 新定义,nil表示删除
|
||||
// 返回: map[string][]byte 需要修改的结构文件;error 读取、解析或被引用错误
|
||||
func editorBookmarkReferences(reader *Reader, name string, next *Bookmark) (map[string][]byte, error) {
|
||||
_, structures, err := reader.versionDependencies(context.Background(), reader.documentRoot, nil, nil)
|
||||
if err != nil {
|
||||
@@ -522,9 +522,9 @@ func editorBookmarkReferences(reader *Reader, name string, next *Bookmark) (map[
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
// editorBookmarkReferenceXML 仅识别标准动作中的名称引用,保留扩展命名空间及不透明扩展负载。
|
||||
// 入参: data 结构原文;file 错误上下文中的文件名;name 原名称;next 新定义,nil表示检查删除。
|
||||
// 返回: []byte 修改后的原文;error 解析、属性编码或被引用错误。
|
||||
// editorBookmarkReferenceXML 仅识别标准动作中的名称引用,保留扩展命名空间及不透明扩展负载
|
||||
// 入参: data 结构原文;file 错误上下文中的文件名;name 原名称;next 新定义,nil表示检查删除
|
||||
// 返回: []byte 修改后的原文;error 解析、属性编码或被引用错误
|
||||
func editorBookmarkReferenceXML(data []byte, file, name string, next *Bookmark) ([]byte, error) {
|
||||
root, err := parseEditorXML(data)
|
||||
if err != nil {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+22
-22
@@ -23,8 +23,8 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// LayerInfo 描述普通页面图层,Order为同类型零基顺序,ObjectCount包含页块中的图元。
|
||||
// ObjectIDs按层内文件顺序列出图元标识,包含PageBlock成员但不展开复合图元,空层返回空切片。
|
||||
// LayerInfo 描述普通页面图层,Order为同类型零基顺序,ObjectCount包含页块中的图元
|
||||
// ObjectIDs按层内文件顺序列出图元标识,包含PageBlock成员但不展开复合图元,空层返回空切片
|
||||
type LayerInfo struct {
|
||||
ID string
|
||||
Type string
|
||||
@@ -34,7 +34,7 @@ type LayerInfo struct {
|
||||
ObjectIDs []string
|
||||
}
|
||||
|
||||
// Layers 按文件顺序返回普通页面图层,省略的类型规范化为Body,不包含模板图层。
|
||||
// Layers 按文件顺序返回普通页面图层,省略的类型规范化为Body,不包含模板图层
|
||||
// 入参: page 零基页面索引
|
||||
// 返回: []LayerInfo 独立图层清单, error 页面读取错误
|
||||
func (e *Editor) Layers(page int) ([]LayerInfo, error) {
|
||||
@@ -45,7 +45,7 @@ func (e *Editor) Layers(page int) ([]LayerInfo, error) {
|
||||
return editorLayerInfos(p), nil
|
||||
}
|
||||
|
||||
// Layers 直接读取普通页面图层,不创建编辑器或分配新标识,返回值不持有页面可变数据。
|
||||
// Layers 直接读取普通页面图层,不创建编辑器或分配新标识,返回值不持有页面可变数据
|
||||
// 入参: page 零基页面索引
|
||||
// 返回: []LayerInfo 按文件顺序排列的图层, error 页面读取错误
|
||||
func (r *Reader) Layers(page int) ([]LayerInfo, error) {
|
||||
@@ -56,7 +56,7 @@ func (r *Reader) Layers(page int) ([]LayerInfo, error) {
|
||||
return editorLayerInfos(p), nil
|
||||
}
|
||||
|
||||
// editorLayerInfos 将页面图层投影为不共享可变容器的管理清单。
|
||||
// editorLayerInfos 将页面图层投影为不共享可变容器的管理清单
|
||||
// 入参: p 已读取的页面
|
||||
// 返回: []LayerInfo 图层清单
|
||||
func editorLayerInfos(p *PageContent) []LayerInfo {
|
||||
@@ -74,7 +74,7 @@ func editorLayerInfos(p *PageContent) []LayerInfo {
|
||||
return result
|
||||
}
|
||||
|
||||
// AddLayer 追加空图层并返回新标识,空类型采用Body,空DrawParam表示不设置继承参数。
|
||||
// AddLayer 追加空图层并返回新标识,空类型采用Body,空DrawParam表示不设置继承参数
|
||||
// 入参: page 页面索引, layerType 图层类型, drawParam 绘制参数标识
|
||||
// 返回: string 新图层标识, error 类型、资源或结构错误
|
||||
func (e *Editor) AddLayer(page int, layerType, drawParam string) (string, error) {
|
||||
@@ -120,7 +120,7 @@ func (e *Editor) AddLayer(page int, layerType, drawParam string) (string, error)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// SetLayerType 设置图层类型,不改变图元坐标及绘制参数;同类型顺序由文件位置决定。
|
||||
// SetLayerType 设置图层类型,不改变图元坐标及绘制参数;同类型顺序由文件位置决定
|
||||
// 入参: page 页面索引, id 图层标识, layerType 图层类型
|
||||
// 返回: error 标识、类型或结构错误
|
||||
func (e *Editor) SetLayerType(page int, id, layerType string) error {
|
||||
@@ -140,7 +140,7 @@ func (e *Editor) SetLayerType(page int, id, layerType string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// SetLayerDrawParam 设置图层绘制参数,空标识清除引用,图元局部覆盖保持不变。
|
||||
// SetLayerDrawParam 设置图层绘制参数,空标识清除引用,图元局部覆盖保持不变
|
||||
// 入参: page 页面索引, id 图层标识, drawParam 绘制参数标识
|
||||
// 返回: error 标识、资源或结构错误
|
||||
func (e *Editor) SetLayerDrawParam(page int, id, drawParam string) error {
|
||||
@@ -159,7 +159,7 @@ func (e *Editor) SetLayerDrawParam(page int, id, drawParam string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// MoveLayer 调整图层在同类型图层中的零基顺序,保留其他类型及扩展节点的位置。
|
||||
// MoveLayer 调整图层在同类型图层中的零基顺序,保留其他类型及扩展节点的位置
|
||||
// 入参: page 页面索引, id 图层标识, order 同类型目标顺序
|
||||
// 返回: error 标识、顺序或结构错误
|
||||
func (e *Editor) MoveLayer(page int, id string, order int) error {
|
||||
@@ -178,7 +178,7 @@ func (e *Editor) MoveLayer(page int, id string, order int) error {
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteLayer 删除空图层,含图元、页块或扩展子元素时拒绝;可先在Transaction中迁移图元。
|
||||
// DeleteLayer 删除空图层,含图元、页块或扩展子元素时拒绝;可先在Transaction中迁移图元
|
||||
// 入参: page 页面索引, id 图层标识
|
||||
// 返回: error 标识、非空图层或结构错误
|
||||
func (e *Editor) DeleteLayer(page int, id string) error {
|
||||
@@ -201,8 +201,8 @@ func (e *Editor) DeleteLayer(page int, id string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// MoveObjectsToLayer 将普通页面选区按原绘制顺序追加到目标层,保持标识、坐标、扩展及有效样式。
|
||||
// 不接受模板或注解标识;页块成员须先通过已有复合编辑接口处理,失败不产生部分移动。
|
||||
// MoveObjectsToLayer 将普通页面选区按原绘制顺序追加到目标层,保持标识、坐标、扩展及有效样式
|
||||
// 不接受模板或注解标识;页块成员须先通过已有复合编辑接口处理,失败不产生部分移动
|
||||
// 入参: page 页面索引, ids 不重复的普通图元标识, layerID 目标图层标识
|
||||
// 返回: error 选区、继承参数或结构错误
|
||||
func (e *Editor) MoveObjectsToLayer(page int, ids []string, layerID string) error {
|
||||
@@ -295,7 +295,7 @@ func (e *Editor) MoveObjectsToLayer(page int, ids []string, layerID string) erro
|
||||
})
|
||||
}
|
||||
|
||||
// editorLayerType 将标准省略类型规范化,不改变已明确指定的类型。
|
||||
// editorLayerType 将标准省略类型规范化,不改变已明确指定的类型
|
||||
// 入参: value 原类型
|
||||
// 返回: string 有效类型
|
||||
func editorLayerType(value string) string {
|
||||
@@ -305,14 +305,14 @@ func editorLayerType(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// validEditorLayerType 按正文表15校验可管理的图层类型。
|
||||
// validEditorLayerType 按正文表15校验可管理的图层类型
|
||||
// 入参: value 类型值
|
||||
// 返回: bool 是否为支持的标准类型
|
||||
func validEditorLayerType(value string) bool {
|
||||
return value == "Body" || value == "Background" || value == "Foreground"
|
||||
}
|
||||
|
||||
// validateLayerDrawParam 在目标页面资源环境中核验完整绘制参数继承链。
|
||||
// validateLayerDrawParam 在目标页面资源环境中核验完整绘制参数继承链
|
||||
// 入参: page 页面索引, id 绘制参数标识
|
||||
// 返回: error 页面或资源错误
|
||||
func (e *Editor) validateLayerDrawParam(page int, id string) error {
|
||||
@@ -323,7 +323,7 @@ func (e *Editor) validateLayerDrawParam(page int, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// editLayerPage 在完整包快照上编辑页面原文,事务失败回滚,提交时隔离共享页面及历史版本。
|
||||
// editLayerPage 在完整包快照上编辑页面原文,事务失败回滚,提交时隔离共享页面及历史版本
|
||||
// 入参: page 页面索引, edit 页面原文修改回调
|
||||
// 返回: error 读取、修改或提交错误
|
||||
func (e *Editor) editLayerPage(page int, edit func(*Reader, []byte, *editorXML) ([]byte, error)) error {
|
||||
@@ -380,7 +380,7 @@ func (e *Editor) editLayerPage(page int, edit func(*Reader, []byte, *editorXML)
|
||||
})
|
||||
}
|
||||
|
||||
// editorStructureChildren 仅枚举当前标准命名空间中的直接子元素。
|
||||
// editorStructureChildren 仅枚举当前标准命名空间中的直接子元素
|
||||
// 入参: root 容器节点, kind 子元素名称
|
||||
// 返回: []*editorXML 文件顺序节点清单
|
||||
func editorStructureChildren(root *editorXML, kind string) []*editorXML {
|
||||
@@ -395,7 +395,7 @@ func editorStructureChildren(root *editorXML, kind string) []*editorXML {
|
||||
return nodes
|
||||
}
|
||||
|
||||
// editorFindStructureID 按数值标识查找唯一直接子元素,拒绝缺失及歧义标识。
|
||||
// editorFindStructureID 按数值标识查找唯一直接子元素,拒绝缺失及歧义标识
|
||||
// 入参: root 容器节点, kind 元素名称, attribute 标识属性名, id 标识值
|
||||
// 返回: *editorXML 唯一节点, error 标识错误
|
||||
func editorFindStructureID(root *editorXML, kind, attribute, id string) (*editorXML, error) {
|
||||
@@ -417,7 +417,7 @@ func editorFindStructureID(root *editorXML, kind, attribute, id string) (*editor
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// editorStructureAttribute 局部替换或删除无命名空间属性,保留扩展及其余原文。
|
||||
// editorStructureAttribute 局部替换或删除无命名空间属性,保留扩展及其余原文
|
||||
// 入参: data 原文, node 目标节点, name 属性名, value 新值,空值删除属性
|
||||
// 返回: []byte 更新后的完整原文, error XML错误
|
||||
func editorStructureAttribute(data []byte, node *editorXML, name, value string) ([]byte, error) {
|
||||
@@ -432,7 +432,7 @@ func editorStructureAttribute(data []byte, node *editorXML, name, value string)
|
||||
return editorPatchXML(data, []editorXMLPatch{patch}), nil
|
||||
}
|
||||
|
||||
// editorAppendStructure 向普通或自闭合容器追加已绑定命名空间的元素。
|
||||
// editorAppendStructure 向普通或自闭合容器追加已绑定命名空间的元素
|
||||
// 入参: data 原文, node 容器节点, fragment 待追加片段
|
||||
// 返回: []byte 更新后的完整原文
|
||||
func editorAppendStructure(data []byte, node *editorXML, fragment []byte) []byte {
|
||||
@@ -442,7 +442,7 @@ func editorAppendStructure(data []byte, node *editorXML, fragment []byte) []byte
|
||||
return editorPatchXML(data, []editorXMLPatch{{node.close, node.close, fragment}})
|
||||
}
|
||||
|
||||
// editorMoveStructure 在指定同类节点位置间移动完整原文,不触及夹杂的扩展元素。
|
||||
// editorMoveStructure 在指定同类节点位置间移动完整原文,不触及夹杂的扩展元素
|
||||
// 入参: data 原文, nodes 同类节点, node 被移动节点, order 目标零基顺序
|
||||
// 返回: []byte 更新后的完整原文, error 顺序错误
|
||||
func editorMoveStructure(data []byte, nodes []*editorXML, node *editorXML, order int) ([]byte, error) {
|
||||
@@ -463,7 +463,7 @@ func editorMoveStructure(data []byte, nodes []*editorXML, node *editorXML, order
|
||||
return editorPatchXML(data, patches), nil
|
||||
}
|
||||
|
||||
// commitLayerTemplateParts 持久化结构与已注册资源,隔离共享文件并恢复未变图元的会话编辑状态。
|
||||
// commitLayerTemplateParts 持久化结构与已注册资源,隔离共享文件并恢复未变图元的会话编辑状态
|
||||
// 入参: reader 当前独立包快照, parts 文件修改集
|
||||
// 返回: error 资源、隔离或提交错误
|
||||
func (e *Editor) commitLayerTemplateParts(reader *Reader, parts map[string][]byte) error {
|
||||
|
||||
+27
-27
@@ -28,13 +28,13 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// richTextTagType 标识富文本编辑标引,正文使用标准复合资源和图元。
|
||||
// richTextTagType 标识富文本编辑标引,正文使用标准复合资源和图元
|
||||
const richTextTagType = "OFDGo.RichText"
|
||||
|
||||
// richTextNamespace 标识富文本编辑语义的命名空间,版本仅随不兼容的语义变更升级。
|
||||
// richTextNamespace 标识富文本编辑语义的命名空间,版本仅随不兼容的语义变更升级
|
||||
const richTextNamespace = "https://ofdgo.aite.me/ns/rich-text/1"
|
||||
|
||||
// richTextMetadata 不保存字体编号,以标准主文字对象序号恢复当前文档字体。
|
||||
// richTextMetadata 不保存字体编号,以标准主文字对象序号恢复当前文档字体
|
||||
type richTextMetadata struct {
|
||||
Version int `json:"version"`
|
||||
Value RichText `json:"value"`
|
||||
@@ -45,14 +45,14 @@ type richTextMetadata struct {
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
// richTextEntry 保存标引入口及延迟报告的损坏诊断。
|
||||
// richTextEntry 保存标引入口及延迟报告的损坏诊断
|
||||
type richTextEntry struct {
|
||||
index int
|
||||
metadata richTextMetadata
|
||||
err error
|
||||
}
|
||||
|
||||
// richTextIndex 缓存当前文档修订的标引,不创建页面预览快照。
|
||||
// richTextIndex 缓存当前文档修订的标引,不创建页面预览快照
|
||||
type richTextIndex struct {
|
||||
source *editorSource
|
||||
revision uint64
|
||||
@@ -60,7 +60,7 @@ type richTextIndex struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// AddRichText 原子添加标准组合及持久化编辑标引,成功时只产生一条历史。
|
||||
// AddRichText 原子添加标准组合及持久化编辑标引,成功时只产生一条历史
|
||||
// 入参: page 页面索引, box 页面编辑框, value 富文本内容
|
||||
// 返回: string 组合对象标识, error 排版、资源或持久化错误
|
||||
func (e *Editor) AddRichText(page int, box Box, value RichText) (string, error) {
|
||||
@@ -88,15 +88,15 @@ func (e *Editor) AddRichText(page int, box Box, value RichText) (string, error)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// UpdateRichText 原子替换富文本内容,保留对象标识、局部编辑框及页面变换。
|
||||
// 安全普通文字显式重排后提升为组合;不修改共享的旧组合资源。
|
||||
// UpdateRichText 原子替换富文本内容,保留对象标识、局部编辑框及页面变换
|
||||
// 安全普通文字显式重排后提升为组合;不修改共享的旧组合资源
|
||||
// 入参: page 页面索引, id 对象标识, value 完整富文本
|
||||
// 返回: error 识别、排版或提交错误
|
||||
func (e *Editor) UpdateRichText(page int, id string, value RichText) error {
|
||||
return e.updateRichTextFrame(page, id, value, 0, 0)
|
||||
}
|
||||
|
||||
// ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放。
|
||||
// ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放
|
||||
// 入参: page 页面索引, id 对象标识, offset 局部横向偏移, width 新的局部段落宽度
|
||||
// 返回: error 尺寸、排版或提交错误
|
||||
func (e *Editor) ResizeRichTextFrame(page int, id string, offset, width float64) error {
|
||||
@@ -110,7 +110,7 @@ func (e *Editor) ResizeRichTextFrame(page int, id string, offset, width float64)
|
||||
return e.updateRichTextFrame(page, id, value, offset, width)
|
||||
}
|
||||
|
||||
// updateRichTextFrame 统一提交正文或编辑框变化,width为零时保持原始宽度。
|
||||
// updateRichTextFrame 统一提交正文或编辑框变化,width为零时保持原始宽度
|
||||
// 入参: page 页面索引, id 对象标识, value 新内容, offset 局部偏移, width 可选宽度
|
||||
// 返回: error 校验或提交错误
|
||||
func (e *Editor) updateRichTextFrame(page int, id string, value RichText, offset, width float64) error {
|
||||
@@ -170,7 +170,7 @@ func (e *Editor) updateRichTextFrame(page int, id string, value RichText, offset
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteRichText 原子删除富文本及其标引,不删除其他实例引用的共享资源。
|
||||
// DeleteRichText 原子删除富文本及其标引,不删除其他实例引用的共享资源
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: error 识别或删除错误
|
||||
func (e *Editor) DeleteRichText(page int, id string) error {
|
||||
@@ -195,8 +195,8 @@ func (e *Editor) DeleteRichText(page int, id string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// RichText 返回独立的富文本编辑值,未标引组合返回ErrNotRichText,损坏标引返回诊断。
|
||||
// 普通文字仅在定位和外观可以安全提升时返回单片段,否则明确拒绝。
|
||||
// RichText 返回独立的富文本编辑值,未标引组合返回ErrNotRichText,损坏标引返回诊断
|
||||
// 普通文字仅在定位和外观可以安全提升时返回单片段,否则明确拒绝
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: RichText 编辑内容, error 识别或持久化错误
|
||||
func (e *Editor) RichText(page int, id string) (RichText, error) {
|
||||
@@ -238,7 +238,7 @@ func (e *Editor) RichText(page int, id string) (RichText, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// RichTextFrame 返回原始局部编辑框及局部到页面的完整变换,不按组合缩放重新解释宽度。
|
||||
// RichTextFrame 返回原始局部编辑框及局部到页面的完整变换,不按组合缩放重新解释宽度
|
||||
// 入参: page 页面索引, id 富文本或安全普通文字对象标识
|
||||
// 返回: Box 局部编辑框, Matrix 页面变换, error 识别或几何错误
|
||||
func (e *Editor) RichTextFrame(page int, id string) (Box, Matrix, error) {
|
||||
@@ -275,7 +275,7 @@ func (e *Editor) RichTextFrame(page int, id string) (Box, Matrix, error) {
|
||||
return entry.metadata.Frame, TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.CompositeGraphicUnit.CTM)).Multiply(TranslationMatrix(entry.metadata.Offset[0], entry.metadata.Offset[1])), nil
|
||||
}
|
||||
|
||||
// prepareRichText 构造独立组合资源和不含字体编号的编辑标引。
|
||||
// prepareRichText 构造独立组合资源和不含字体编号的编辑标引
|
||||
// 入参: box 页面或局部编辑框, value 富文本
|
||||
// 返回: GraphicObject 标准组合, richTextMetadata 标引, error 准备错误
|
||||
func (e *Editor) prepareRichText(box Box, value RichText) (GraphicObject, richTextMetadata, error) {
|
||||
@@ -322,7 +322,7 @@ func (e *Editor) prepareRichText(box Box, value RichText) (GraphicObject, richTe
|
||||
return object, meta, nil
|
||||
}
|
||||
|
||||
// richTextPlacement 根据局部逻辑坐标确定物理组合边界,负向装饰不会被资源裁剪。
|
||||
// richTextPlacement 根据局部逻辑坐标确定物理组合边界,负向装饰不会被资源裁剪
|
||||
// 入参: meta 编辑框与资源尺寸, matrix 逻辑坐标到页面变换
|
||||
// 返回: CompositeGraphicUnit 组合几何
|
||||
func richTextPlacement(meta richTextMetadata, matrix Matrix) CompositeGraphicUnit {
|
||||
@@ -331,7 +331,7 @@ func richTextPlacement(meta richTextMetadata, matrix Matrix) CompositeGraphicUni
|
||||
return CompositeGraphicUnit{Boundary: editorBoxString(box), CTM: TranslationMatrix(-box.X, -box.Y).Multiply(matrix).String()}
|
||||
}
|
||||
|
||||
// richTextDigest 对已编码图元计算不含字体、颜色空间和字形编号的内容摘要。
|
||||
// richTextDigest 对已编码图元计算不含字体、颜色空间和字形编号的内容摘要
|
||||
// 入参: objects 标准子对象
|
||||
// 返回: string 稳定内容摘要
|
||||
func richTextDigest(objects []GraphicObject) string {
|
||||
@@ -363,7 +363,7 @@ func richTextDigest(objects []GraphicObject) string {
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// richTextEntry 从当前文档标引缓存定位对象,损坏项不降级为普通组合。
|
||||
// richTextEntry 从当前文档标引缓存定位对象,损坏项不降级为普通组合
|
||||
// 入参: page 页面索引, id 组合标识
|
||||
// 返回: richTextEntry 标引, error 缺失或损坏错误
|
||||
func (e *Editor) richTextEntry(page int, id string) (richTextEntry, error) {
|
||||
@@ -381,7 +381,7 @@ func (e *Editor) richTextEntry(page int, id string) (richTextEntry, error) {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// richTextMetadataIndex 按来源和修订缓存标引,仅读取标引文件,不生成Reader快照。
|
||||
// richTextMetadataIndex 按来源和修订缓存标引,仅读取标引文件,不生成Reader快照
|
||||
// 返回: *richTextIndex 只读缓存
|
||||
func (e *Editor) richTextMetadataIndex() *richTextIndex {
|
||||
if index := e.richIndex; index != nil && index.source == e.source && index.revision == e.revision {
|
||||
@@ -437,7 +437,7 @@ func (e *Editor) richTextMetadataIndex() *richTextIndex {
|
||||
return index
|
||||
}
|
||||
|
||||
// putRichTextMetadata 添加或替换带有标准标引引用的富文本语义。
|
||||
// putRichTextMetadata 添加或替换带有标准标引引用的富文本语义
|
||||
// 入参: page 页面索引, id 对象标识, meta 编辑信息, index 原入口或-1
|
||||
// 返回: error 编码或提交错误
|
||||
func (e *Editor) putRichTextMetadata(page int, id string, meta richTextMetadata, index int) error {
|
||||
@@ -454,7 +454,7 @@ func (e *Editor) putRichTextMetadata(page int, id string, meta richTextMetadata,
|
||||
return err
|
||||
}
|
||||
|
||||
// promoteRichText 仅提升已有明确排版或可验证单行定位的普通文字。
|
||||
// promoteRichText 仅提升已有明确排版或可验证单行定位的普通文字
|
||||
// 入参: page 页面索引, object 普通文字快照
|
||||
// 返回: RichText 单片段, error 不安全提升错误
|
||||
func (e *Editor) promoteRichText(page int, object GraphicObject) (RichText, error) {
|
||||
@@ -524,7 +524,7 @@ func (e *Editor) promoteRichText(page int, object GraphicObject) (RichText, erro
|
||||
return result, result.Validate()
|
||||
}
|
||||
|
||||
// richPlainOffset 保存可提升单行原文的基线偏移,不推断复杂定位布局。
|
||||
// richPlainOffset 保存可提升单行原文的基线偏移,不推断复杂定位布局
|
||||
// 入参: text 已校验的普通文字
|
||||
// 返回: float64 横向偏移, float64 基线偏移, error 无效坐标
|
||||
func (e *Editor) richPlainOffset(text TextObject) (float64, float64, error) {
|
||||
@@ -553,7 +553,7 @@ func (e *Editor) richPlainOffset(text TextObject) (float64, float64, error) {
|
||||
return x, y - baseline, nil
|
||||
}
|
||||
|
||||
// copyRichTextMetadata 复制已识别组合的语义,使用目标标准子对象的字体引用。
|
||||
// copyRichTextMetadata 复制已识别组合的语义,使用目标标准子对象的字体引用
|
||||
// 入参: source 来源编辑器, sourcePage 来源页, ids 来源标识, page 目标页, copied 目标标识
|
||||
// 返回: error 元数据损坏或提交错误
|
||||
func (e *Editor) copyRichTextMetadata(source *Editor, sourcePage int, ids []string, page int, copied []string) error {
|
||||
@@ -582,7 +582,7 @@ func (e *Editor) copyRichTextMetadata(source *Editor, sourcePage int, ids []stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// richTextColors 为自产RGB颜色指定文档内有效颜色空间,不改变默认颜色空间。
|
||||
// richTextColors 为自产RGB颜色指定文档内有效颜色空间,不改变默认颜色空间
|
||||
// 入参: object 排版生成的标准图元
|
||||
// 返回: error 颜色或资源错误
|
||||
func (e *Editor) richTextColors(object *GraphicObject) error {
|
||||
@@ -612,7 +612,7 @@ func (e *Editor) richTextColors(object *GraphicObject) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// copiedRichTextMetadata 检查当前文档对象快照的标引,不将外部同号对象误认成本地对象。
|
||||
// copiedRichTextMetadata 检查当前文档对象快照的标引,不将外部同号对象误认成本地对象
|
||||
// 入参: objects 待复制快照
|
||||
// 返回: map[int]richTextMetadata 需要复制的语义, error 损坏诊断
|
||||
func (e *Editor) copiedRichTextMetadata(objects []GraphicObject) (map[int]richTextMetadata, error) {
|
||||
@@ -668,7 +668,7 @@ func (e *Editor) copiedRichTextMetadata(objects []GraphicObject) (map[int]richTe
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// attachRichTextSnapshot 为普通对象快照附加不可变编辑语义,复制后不依赖原对象继续存在。
|
||||
// attachRichTextSnapshot 为普通对象快照附加不可变编辑语义,复制后不依赖原对象继续存在
|
||||
// 入参: page 页面索引, object 对象快照
|
||||
func (e *Editor) attachRichTextSnapshot(page int, object *GraphicObject) {
|
||||
object.richText = nil
|
||||
@@ -683,7 +683,7 @@ func (e *Editor) attachRichTextSnapshot(page int, object *GraphicObject) {
|
||||
object.richText = &meta
|
||||
}
|
||||
|
||||
// richTextTagsFontSafe 仅允许完整识别且不含资源编号的本库标引通过字体重绑定检查。
|
||||
// richTextTagsFontSafe 仅允许完整识别且不含资源编号的本库标引通过字体重绑定检查
|
||||
// 入参: ctx 取消上下文, reader 包阅读器, index 文档索引
|
||||
// 返回: bool 是否可以排除字体引用, error 读取或取消错误
|
||||
func richTextTagsFontSafe(ctx context.Context, reader *Reader, index int) (bool, error) {
|
||||
|
||||
+25
-25
@@ -21,15 +21,15 @@ import (
|
||||
"path"
|
||||
)
|
||||
|
||||
// TemplateReference 普通页面的模板引用,保留原始模板标识及绑定层次。
|
||||
// PageIndex为当前文档的零基页面索引,PageID为页面标识;ZOrder为空表示沿用模板默认层次。
|
||||
// TemplateReference 普通页面的模板引用,保留原始模板标识及绑定层次
|
||||
// PageIndex为当前文档的零基页面索引,PageID为页面标识;ZOrder为空表示沿用模板默认层次
|
||||
type TemplateReference struct {
|
||||
PageIndex int
|
||||
PageID string
|
||||
Template
|
||||
}
|
||||
|
||||
// Templates 返回当前文档及所选版本的模板定义副本,省略的ZOrder返回Background。
|
||||
// Templates 返回当前文档及所选版本的模板定义副本,省略的ZOrder返回Background
|
||||
// 返回: []TemplatePage 独立模板清单, error 文档读取错误
|
||||
func (e *Editor) Templates() ([]TemplatePage, error) {
|
||||
if e.source == nil {
|
||||
@@ -42,7 +42,7 @@ func (e *Editor) Templates() ([]TemplatePage, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Templates 直接返回当前文档及版本的模板清单,不创建编辑器,省略的ZOrder返回Background。
|
||||
// Templates 直接返回当前文档及版本的模板清单,不创建编辑器,省略的ZOrder返回Background
|
||||
// 返回: []TemplatePage 独立模板定义副本, error 文档读取错误
|
||||
func (r *Reader) Templates() ([]TemplatePage, error) {
|
||||
doc, err := r.Doc()
|
||||
@@ -56,7 +56,7 @@ func (r *Reader) Templates() ([]TemplatePage, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PageTemplates 按文件顺序返回普通页面模板引用,ZOrder为空表示沿用模板默认层次。
|
||||
// PageTemplates 按文件顺序返回普通页面模板引用,ZOrder为空表示沿用模板默认层次
|
||||
// 入参: page 零基页面索引
|
||||
// 返回: []Template 独立引用清单, error 页面读取错误
|
||||
func (e *Editor) PageTemplates(page int) ([]Template, error) {
|
||||
@@ -67,8 +67,8 @@ func (e *Editor) PageTemplates(page int) ([]Template, error) {
|
||||
return append([]Template{}, p.Template...), nil
|
||||
}
|
||||
|
||||
// TemplateReferences 返回当前文档及所选版本全部普通页面的模板引用副本,不包含模板页自身引用。
|
||||
// 清单按页面索引及页内引用文件顺序排列,无引用时返回空切片,读取失败时不返回部分结果。
|
||||
// TemplateReferences 返回当前文档及所选版本全部普通页面的模板引用副本,不包含模板页自身引用
|
||||
// 清单按页面索引及页内引用文件顺序排列,无引用时返回空切片,读取失败时不返回部分结果
|
||||
// 返回: []TemplateReference 引用定位及绑定层次清单, error 页面读取错误
|
||||
func (e *Editor) TemplateReferences() ([]TemplateReference, error) {
|
||||
result := []TemplateReference{}
|
||||
@@ -84,8 +84,8 @@ func (e *Editor) TemplateReferences() ([]TemplateReference, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TemplateReferences 只读枚举当前文档及所选版本全部普通页面的模板引用,不创建编辑器或遍历其他文档、版本。
|
||||
// 返回独立副本,按页面索引及页内引用文件顺序排列;无引用时返回空切片,读取失败时不返回部分结果。
|
||||
// TemplateReferences 只读枚举当前文档及所选版本全部普通页面的模板引用,不创建编辑器或遍历其他文档、版本
|
||||
// 返回独立副本,按页面索引及页内引用文件顺序排列;无引用时返回空切片,读取失败时不返回部分结果
|
||||
// 返回: []TemplateReference 引用定位及绑定层次清单, error 文档或页面读取错误
|
||||
func (r *Reader) TemplateReferences() ([]TemplateReference, error) {
|
||||
doc, err := r.Doc()
|
||||
@@ -105,7 +105,7 @@ func (r *Reader) TemplateReferences() ([]TemplateReference, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// BindTemplate 在页面末尾绑定模板,空ZOrder沿用模板默认层次,重复绑定拒绝。
|
||||
// BindTemplate 在页面末尾绑定模板,空ZOrder沿用模板默认层次,重复绑定拒绝
|
||||
// 入参: page 页面索引, id 模板标识, zOrder 页面覆盖层次
|
||||
// 返回: error 标识、层次或重复绑定错误
|
||||
func (e *Editor) BindTemplate(page int, id, zOrder string) error {
|
||||
@@ -139,7 +139,7 @@ func (e *Editor) BindTemplate(page int, id, zOrder string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// UnbindTemplate 解除当前页面的唯一指定模板引用,保留模板定义与其他页面绑定。
|
||||
// UnbindTemplate 解除当前页面的唯一指定模板引用,保留模板定义与其他页面绑定
|
||||
// 入参: page 页面索引, id 模板标识
|
||||
// 返回: error 引用或结构错误
|
||||
func (e *Editor) UnbindTemplate(page int, id string) error {
|
||||
@@ -152,7 +152,7 @@ func (e *Editor) UnbindTemplate(page int, id string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// SetPageTemplateZOrder 设置页面模板的前景或背景层次,空值删除覆盖以沿用模板默认值。
|
||||
// SetPageTemplateZOrder 设置页面模板的前景或背景层次,空值删除覆盖以沿用模板默认值
|
||||
// 入参: page 页面索引, id 模板标识, zOrder 页面覆盖层次
|
||||
// 返回: error 引用、层次或结构错误
|
||||
func (e *Editor) SetPageTemplateZOrder(page int, id, zOrder string) error {
|
||||
@@ -171,7 +171,7 @@ func (e *Editor) SetPageTemplateZOrder(page int, id, zOrder string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// MovePageTemplate 调整引用在页面全部模板引用中的零基文件顺序,同层次引用按此顺序绘制。
|
||||
// MovePageTemplate 调整引用在页面全部模板引用中的零基文件顺序,同层次引用按此顺序绘制
|
||||
// 入参: page 页面索引, id 模板标识, order 全部引用中的目标顺序
|
||||
// 返回: error 引用、顺序或结构错误
|
||||
func (e *Editor) MovePageTemplate(page int, id string, order int) error {
|
||||
@@ -184,9 +184,9 @@ func (e *Editor) MovePageTemplate(page int, id string, order int) error {
|
||||
})
|
||||
}
|
||||
|
||||
// CreateTemplateFromPage 复制指定普通页面自身内容为模板,不改变源页或自动绑定,空层次采用Background。
|
||||
// 复用页面复制的标识重映射并保留原XML及资源;模板引用不递归复制,注解和签章不属于模板内容。
|
||||
// 模板编辑可使用普通页面副本及现有编辑接口,再通过ReplaceTemplateFromPage保持模板身份提交。
|
||||
// CreateTemplateFromPage 复制指定普通页面自身内容为模板,不改变源页或自动绑定,空层次采用Background
|
||||
// 复用页面复制的标识重映射并保留原XML及资源;模板引用不递归复制,注解和签章不属于模板内容
|
||||
// 模板编辑可使用普通页面副本及现有编辑接口,再通过ReplaceTemplateFromPage保持模板身份提交
|
||||
// 入参: page 源页面索引, name 模板名称, zOrder 默认前景背景层次
|
||||
// 返回: string 新模板标识, error 页面、标识重映射或提交错误
|
||||
func (e *Editor) CreateTemplateFromPage(page int, name, zOrder string) (string, error) {
|
||||
@@ -300,7 +300,7 @@ func (e *Editor) CreateTemplateFromPage(page int, name, zOrder string) (string,
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// RenameTemplate 修改当前文档模板名称,空字符串清空名称,不修改共享模板内容。
|
||||
// RenameTemplate 修改当前文档模板名称,空字符串清空名称,不修改共享模板内容
|
||||
// 入参: id 模板标识, name 模板名称
|
||||
// 返回: error 标识或结构错误
|
||||
func (e *Editor) RenameTemplate(id, name string) error {
|
||||
@@ -312,7 +312,7 @@ func (e *Editor) RenameTemplate(id, name string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// SetTemplateZOrder 设置模板默认前景背景层次,不修改页面显式覆盖,空值采用Background。
|
||||
// SetTemplateZOrder 设置模板默认前景背景层次,不修改页面显式覆盖,空值采用Background
|
||||
// 入参: id 模板标识, zOrder 默认前景背景层次
|
||||
// 返回: error 标识、层次或结构错误
|
||||
func (e *Editor) SetTemplateZOrder(id, zOrder string) error {
|
||||
@@ -328,8 +328,8 @@ func (e *Editor) SetTemplateZOrder(id, zOrder string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// ReplaceTemplateFromPage 复制普通页自身内容替换模板内容,保持模板标识、名称、层次及全部绑定。
|
||||
// 源页及其他文档和历史版本保持不变,整个替换仅产生一条撤销记录。
|
||||
// ReplaceTemplateFromPage 复制普通页自身内容替换模板内容,保持模板标识、名称、层次及全部绑定
|
||||
// 源页及其他文档和历史版本保持不变,整个替换仅产生一条撤销记录
|
||||
// 入参: id 目标模板标识, page 普通源页面索引
|
||||
// 返回: error 标识、复制或提交错误
|
||||
func (e *Editor) ReplaceTemplateFromPage(id string, page int) error {
|
||||
@@ -382,7 +382,7 @@ func (e *Editor) ReplaceTemplateFromPage(id string, page int) error {
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTemplate 删除当前文档未被普通页面引用的模板定义,保留可能被其他文档、版本或扩展引用的文件。
|
||||
// DeleteTemplate 删除当前文档未被普通页面引用的模板定义,保留可能被其他文档、版本或扩展引用的文件
|
||||
// 入参: id 模板标识
|
||||
// 返回: error 标识、被引用或结构错误
|
||||
func (e *Editor) DeleteTemplate(id string) error {
|
||||
@@ -406,7 +406,7 @@ func (e *Editor) DeleteTemplate(id string) error {
|
||||
})
|
||||
}
|
||||
|
||||
// editorTemplateOrder 将省略的模板默认层次规范化为Background。
|
||||
// editorTemplateOrder 将省略的模板默认层次规范化为Background
|
||||
// 入参: value 原始层次
|
||||
// 返回: string 有效默认层次
|
||||
func editorTemplateOrder(value string) string {
|
||||
@@ -416,14 +416,14 @@ func editorTemplateOrder(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// validEditorTemplateOrder 校验模板前景背景枚举及未指定状态。
|
||||
// validEditorTemplateOrder 校验模板前景背景枚举及未指定状态
|
||||
// 入参: value 层次值
|
||||
// 返回: bool 是否有效
|
||||
func validEditorTemplateOrder(value string) bool {
|
||||
return value == "" || value == "Background" || value == "Foreground"
|
||||
}
|
||||
|
||||
// editorTemplateDefinition 查找当前阅读器中的唯一模板定义,拒绝缺失及歧义标识。
|
||||
// editorTemplateDefinition 查找当前阅读器中的唯一模板定义,拒绝缺失及歧义标识
|
||||
// 入参: reader 当前阅读器, id 模板标识
|
||||
// 返回: TemplatePage 定义副本, error 文档或标识错误
|
||||
func editorTemplateDefinition(reader *Reader, id string) (TemplatePage, error) {
|
||||
@@ -446,7 +446,7 @@ func editorTemplateDefinition(reader *Reader, id string) (TemplatePage, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// editTemplateDefinition 在事务快照中局部修改模板声明,保留命名空间、扩展及其他版本。
|
||||
// editTemplateDefinition 在事务快照中局部修改模板声明,保留命名空间、扩展及其他版本
|
||||
// 入参: id 模板标识, edit 文档原文修改回调
|
||||
// 返回: error 读取、修改或提交错误
|
||||
func (e *Editor) editTemplateDefinition(id string, edit func(*Reader, []byte, *editorXML) ([]byte, error)) error {
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ func (r *Reader) InvoicesContext(ctx context.Context) (InvoiceReport, error) {
|
||||
report.Issues = append(report.Issues, InvoiceIssue{Code: "read-error", Message: err.Error(), Source: InvoiceSource{Kind: "tag"}})
|
||||
}
|
||||
for i, tag := range tags {
|
||||
if tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace {
|
||||
if tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace || tag.TypeID == invoiceTagType && tag.NameSpace == invoiceNamespace {
|
||||
continue
|
||||
}
|
||||
if err := read(InvoiceSource{Kind: "tag", ID: strconv.Itoa(i), FileLoc: tag.FileLoc}); err != nil {
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InvoiceLayout 指定发票信息页的字体资源及毫米尺寸,不代表税务机关的固定票样
|
||||
// Font必填;其余零值依次使用4毫米字号、210毫米宽、297毫米高、15毫米页边距
|
||||
type InvoiceLayout struct {
|
||||
Font string `json:"font"`
|
||||
FontSize float64 `json:"fontSize"`
|
||||
PageWidth float64 `json:"pageWidth"`
|
||||
PageHeight float64 `json:"pageHeight"`
|
||||
Margin float64 `json:"margin"`
|
||||
}
|
||||
|
||||
// InvoiceBinding 将一个完整字段绑定至当前文档的文字对象,页码及明细索引从0开始
|
||||
// Field使用number、buyer.name、items.name等规范字段名,非明细字段的Item必须为-1
|
||||
// 原始事实使用facts.N或items.fields.N,N为原始事实索引,不根据名称合并不同上下文
|
||||
type InvoiceBinding struct {
|
||||
Field string `json:"field"`
|
||||
Item int `json:"item"`
|
||||
Page int `json:"page"`
|
||||
ObjectID string `json:"objectId"`
|
||||
}
|
||||
|
||||
// invoiceLayoutField 保存票面字段及独立标签
|
||||
type invoiceLayoutField struct {
|
||||
field, label, value string
|
||||
item int
|
||||
}
|
||||
|
||||
// invoiceAppearanceMatches 比较票面Unicode字符序列,仅忽略回车与换行,保留全部空格
|
||||
// 不校验显式换行、软折行、位置或可见性,字形索引文本无法确定原文时返回false
|
||||
// 入参: text 票面文字对象, value 结构化字段原文
|
||||
// 返回: bool 忽略回车与换行后的字符序列是否一致
|
||||
func invoiceAppearanceMatches(text TextObject, value string) bool {
|
||||
var content strings.Builder
|
||||
for _, code := range text.TextCode {
|
||||
if code.Index != "" {
|
||||
return false
|
||||
}
|
||||
content.WriteString(string(textCodeRunes(code.Value)))
|
||||
}
|
||||
actual := strings.ReplaceAll(strings.ReplaceAll(content.String(), "\r", ""), "\n", "")
|
||||
expected := strings.ReplaceAll(strings.ReplaceAll(value, "\r", ""), "\n", "")
|
||||
return actual == expected
|
||||
}
|
||||
|
||||
// invoiceLayoutFields 按稳定顺序展开公共字段及明细,不推算缺失值
|
||||
// 入参: invoice 发票数据
|
||||
// 返回: []invoiceLayoutField 字段列表
|
||||
func invoiceLayoutFields(invoice Invoice) []invoiceLayoutField {
|
||||
fields := []invoiceLayoutField{
|
||||
{"kind", "票种", invoice.Kind, -1}, {"standard", "规范", invoice.Standard, -1},
|
||||
{"version", "版本", invoice.Version, -1}, {"number", "发票号码", invoice.Number, -1},
|
||||
{"code", "发票代码", invoice.Code, -1}, {"date", "开票日期", invoice.Date, -1},
|
||||
{"currency", "币种", invoice.Currency, -1}, {"status", "状态", invoice.Status, -1},
|
||||
}
|
||||
for _, party := range []struct {
|
||||
prefix, label string
|
||||
value InvoiceParty
|
||||
}{{"buyer", "购买方", invoice.Buyer}, {"seller", "销售方", invoice.Seller}} {
|
||||
for _, field := range []invoiceLayoutField{{"name", "名称", party.value.Name, -1}, {"taxId", "税号", party.value.TaxID, -1}, {"address", "地址", party.value.Address, -1}, {"phone", "电话", party.value.Phone, -1}, {"bank", "开户行", party.value.Bank, -1}, {"account", "账号", party.value.Account, -1}} {
|
||||
field.field, field.label = party.prefix+"."+field.field, party.label+field.label
|
||||
fields = append(fields, field)
|
||||
}
|
||||
}
|
||||
for i, item := range invoice.Items {
|
||||
for _, field := range []invoiceLayoutField{{"name", "名称", item.Name, i}, {"specification", "规格", item.Specification, i}, {"unit", "单位", item.Unit, i}, {"quantity", "数量", item.Quantity, i}, {"unitPrice", "单价", item.UnitPrice, i}, {"amount", "金额", item.Amount, i}, {"taxRate", "税率", item.TaxRate, i}, {"taxAmount", "税额", item.TaxAmount, i}} {
|
||||
field.field, field.label = "items."+field.field, fmt.Sprintf("明细%d %s", i+1, field.label)
|
||||
fields = append(fields, field)
|
||||
}
|
||||
}
|
||||
fields = append(fields, invoiceLayoutField{"amount", "不含税金额", invoice.Amount, -1}, invoiceLayoutField{"taxAmount", "税额", invoice.TaxAmount, -1}, invoiceLayoutField{"totalAmount", "价税合计", invoice.TotalAmount, -1}, invoiceLayoutField{"remarks", "备注", invoice.Remarks, -1})
|
||||
for i, fact := range invoice.Facts {
|
||||
if invoiceLayoutFactVisible(invoice, fact) {
|
||||
fields = append(fields, invoiceLayoutField{fmt.Sprintf("facts.%d", i), fact.Name, fact.Value, -1})
|
||||
}
|
||||
}
|
||||
for i, item := range invoice.Items {
|
||||
for j, fact := range item.Fields {
|
||||
duplicate := false
|
||||
for _, existing := range invoice.Facts {
|
||||
if reflect.DeepEqual(existing, fact) {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !duplicate && invoiceLayoutFactVisible(invoice, fact) {
|
||||
fields = append(fields, invoiceLayoutField{fmt.Sprintf("items.fields.%d", j), fmt.Sprintf("明细%d %s", i+1, fact.Name), fact.Value, i})
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// invoiceLayoutFactVisible 排除上下文、单位及已明确映射的相同字段值
|
||||
// 入参: invoice 发票数据, fact 原始业务事实
|
||||
// 返回: bool 是否需要单独显示
|
||||
func invoiceLayoutFactVisible(invoice Invoice, fact InvoiceFact) bool {
|
||||
if fact.Nil || fact.Value == "" || fact.Namespace == invoiceXBRL && (fact.Name == "context" || fact.Name == "unit") {
|
||||
return false
|
||||
}
|
||||
kind, _ := invoiceTaxonomy(fact.Namespace)
|
||||
if kind != "" && kind == invoice.Kind {
|
||||
if field := invoiceXMLField(&invoice, kind, fact.Name); field != nil && *field == fact.Value {
|
||||
return false
|
||||
}
|
||||
for _, item := range invoice.Items {
|
||||
if field := invoiceItemField(&item, kind, fact.Name); field != nil && *field == fact.Value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if fact.Namespace == "" {
|
||||
var path strings.Builder
|
||||
valid := true
|
||||
for _, segment := range strings.Split(strings.TrimPrefix(fact.Source.NodePath, "/"), "/") {
|
||||
name, index, ok := strings.Cut(segment, "[")
|
||||
if !ok || !strings.HasPrefix(name, "{}") || !strings.HasSuffix(index, "]") {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
path.WriteByte('/')
|
||||
path.WriteString(strings.TrimPrefix(name, "{}"))
|
||||
}
|
||||
if valid {
|
||||
if path.String() == "/EInvoice/Header/Version" && fact.Value == invoice.Version {
|
||||
return false
|
||||
}
|
||||
if field, _ := invoicePlainField(&invoice, nil, path.String()); field != nil && *field == fact.Value {
|
||||
return false
|
||||
}
|
||||
for _, item := range invoice.Items {
|
||||
if field, _ := invoicePlainField(&invoice, &item, path.String()); field != nil && *field == fact.Value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// invoiceBindingValue 读取明确字段与明细索引,不接受未知字段或隐式索引
|
||||
// 入参: invoice 发票数据, binding 字段绑定
|
||||
// 返回: string 字段原文, error 字段或索引错误
|
||||
func invoiceBindingValue(invoice Invoice, binding InvoiceBinding) (string, error) {
|
||||
if strings.HasPrefix(binding.Field, "facts.") && binding.Item == -1 {
|
||||
index, err := strconv.Atoi(strings.TrimPrefix(binding.Field, "facts."))
|
||||
if err == nil && index >= 0 && index < len(invoice.Facts) && binding.Field == fmt.Sprintf("facts.%d", index) {
|
||||
return invoice.Facts[index].Value, nil
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(binding.Field, "items.") {
|
||||
if binding.Item >= 0 && binding.Item < len(invoice.Items) {
|
||||
item := invoice.Items[binding.Item]
|
||||
if strings.HasPrefix(binding.Field, "items.fields.") {
|
||||
index, err := strconv.Atoi(strings.TrimPrefix(binding.Field, "items.fields."))
|
||||
if err == nil && index >= 0 && index < len(item.Fields) && binding.Field == fmt.Sprintf("items.fields.%d", index) {
|
||||
return item.Fields[index].Value, nil
|
||||
}
|
||||
}
|
||||
switch binding.Field {
|
||||
case "items.name":
|
||||
return item.Name, nil
|
||||
case "items.specification":
|
||||
return item.Specification, nil
|
||||
case "items.unit":
|
||||
return item.Unit, nil
|
||||
case "items.quantity":
|
||||
return item.Quantity, nil
|
||||
case "items.unitPrice":
|
||||
return item.UnitPrice, nil
|
||||
case "items.amount":
|
||||
return item.Amount, nil
|
||||
case "items.taxRate":
|
||||
return item.TaxRate, nil
|
||||
case "items.taxAmount":
|
||||
return item.TaxAmount, nil
|
||||
}
|
||||
}
|
||||
} else if binding.Item == -1 {
|
||||
switch binding.Field {
|
||||
case "kind":
|
||||
return invoice.Kind, nil
|
||||
case "standard":
|
||||
return invoice.Standard, nil
|
||||
case "version":
|
||||
return invoice.Version, nil
|
||||
case "number":
|
||||
return invoice.Number, nil
|
||||
case "code":
|
||||
return invoice.Code, nil
|
||||
case "date":
|
||||
return invoice.Date, nil
|
||||
case "currency":
|
||||
return invoice.Currency, nil
|
||||
case "status":
|
||||
return invoice.Status, nil
|
||||
case "amount":
|
||||
return invoice.Amount, nil
|
||||
case "taxAmount":
|
||||
return invoice.TaxAmount, nil
|
||||
case "totalAmount":
|
||||
return invoice.TotalAmount, nil
|
||||
case "remarks":
|
||||
return invoice.Remarks, nil
|
||||
}
|
||||
prefix, name, ok := strings.Cut(binding.Field, ".")
|
||||
if ok && (prefix == "buyer" || prefix == "seller") {
|
||||
party := invoice.Buyer
|
||||
if prefix == "seller" {
|
||||
party = invoice.Seller
|
||||
}
|
||||
switch name {
|
||||
case "name":
|
||||
return party.Name, nil
|
||||
case "taxId":
|
||||
return party.TaxID, nil
|
||||
case "address":
|
||||
return party.Address, nil
|
||||
case "phone":
|
||||
return party.Phone, nil
|
||||
case "bank":
|
||||
return party.Bank, nil
|
||||
case "account":
|
||||
return party.Account, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("invalid invoice binding field %q item %d", binding.Field, binding.Item)
|
||||
}
|
||||
|
||||
// AppendInvoicePages 追加可搜索的发票信息页,字段内折行、字段间分页,不修改已有页面
|
||||
// 每字段对应一个完整文字对象,单字段超过页面容量时失败;绑定持久化由调用方处理
|
||||
// 入参: ctx 取消上下文, invoice 发票数据, options 字体与页面尺寸
|
||||
// 返回: []InvoiceBinding 新增字段绑定, error 排版或字体错误
|
||||
func (e *Editor) AppendInvoicePages(ctx context.Context, invoice Invoice, options InvoiceLayout) ([]InvoiceBinding, error) {
|
||||
if e == nil || ctx == nil {
|
||||
return nil, fmt.Errorf("editor and context are required")
|
||||
}
|
||||
if options.Font == "" {
|
||||
return nil, fmt.Errorf("invoice font resource is required")
|
||||
}
|
||||
if options.FontSize == 0 {
|
||||
options.FontSize = 4
|
||||
}
|
||||
if options.PageWidth == 0 {
|
||||
options.PageWidth = 210
|
||||
}
|
||||
if options.PageHeight == 0 {
|
||||
options.PageHeight = 297
|
||||
}
|
||||
if options.Margin == 0 {
|
||||
options.Margin = 15
|
||||
}
|
||||
for _, value := range []float64{options.FontSize, options.PageWidth, options.PageHeight, options.Margin} {
|
||||
if !finite(value) || value <= 0 {
|
||||
return nil, fmt.Errorf("invoice layout dimensions must be positive and finite")
|
||||
}
|
||||
}
|
||||
width, height := options.PageWidth-2*options.Margin, options.PageHeight-2*options.Margin
|
||||
if width <= 0 || height <= 0 {
|
||||
return nil, fmt.Errorf("invoice margins leave no page area")
|
||||
}
|
||||
var bindings []InvoiceBinding
|
||||
err := e.Transaction(func(editor *Editor) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
metrics, err := editor.editorFont(options.Font)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
page, y := -1, options.Margin
|
||||
for _, field := range invoiceLayoutFields(invoice) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if field.value == "" {
|
||||
continue
|
||||
}
|
||||
labelWidth := math.Min(width*.4, options.FontSize*8)
|
||||
gap := options.FontSize
|
||||
label := TextObject{Font: options.Font, Size: options.FontSize, Boundary: fmt.Sprintf("0 0 %s %s", ofdNumber(labelWidth), ofdNumber(height))}
|
||||
value := TextObject{Font: options.Font, Size: options.FontSize, Boundary: fmt.Sprintf("0 0 %s %s", ofdNumber(width-labelWidth-gap), ofdNumber(height))}
|
||||
labelHeight, err := invoiceLayoutText(&label, field.label, metrics)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invoice label %s: %w", field.field, err)
|
||||
}
|
||||
valueHeight, err := invoiceLayoutText(&value, field.value, metrics)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invoice field %s: %w", field.field, err)
|
||||
}
|
||||
rowHeight := math.Max(labelHeight, valueHeight)
|
||||
if page < 0 || y+rowHeight > options.PageHeight-options.Margin {
|
||||
page, err = editor.AddPage(options.PageWidth, options.PageHeight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
y = options.Margin
|
||||
}
|
||||
label.Boundary = fmt.Sprintf("%s %s %s %s", ofdNumber(options.Margin), ofdNumber(y), ofdNumber(labelWidth), ofdNumber(rowHeight))
|
||||
value.Boundary = fmt.Sprintf("%s %s %s %s", ofdNumber(options.Margin+labelWidth+gap), ofdNumber(y), ofdNumber(width-labelWidth-gap), ofdNumber(rowHeight))
|
||||
if _, err := editor.AddObject(page, GraphicObject{Type: "TextObject", TextObject: label}); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := editor.AddObject(page, GraphicObject{Type: "TextObject", TextObject: value})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bindings = append(bindings, InvoiceBinding{Field: field.field, Item: field.item, Page: page, ObjectID: id})
|
||||
y += rowHeight + options.FontSize*.5
|
||||
}
|
||||
if len(bindings) == 0 {
|
||||
return fmt.Errorf("invoice has no display fields")
|
||||
}
|
||||
return ctx.Err()
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bindings, nil
|
||||
}
|
||||
|
||||
// SyncInvoicePages 原子更新显式绑定的普通横向文字,不新增、移动或缩小对象
|
||||
// 不接受重复绑定、空文本、缺失对象及超出原框的内容;可嵌入上层事务与结构化数据一起提交
|
||||
// 入参: ctx 取消上下文, invoice 发票数据, bindings 当前文档字段绑定
|
||||
// 返回: error 绑定、字体或容量错误
|
||||
func (e *Editor) SyncInvoicePages(ctx context.Context, invoice Invoice, bindings []InvoiceBinding) error {
|
||||
if e == nil || ctx == nil {
|
||||
return fmt.Errorf("editor and context are required")
|
||||
}
|
||||
return e.Transaction(func(editor *Editor) error {
|
||||
objects, fields := make(map[string]bool), make(map[string]bool)
|
||||
for _, binding := range bindings {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
objectKey := fmt.Sprintf("%d/%s", binding.Page, editorResourceID(binding.ObjectID))
|
||||
fieldKey := fmt.Sprintf("%d/%s", binding.Item, binding.Field)
|
||||
if binding.ObjectID == "" || objects[objectKey] || fields[fieldKey] {
|
||||
return fmt.Errorf("ambiguous invoice binding %q", binding.Field)
|
||||
}
|
||||
objects[objectKey], fields[fieldKey] = true, true
|
||||
value, err := invoiceBindingValue(invoice, binding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
page, err := editor.page(binding.Page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
matches := 0
|
||||
for _, layer := range page.Content.Layer {
|
||||
for _, candidate := range layer.Objects {
|
||||
if sameResourceID(editorObjectID(candidate), binding.ObjectID) {
|
||||
matches++
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches != 1 {
|
||||
return fmt.Errorf("invoice binding %q requires one text object", binding.Field)
|
||||
}
|
||||
object, err := editor.Object(binding.Page, binding.ObjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if object.Type != "TextObject" {
|
||||
return fmt.Errorf("invoice binding %q is not text", binding.Field)
|
||||
}
|
||||
text := &object.TextObject
|
||||
if text.CTM != "" || text.Clips != nil || text.Italic || text.Weight > 400 || text.VScale != 0 || text.Decoration != "" || text.DrawParam != "" || text.Stroke != nil && *text.Stroke || text.HScale != 0 && text.HScale != 1 {
|
||||
return fmt.Errorf("invoice binding %q requires plain untransformed text", binding.Field)
|
||||
}
|
||||
metrics, err := editor.editorFont(text.Font)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := invoiceLayoutText(text, value, metrics); err != nil {
|
||||
return fmt.Errorf("invoice field %s: %w", binding.Field, err)
|
||||
}
|
||||
if err := editor.UpdateObject(binding.Page, binding.ObjectID, object); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
})
|
||||
}
|
||||
|
||||
// invoiceLayoutText 按实际字形及步进检查折行容量,并将负侧承移入原框
|
||||
// 入参: text 普通横向文字, value 字段原文, metrics 字体度量
|
||||
// 返回: float64 内容高度, error 缺字或越界错误
|
||||
func invoiceLayoutText(text *TextObject, value string, metrics FontMetrics) (float64, error) {
|
||||
box, err := text.TextFrame()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if value == "" {
|
||||
return 0, fmt.Errorf("empty invoice field requires removing its binding and text object")
|
||||
}
|
||||
if strings.Trim(value, "\r\n") == "" {
|
||||
return 0, fmt.Errorf("invoice text contains no display characters")
|
||||
}
|
||||
if err := LayoutText(text, value, TextLayout{Wrap: true}, metrics); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
outlines, ok := metrics.(FontOutlines)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invoice layout requires glyph outlines")
|
||||
}
|
||||
unit := text.Size / float64(metrics.UnitsPerEm())
|
||||
_, descender, _ := metrics.VerticalMetrics()
|
||||
left, top, right, bottom := 0.0, 0.0, 0.0, 0.0
|
||||
for _, code := range text.TextCode {
|
||||
x, err := strconv.ParseFloat(code.X, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
y, err := strconv.ParseFloat(code.Y, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
bottom = math.Max(bottom, y+float64(descender)*unit)
|
||||
for _, char := range textCodeRunes(code.Value) {
|
||||
glyph := metrics.GlyphIndex(char)
|
||||
var bounds Box
|
||||
if provider, ok := metrics.(FontGlyphBounds); ok {
|
||||
bounds, err = provider.GlyphBounds(glyph, text.Size)
|
||||
} else {
|
||||
var outline GeometryPath
|
||||
outline, err = outlines.GlyphOutline(glyph, text.Size)
|
||||
if err == nil {
|
||||
bounds, err = outline.Bounds()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
left, top = math.Min(left, x+bounds.X), math.Min(top, y+bounds.Y)
|
||||
right, bottom = math.Max(right, x+bounds.X+bounds.W), math.Max(bottom, y+bounds.Y+bounds.H)
|
||||
x += float64(metrics.GlyphAdvance(glyph)) * unit
|
||||
right = math.Max(right, x)
|
||||
}
|
||||
}
|
||||
if right-left > box.W+1e-6 || bottom-top > box.H+1e-6 {
|
||||
return 0, fmt.Errorf("invoice text exceeds binding frame")
|
||||
}
|
||||
for i := range text.TextCode {
|
||||
x, _ := strconv.ParseFloat(text.TextCode[i].X, 64)
|
||||
y, _ := strconv.ParseFloat(text.TextCode[i].Y, 64)
|
||||
text.TextCode[i].X, text.TextCode[i].Y = ofdNumber(x-left), ofdNumber(y-top)
|
||||
}
|
||||
return math.Ceil((bottom-top)*1e6) / 1e6, nil
|
||||
}
|
||||
|
||||
// invoiceCheckReflowReferences 检查当前文档快照中指向待重排页的标准导航及已识别标引
|
||||
// 排除本次发票自身的标引,不推测未知扩展中的引用,也不读取其他文档或历史入口
|
||||
// 入参: ctx 取消上下文, e 当前编辑器, ownedPageIDs 待替换页面标识, ownStructureIndex 自有标引索引,-1表示无
|
||||
// 返回: error 引用冲突、结构读取或取消错误
|
||||
func invoiceCheckReflowReferences(ctx context.Context, e *Editor, ownedPageIDs []string, ownStructureIndex int) error {
|
||||
if ctx == nil || e == nil {
|
||||
return fmt.Errorf("editor and context are required")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
owned := make(map[string]bool, len(ownedPageIDs))
|
||||
for _, id := range ownedPageIDs {
|
||||
key := editorResourceID(id)
|
||||
if key == "" {
|
||||
return fmt.Errorf("invalid generated invoice page identifier")
|
||||
}
|
||||
owned[key] = true
|
||||
}
|
||||
reader, err := e.ReaderContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
structures, err := reader.StructureContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ownStructureIndex < -1 || ownStructureIndex >= len(structures) {
|
||||
return fmt.Errorf("invoice structure index out of range")
|
||||
}
|
||||
var checkStructure func(*StructureNode) error
|
||||
checkStructure = func(node *StructureNode) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
for _, reference := range node.References {
|
||||
if owned[editorResourceID(reference.PageID)] {
|
||||
return fmt.Errorf("generated invoice page is referenced by another structure")
|
||||
}
|
||||
}
|
||||
for i := range node.Children {
|
||||
if err := checkStructure(&node.Children[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for index, structure := range structures {
|
||||
if index == ownStructureIndex {
|
||||
continue
|
||||
}
|
||||
if structure.Error != "" {
|
||||
return fmt.Errorf("cannot inspect invoice reflow references: %s", structure.Error)
|
||||
}
|
||||
if err := checkStructure(structure.Root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, files, err := reader.versionDependencies(ctx, reader.documentRoot, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var walk func(*editorXML) error
|
||||
walk = func(node *editorXML) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !node.matchesOFD(node.name.Local) {
|
||||
return nil
|
||||
}
|
||||
if node.matchesOFD("Data") && node.parent != nil && node.parent.matchesOFD("Extension") {
|
||||
return nil
|
||||
}
|
||||
if node.matchesOFD("Dest") && node.parent != nil && (node.parent.matchesOFD("Goto") || node.parent.matchesOFD("Bookmark") || node.parent.matchesOFD("OutlineElem")) && owned[editorResourceID(node.attr("PageID"))] {
|
||||
return fmt.Errorf("generated invoice page is referenced by navigation")
|
||||
}
|
||||
if node.matchesOFD("Extension") && owned[editorResourceID(node.attr("RefId"))] {
|
||||
return fmt.Errorf("generated invoice page is referenced by an extension")
|
||||
}
|
||||
for _, child := range node.children {
|
||||
if err := walk(child); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, file := range slices.Sorted(maps.Keys(files)) {
|
||||
data, err := reader.readFileViewContext(ctx, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err := parseEditorXMLContext(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := walk(root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InvoiceDigest 计算原始导出数据的SHA256摘要
|
||||
// 入参: data 导出数据
|
||||
// 返回: string 小写十六进制摘要
|
||||
func InvoiceDigest(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// ExportInvoice 导出当前文档及版本声明的发票,内联内容补齐继承的命名空间
|
||||
// 相同选择器对应多个声明时拒绝操作,允许已识别的旧数据带解析诊断
|
||||
// 入参: ctx 取消上下文, source 提取结果中的精确来源
|
||||
// 返回: []byte 独立XML数据, error 来源或读取错误
|
||||
func (r *Reader) ExportInvoice(ctx context.Context, source InvoiceSource) ([]byte, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
report, err := r.InvoicesContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
found := false
|
||||
for _, invoice := range report.Invoices {
|
||||
for _, candidate := range invoice.Sources {
|
||||
found = found || candidate == source
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("invoice source is not declared in current document")
|
||||
}
|
||||
_, data, node, _, err := r.invoiceSourceNode(ctx, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if source.NodePath != "" {
|
||||
return editorXMLStandaloneContext(ctx, data[node.start:node.end], node)
|
||||
}
|
||||
payload, err := r.invoiceData(ctx, source.FileLoc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.Clone(payload), nil
|
||||
}
|
||||
|
||||
// ExportInvoice 导出编辑器当前文档及版本声明的发票数据
|
||||
// 入参: ctx 取消上下文, source 提取结果中的精确来源
|
||||
// 返回: []byte 独立XML数据, error 来源或读取错误
|
||||
func (e *Editor) ExportInvoice(ctx context.Context, source InvoiceSource) ([]byte, error) {
|
||||
if e == nil || e.source == nil {
|
||||
return nil, fmt.Errorf("invoice source not found")
|
||||
}
|
||||
return e.source.reader.ExportInvoice(ctx, source)
|
||||
}
|
||||
|
||||
// ReplaceInvoiceData 仅替换选中声明,独立资源写时复制,旧数据允许诊断而新数据必须无诊断
|
||||
// 不删除原签名,修改签名覆盖的内容后需重新验签
|
||||
// 入参: ctx 取消上下文, source 精确来源, expectedDigest 必填原数据摘要, data 新发票XML
|
||||
// 返回: InvoiceSource 提交后的实际来源, error 校验、冲突或提交错误
|
||||
func (e *Editor) ReplaceInvoiceData(ctx context.Context, source InvoiceSource, expectedDigest string, data []byte) (InvoiceSource, error) {
|
||||
return e.changeInvoiceSource(ctx, source, expectedDigest, data, false)
|
||||
}
|
||||
|
||||
// DeleteInvoiceData 仅删除选中来源声明或内联发票子树,不删除资源及其他别名
|
||||
// 不删除原签名,修改签名覆盖的内容后需重新验签
|
||||
// 入参: ctx 取消上下文, source 精确来源, expectedDigest 必填原数据摘要
|
||||
// 返回: error 校验、冲突或提交错误
|
||||
func (e *Editor) DeleteInvoiceData(ctx context.Context, source InvoiceSource, expectedDigest string) error {
|
||||
_, err := e.changeInvoiceSource(ctx, source, expectedDigest, nil, true)
|
||||
return err
|
||||
}
|
||||
|
||||
// changeInvoiceSource 校验原始摘要并原子修改声明,预检隔离后的来源供绑定重定向
|
||||
// 入参: ctx 取消上下文, source 来源, digest 原始摘要, data 替换数据, remove 删除声明
|
||||
// 返回: InvoiceSource 新来源, error 校验或提交错误
|
||||
func (e *Editor) changeInvoiceSource(ctx context.Context, source InvoiceSource, digest string, data []byte, remove bool) (InvoiceSource, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if digest == "" {
|
||||
return InvoiceSource{}, fmt.Errorf("invoice digest is required")
|
||||
}
|
||||
old, err := e.ExportInvoice(ctx, source)
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
if InvoiceDigest(old) != digest {
|
||||
return InvoiceSource{}, fmt.Errorf("invoice source has changed")
|
||||
}
|
||||
if !remove {
|
||||
if len(data) > invoiceXMLLimit {
|
||||
return InvoiceSource{}, fmt.Errorf("invoice data limit exceeded")
|
||||
}
|
||||
if _, err := invoiceWritableReport(ctx, data); err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
if err := invoiceValidateXMLWrite(ctx, old, data); err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
if bytes.Equal(old, data) {
|
||||
return source, ctx.Err()
|
||||
}
|
||||
}
|
||||
base := e.source
|
||||
name, outer, selected, reference, err := base.reader.invoiceSourceNode(ctx, source)
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
parts := make(map[string][]byte)
|
||||
patch := editorXMLPatch{start: selected.start, end: selected.end}
|
||||
next := source
|
||||
if !remove && source.NodePath != "" {
|
||||
root, err := parseEditorXMLContext(ctx, data)
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
patch.data, err = editorXMLStandaloneContext(ctx, data[root.start:root.end], root)
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
} else if !remove {
|
||||
file := e.packageName("Invoices/Invoice.xml")
|
||||
var escaped bytes.Buffer
|
||||
if err := xml.EscapeText(&escaped, []byte("/"+file)); err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
patch = editorXMLContent(outer, reference, escaped.Bytes())
|
||||
parts[file] = bytes.Clone(data)
|
||||
next.FileLoc = file
|
||||
}
|
||||
parts[name] = editorPatchXML(outer, []editorXMLPatch{patch})
|
||||
if !remove && source.Kind == "attachment" {
|
||||
root, err := parseEditorXMLContext(ctx, parts[name])
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
if root.matchesOFD("Document") {
|
||||
root = root.child("Attachments")
|
||||
}
|
||||
for _, child := range root.children {
|
||||
if child.matchesOFD("Attachment") && child.attr("ID") == source.ID {
|
||||
attribute, err := editorXMLAttributePatch(parts[name], child, "Size", ofdNumber(float64(len(data))/1024))
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
parts[name] = editorPatchXML(parts[name], []editorXMLPatch{attribute})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
isolated, err := base.reader.versionChanges(ctx, parts)
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
files := make(map[string][]byte, len(base.reader.files)+len(isolated))
|
||||
for key, value := range base.reader.files {
|
||||
files[key] = value
|
||||
}
|
||||
for key, value := range isolated {
|
||||
files[key] = value
|
||||
}
|
||||
view := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex, selectedVersion: base.reader.selectedVersion}
|
||||
if err := view.initRootContext(ctx); err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
if !remove {
|
||||
report, err := view.InvoicesContext(ctx)
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
found := false
|
||||
for _, invoice := range report.Invoices {
|
||||
for _, candidate := range invoice.Sources {
|
||||
if candidate.Kind == next.Kind && candidate.ID == next.ID && candidate.NodePath == next.NodePath && (next.NodePath != "" || candidate.FileLoc == next.FileLoc) {
|
||||
next, found = candidate, true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return InvoiceSource{}, fmt.Errorf("replacement invoice source is not reachable")
|
||||
}
|
||||
}
|
||||
if remove && source.Kind == "tag" {
|
||||
index, err := strconv.Atoi(source.ID)
|
||||
if err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
updated := *base
|
||||
updated.structureStarts = make(map[int]map[string]int, len(base.structureStarts))
|
||||
for entry, starts := range base.structureStarts {
|
||||
if entry < index {
|
||||
updated.structureStarts[entry] = starts
|
||||
} else if entry > index {
|
||||
updated.structureStarts[entry-1] = starts
|
||||
}
|
||||
}
|
||||
base = &updated
|
||||
}
|
||||
if err := e.commitAnnotationPartsContext(ctx, base, parts); err != nil {
|
||||
return InvoiceSource{}, err
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// invoiceSourceNode 定位唯一声明及其引用节点,拒绝歧义别名,不接受任意包路径
|
||||
// 入参: ctx 取消上下文, source 已提取来源
|
||||
// 返回: string 声明文件, []byte 原文, *editorXML 选中声明, *editorXML 资源引用, error 定位错误
|
||||
func (r *Reader) invoiceSourceNode(ctx context.Context, source InvoiceSource) (string, []byte, *editorXML, *editorXML, error) {
|
||||
doc, err := r.docStructureContext(ctx)
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
container, entry, location := "", "", ""
|
||||
switch source.Kind {
|
||||
case "attachment":
|
||||
container, entry, location = "Attachments", "Attachment", doc.Attachments.Path
|
||||
case "tag":
|
||||
container, entry, location = "CustomTags", "CustomTag", doc.CustomTags.Path
|
||||
case "extension":
|
||||
container, entry, location = "Extensions", "Extension", doc.Extensions.Path
|
||||
default:
|
||||
return "", nil, nil, nil, fmt.Errorf("invalid invoice source kind")
|
||||
}
|
||||
name := r.documentRoot
|
||||
if strings.TrimSpace(location) != "" {
|
||||
name = r.ResPath(location)
|
||||
}
|
||||
data, err := r.invoiceData(ctx, name)
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
root, err := parseEditorXMLContext(ctx, data)
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
if root.matchesOFD("Document") {
|
||||
root = root.child(container)
|
||||
}
|
||||
var selected, reference *editorXML
|
||||
count, index, declarations := 0, 0, 0
|
||||
if root != nil && root.matchesOFD(container) {
|
||||
for _, child := range root.children {
|
||||
if !child.matchesOFD(entry) {
|
||||
continue
|
||||
}
|
||||
id := strconv.Itoa(index)
|
||||
index++
|
||||
if source.Kind == "attachment" {
|
||||
id = child.attr("ID")
|
||||
}
|
||||
if id != source.ID {
|
||||
continue
|
||||
}
|
||||
declarations++
|
||||
dataIndex := 0
|
||||
for _, node := range child.children {
|
||||
if source.NodePath != "" && source.Kind == "extension" && node.matchesOFD("Data") {
|
||||
dataIndex++
|
||||
for k, payload := range node.children {
|
||||
if source.NodePath == fmt.Sprintf("/Extensions/Extension[%d]/Data[%d]/*[%d]", index, dataIndex, k+1) && source.FileLoc == name {
|
||||
selected, count = payload, count+1
|
||||
}
|
||||
}
|
||||
} else if source.NodePath == "" && (source.Kind == "extension" && node.matchesOFD("ExtendData") || source.Kind != "extension" && node.matchesOFD("FileLoc")) {
|
||||
resolved := resolveResourcePath(name, "", strings.TrimSpace(editorImportText(data, node)))
|
||||
if resolved == source.FileLoc {
|
||||
selected, reference, count = child, node, count+1
|
||||
if source.Kind == "extension" {
|
||||
selected = node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if count != 1 || declarations != 1 {
|
||||
return "", nil, nil, nil, fmt.Errorf("invoice source declaration is missing or ambiguous")
|
||||
}
|
||||
return name, data, selected, reference, ctx.Err()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+83
-83
@@ -28,16 +28,16 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// invoiceXBRL 表示XBRL实例命名空间。
|
||||
// invoiceXBRL 表示XBRL实例命名空间
|
||||
const invoiceXBRL = "http://www.xbrl.org/2003/instance"
|
||||
|
||||
// invoiceXSI 表示XML Schema实例命名空间。
|
||||
// invoiceXSI 表示XML Schema实例命名空间
|
||||
const invoiceXSI = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
// invoiceDecimalPattern 仅接受XML十进制词法。
|
||||
// invoiceDecimalPattern 仅接受XML十进制词法
|
||||
var invoiceDecimalPattern = regexp.MustCompile(`^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$`)
|
||||
|
||||
// invoiceXMLNode 保存有界XML节点及原始文本切片。
|
||||
// invoiceXMLNode 保存有界XML节点及原始文本切片
|
||||
type invoiceXMLNode struct {
|
||||
name xml.Name
|
||||
attrs []xml.Attr
|
||||
@@ -52,23 +52,23 @@ type invoiceXMLNode struct {
|
||||
parts []invoiceXMLPart
|
||||
}
|
||||
|
||||
// invoiceXMLPart 保留混合内容顺序,供类型化维度规范化使用。
|
||||
// invoiceXMLPart 保留混合内容顺序,供类型化维度规范化使用
|
||||
type invoiceXMLPart struct {
|
||||
text string
|
||||
child *invoiceXMLNode
|
||||
}
|
||||
|
||||
// invoiceDecimalEqual 精确比较十进制词法值,不接受指数或分数。
|
||||
// 入参: a、b 原始十进制文本。
|
||||
// 返回: bool 两个合法值是否相等。
|
||||
// invoiceDecimalEqual 精确比较十进制词法值,不接受指数或分数
|
||||
// 入参: a、b 原始十进制文本
|
||||
// 返回: bool 两个合法值是否相等
|
||||
func invoiceDecimalEqual(a, b string) bool {
|
||||
x, y := invoiceDecimal(a), invoiceDecimal(b)
|
||||
return x != nil && y != nil && x.Cmp(y) == 0
|
||||
}
|
||||
|
||||
// invoiceDecimal 解析有界十进制文本。
|
||||
// 入参: s 原始文本。
|
||||
// 返回: *big.Rat 精确数值,非法时为空。
|
||||
// invoiceDecimal 解析有界十进制文本
|
||||
// 入参: s 原始文本
|
||||
// 返回: *big.Rat 精确数值,非法时为空
|
||||
func invoiceDecimal(s string) *big.Rat {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) > 256 || !invoiceDecimalPattern.MatchString(s) {
|
||||
@@ -81,9 +81,9 @@ func invoiceDecimal(s string) *big.Rat {
|
||||
return r
|
||||
}
|
||||
|
||||
// invoiceXMLAttr 按命名空间读取属性。
|
||||
// 入参: n 节点,space 命名空间,name 属性名。
|
||||
// 返回: string 属性值。
|
||||
// invoiceXMLAttr 按命名空间读取属性
|
||||
// 入参: n 节点,space 命名空间,name 属性名
|
||||
// 返回: string 属性值
|
||||
func invoiceXMLAttr(n *invoiceXMLNode, space, name string) string {
|
||||
for _, a := range n.attrs {
|
||||
if a.Name.Space == space && a.Name.Local == name {
|
||||
@@ -93,9 +93,9 @@ func invoiceXMLAttr(n *invoiceXMLNode, space, name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// invoiceXMLHasAttr 区分缺省属性与空字符串属性。
|
||||
// 入参: n 节点,space 命名空间,name 属性名。
|
||||
// 返回: 属性是否存在。
|
||||
// invoiceXMLHasAttr 区分缺省属性与空字符串属性
|
||||
// 入参: n 节点,space 命名空间,name 属性名
|
||||
// 返回: 属性是否存在
|
||||
func invoiceXMLHasAttr(n *invoiceXMLNode, space, name string) bool {
|
||||
for _, a := range n.attrs {
|
||||
if a.Name == (xml.Name{Space: space, Local: name}) {
|
||||
@@ -105,9 +105,9 @@ func invoiceXMLHasAttr(n *invoiceXMLNode, space, name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// invoiceXMLTree 读取受限XML树,不解析外部实体。
|
||||
// 入参: ctx 取消上下文,data 文档字节。
|
||||
// 返回: 根节点及解析错误。
|
||||
// invoiceXMLTree 读取受限XML树,不解析外部实体
|
||||
// 入参: ctx 取消上下文,data 文档字节
|
||||
// 返回: 根节点及解析错误
|
||||
func invoiceXMLTree(ctx context.Context, data []byte) (*invoiceXMLNode, error) {
|
||||
if len(data) > 16<<20 {
|
||||
return nil, fmt.Errorf("invoice XML exceeds size limit")
|
||||
@@ -215,9 +215,9 @@ func invoiceXMLTree(ctx context.Context, data []byte) (*invoiceXMLNode, error) {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// invoiceTaxonomy 识别已核实的官方命名空间。
|
||||
// 入参: ns 命名空间URI。
|
||||
// 返回: 票种及版本,未知时为空。
|
||||
// invoiceTaxonomy 识别已核实的官方命名空间
|
||||
// 入参: ns 命名空间URI
|
||||
// 返回: 票种及版本,未知时为空
|
||||
func invoiceTaxonomy(ns string) (string, string) {
|
||||
switch ns {
|
||||
case "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/inv":
|
||||
@@ -234,9 +234,9 @@ func invoiceTaxonomy(ns string) (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// parseInvoiceXML 解析明确识别的发票格式,未知格式仅产生诊断。
|
||||
// 入参: ctx 取消上下文,data XML数据,source 来源。
|
||||
// 返回: InvoiceReport 独立候选与诊断,error XML或限额错误。
|
||||
// parseInvoiceXML 解析明确识别的发票格式,未知格式仅产生诊断
|
||||
// 入参: ctx 取消上下文,data XML数据,source 来源
|
||||
// 返回: InvoiceReport 独立候选与诊断,error XML或限额错误
|
||||
func parseInvoiceXML(ctx context.Context, data []byte, source InvoiceSource) (InvoiceReport, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
@@ -498,9 +498,9 @@ func parseInvoiceXML(ctx context.Context, data []byte, source InvoiceSource) (In
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// invoiceXMLField 映射官方分类标准明确规定的概念。
|
||||
// 入参: inv 发票,kind 标准类别,name 概念名。
|
||||
// 返回: 对应字段地址,未映射时为空。
|
||||
// invoiceXMLField 映射官方分类标准明确规定的概念
|
||||
// 入参: inv 发票,kind 标准类别,name 概念名
|
||||
// 返回: 对应字段地址,未映射时为空
|
||||
func invoiceXMLField(inv *Invoice, kind, name string) *string {
|
||||
if kind == "inv" {
|
||||
switch name {
|
||||
@@ -588,9 +588,9 @@ func invoiceXMLField(inv *Invoice, kind, name string) *string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// invoiceSortedNodes 为定义生成稳定顺序。
|
||||
// 入参: nodes 定义索引。
|
||||
// 返回: 排序后的标识。
|
||||
// invoiceSortedNodes 为定义生成稳定顺序
|
||||
// 入参: nodes 定义索引
|
||||
// 返回: 排序后的标识
|
||||
func invoiceSortedNodes(nodes map[string]*invoiceXMLNode) []string {
|
||||
ids := make([]string, 0, len(nodes))
|
||||
for id := range nodes {
|
||||
@@ -600,9 +600,9 @@ func invoiceSortedNodes(nodes map[string]*invoiceXMLNode) []string {
|
||||
return ids
|
||||
}
|
||||
|
||||
// invoiceQName 依据节点处的前缀绑定展开QName。
|
||||
// 入参: n 节点,value QName文本。
|
||||
// 返回: 展开名称及有效标志。
|
||||
// invoiceQName 依据节点处的前缀绑定展开QName
|
||||
// 入参: n 节点,value QName文本
|
||||
// 返回: 展开名称及有效标志
|
||||
func invoiceQName(n *invoiceXMLNode, value string) (xml.Name, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
parts := strings.Split(value, ":")
|
||||
@@ -626,9 +626,9 @@ func invoiceQName(n *invoiceXMLNode, value string) (xml.Name, bool) {
|
||||
return xml.Name{Space: ns, Local: local}, true
|
||||
}
|
||||
|
||||
// invoiceNCName 验证XML 1.0不含冒号的名称。
|
||||
// 入参: name 名称文本。
|
||||
// 返回: 是否满足NCName词法。
|
||||
// invoiceNCName 验证XML 1.0不含冒号的名称
|
||||
// 入参: name 名称文本
|
||||
// 返回: 是否满足NCName词法
|
||||
func invoiceNCName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
@@ -642,9 +642,9 @@ func invoiceNCName(name string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// invoicePeriodTime 解析标准期间的日期或日期时间。
|
||||
// 入参: value 日期文本。
|
||||
// 返回: 时刻及有效标志。
|
||||
// invoicePeriodTime 解析标准期间的日期或日期时间
|
||||
// 入参: value 日期文本
|
||||
// 返回: 时刻及有效标志
|
||||
func invoicePeriodTime(value string) (time.Time, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) > 64 {
|
||||
@@ -668,14 +668,14 @@ func invoicePeriodTime(value string) (time.Time, bool) {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// invoiceExpanded 返回不依赖前缀的名称表示。
|
||||
// 入参: name 展开名称。
|
||||
// 返回: Clark名称。
|
||||
// invoiceExpanded 返回不依赖前缀的名称表示
|
||||
// 入参: name 展开名称
|
||||
// 返回: Clark名称
|
||||
func invoiceExpanded(name xml.Name) string { return "{" + name.Space + "}" + name.Local }
|
||||
|
||||
// invoiceCanonicalXML 以展开名称序列化类型化维度,保留混合文本顺序。
|
||||
// 入参: n 类型化维度域元素。
|
||||
// 返回: 不依赖输入元素前缀的XML。
|
||||
// invoiceCanonicalXML 以展开名称序列化类型化维度,保留混合文本顺序
|
||||
// 入参: n 类型化维度域元素
|
||||
// 返回: 不依赖输入元素前缀的XML
|
||||
func invoiceCanonicalXML(n *invoiceXMLNode) string {
|
||||
var b bytes.Buffer
|
||||
e := xml.NewEncoder(&b)
|
||||
@@ -709,9 +709,9 @@ func invoiceCanonicalXML(n *invoiceXMLNode) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// invoiceChildren 按展开名称选择直接子节点。
|
||||
// 入参: n 父节点,space 命名空间,local 本地名。
|
||||
// 返回: 匹配节点。
|
||||
// invoiceChildren 按展开名称选择直接子节点
|
||||
// 入参: n 父节点,space 命名空间,local 本地名
|
||||
// 返回: 匹配节点
|
||||
func invoiceChildren(n *invoiceXMLNode, space, local string) []*invoiceXMLNode {
|
||||
var result []*invoiceXMLNode
|
||||
for _, c := range n.children {
|
||||
@@ -722,9 +722,9 @@ func invoiceChildren(n *invoiceXMLNode, space, local string) []*invoiceXMLNode {
|
||||
return result
|
||||
}
|
||||
|
||||
// invoiceContext 校验主体、期间及维度,不把不同上下文合并。
|
||||
// 入参: n 上下文节点。
|
||||
// 返回: 维度及有效标志。
|
||||
// invoiceContext 校验主体、期间及维度,不把不同上下文合并
|
||||
// 入参: n 上下文节点
|
||||
// 返回: 维度及有效标志
|
||||
func invoiceContext(n *invoiceXMLNode) ([]InvoiceDimension, bool) {
|
||||
entities, periods := invoiceChildren(n, invoiceXBRL, "entity"), invoiceChildren(n, invoiceXBRL, "period")
|
||||
if len(entities) != 1 || len(periods) != 1 {
|
||||
@@ -788,9 +788,9 @@ func invoiceContext(n *invoiceXMLNode) ([]InvoiceDimension, bool) {
|
||||
return dims, valid
|
||||
}
|
||||
|
||||
// invoiceUnit 校验单位结构并展开所有度量QName。
|
||||
// 入参: n 单位定义。
|
||||
// 返回: 规范单位表达及有效标志。
|
||||
// invoiceUnit 校验单位结构并展开所有度量QName
|
||||
// 入参: n 单位定义
|
||||
// 返回: 规范单位表达及有效标志
|
||||
func invoiceUnit(n *invoiceXMLNode) (string, bool) {
|
||||
measures := func(c *invoiceXMLNode) (string, bool) {
|
||||
if len(c.children) == 0 {
|
||||
@@ -823,9 +823,9 @@ func invoiceUnit(n *invoiceXMLNode) (string, bool) {
|
||||
return measures(n)
|
||||
}
|
||||
|
||||
// invoiceNumeric 判断已核实标准中的十进制概念。
|
||||
// 入参: kind 标准类别,name 概念名称。
|
||||
// 返回: 是否为数值概念。
|
||||
// invoiceNumeric 判断已核实标准中的十进制概念
|
||||
// 入参: kind 标准类别,name 概念名称
|
||||
// 返回: 是否为数值概念
|
||||
func invoiceNumeric(kind, name string) bool {
|
||||
if name == "RecordedAmount" {
|
||||
return true
|
||||
@@ -860,9 +860,9 @@ func invoiceNumeric(kind, name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// invoiceAccuracy 校验XBRL精度属性,precision允许零。
|
||||
// 入参: value 原文,nonnegative 是否要求非负整数。
|
||||
// 返回: 是否为整数或INF。
|
||||
// invoiceAccuracy 校验XBRL精度属性,precision允许零
|
||||
// 入参: value 原文,nonnegative 是否要求非负整数
|
||||
// 返回: 是否为整数或INF
|
||||
func invoiceAccuracy(value string, nonnegative bool) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "INF" {
|
||||
@@ -888,16 +888,16 @@ func invoiceAccuracy(value string, nonnegative bool) bool {
|
||||
return !nonnegative || !negative || !nonzero
|
||||
}
|
||||
|
||||
// invoiceMonetary 区分货币事实与数量、百分率。
|
||||
// 入参: kind 类别,name 概念名称。
|
||||
// 返回: 是否为已映射货币概念。
|
||||
// invoiceMonetary 区分货币事实与数量、百分率
|
||||
// 入参: kind 类别,name 概念名称
|
||||
// 返回: 是否为已映射货币概念
|
||||
func invoiceMonetary(kind, name string) bool {
|
||||
return invoiceNumeric(kind, name) && name != "QuantityOfGoodsOrServices" && name != "VatRate" && name != "TaxRate"
|
||||
}
|
||||
|
||||
// invoiceCurrencyUnit 检查单一ISO币种度量,不猜测币种。
|
||||
// 入参: unit 展开的单位表示。
|
||||
// 返回: 是否为三位大写币种度量。
|
||||
// invoiceCurrencyUnit 检查单一ISO币种度量,不猜测币种
|
||||
// 入参: unit 展开的单位表示
|
||||
// 返回: 是否为三位大写币种度量
|
||||
func invoiceCurrencyUnit(unit string) bool {
|
||||
const prefix = "{http://www.xbrl.org/2003/iso4217}"
|
||||
if !strings.HasPrefix(unit, prefix) {
|
||||
@@ -907,16 +907,16 @@ func invoiceCurrencyUnit(unit string) bool {
|
||||
return len(code) == 3 && code[0] >= 'A' && code[0] <= 'Z' && code[1] >= 'A' && code[1] <= 'Z' && code[2] >= 'A' && code[2] <= 'Z'
|
||||
}
|
||||
|
||||
// invoiceItemTuple 识别明细元组,不把会计分录当作商品。
|
||||
// 入参: kind 标准类别,name 元组名。
|
||||
// 返回: 是否为明细。
|
||||
// invoiceItemTuple 识别明细元组,不把会计分录当作商品
|
||||
// 入参: kind 标准类别,name 元组名
|
||||
// 返回: 是否为明细
|
||||
func invoiceItemTuple(kind, name string) bool {
|
||||
return kind == "atr" && name == "DetailInformationOfAirTicketTuple" || kind == "inv" && (name == "InformationOfTaxableGoodsOrServicesDetailItemsTuple" || name == "InformationOfTollFeeDetailItemsTuple")
|
||||
}
|
||||
|
||||
// invoiceItemField 映射增值税商品明细公共字段。
|
||||
// 入参: item 明细,kind 类别,name 概念名。
|
||||
// 返回: 字段地址,其他明细仅保留事实。
|
||||
// invoiceItemField 映射增值税商品明细公共字段
|
||||
// 入参: item 明细,kind 类别,name 概念名
|
||||
// 返回: 字段地址,其他明细仅保留事实
|
||||
func invoiceItemField(item *InvoiceItem, kind, name string) *string {
|
||||
if kind != "inv" {
|
||||
return nil
|
||||
@@ -942,9 +942,9 @@ func invoiceItemField(item *InvoiceItem, kind, name string) *string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// invoiceAmountCheck 精确检查明确的价税合计关系,不推算航空费用。
|
||||
// 入参: inv 发票,report 诊断目标,source 来源。
|
||||
// 返回: 无。
|
||||
// invoiceAmountCheck 精确检查明确的价税合计关系,不推算航空费用
|
||||
// 入参: inv 发票,report 诊断目标,source 来源
|
||||
// 返回: 无
|
||||
func invoiceAmountCheck(inv *Invoice, report *InvoiceReport, source InvoiceSource) {
|
||||
if inv.Kind != "inv" && inv.Kind != "einv" {
|
||||
return
|
||||
@@ -955,9 +955,9 @@ func invoiceAmountCheck(inv *Invoice, report *InvoiceReport, source InvoiceSourc
|
||||
}
|
||||
}
|
||||
|
||||
// invoicePlainXML 解析官方数电发票指南中的无命名空间0.3版本。
|
||||
// 入参: ctx 上下文,root 根节点,source 来源。
|
||||
// 返回: 发票及诊断,取消错误。
|
||||
// invoicePlainXML 解析官方数电发票指南中的无命名空间0.3版本
|
||||
// 入参: ctx 上下文,root 根节点,source 来源
|
||||
// 返回: 发票及诊断,取消错误
|
||||
func invoicePlainXML(ctx context.Context, root *invoiceXMLNode, source InvoiceSource) (InvoiceReport, error) {
|
||||
report := InvoiceReport{}
|
||||
headers := invoiceChildren(root, "", "Header")
|
||||
@@ -1044,9 +1044,9 @@ func invoicePlainXML(ctx context.Context, root *invoiceXMLNode, source InvoiceSo
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// invoicePlainField 按官方样例中的完整路径映射字段。
|
||||
// 入参: inv 发票,item 当前明细,path 完整无命名空间路径。
|
||||
// 返回: 字段地址及数值标志。
|
||||
// invoicePlainField 按官方样例中的完整路径映射字段
|
||||
// 入参: inv 发票,item 当前明细,path 完整无命名空间路径
|
||||
// 返回: 字段地址及数值标志
|
||||
func invoicePlainField(inv *Invoice, item *InvoiceItem, path string) (*string, bool) {
|
||||
switch path {
|
||||
case "/EInvoice/TaxSupervisionInfo/InvoiceNumber":
|
||||
|
||||
+9
-9
@@ -140,7 +140,7 @@ type CompositeGraphicUnit struct {
|
||||
extentSet bool
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析资源文件,隔离外部命名空间的同名集合。
|
||||
// UnmarshalXML 解析资源文件,隔离外部命名空间的同名集合
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (res *Res) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
@@ -168,14 +168,14 @@ func (res *Res) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析颜色空间集合。
|
||||
// UnmarshalXML 解析颜色空间集合
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (spaces *ColorSpaces) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
return decodeOFDCollection(d, start, "ColorSpace", &spaces.ColorSpace)
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析颜色空间属性及调色板。
|
||||
// UnmarshalXML 解析颜色空间属性及调色板
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (space *ColorSpace) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
@@ -195,14 +195,14 @@ func (space *ColorSpace) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析字体集合。
|
||||
// UnmarshalXML 解析字体集合
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (fonts *Fonts) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
return decodeOFDCollection(d, start, "Font", &fonts.Font)
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析字体声明及内嵌字体路径。
|
||||
// UnmarshalXML 解析字体声明及内嵌字体路径
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (font *Font) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
@@ -238,14 +238,14 @@ func (font *Font) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析多媒体集合。
|
||||
// UnmarshalXML 解析多媒体集合
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (media *MultiMedias) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
return decodeOFDCollection(d, start, "MultiMedia", &media.MultiMedia)
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析多媒体声明及包内文件路径。
|
||||
// UnmarshalXML 解析多媒体声明及包内文件路径
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (media *MultiMedia) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
@@ -258,14 +258,14 @@ func (media *MultiMedia) UnmarshalXML(d *xml.Decoder, start xml.StartElement) er
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析绘制参数集合。
|
||||
// UnmarshalXML 解析绘制参数集合
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (params *DrawParams) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
return decodeOFDCollection(d, start, "DrawParam", ¶ms.DrawParam)
|
||||
}
|
||||
|
||||
// UnmarshalXML 解析复合图元资源集合。
|
||||
// UnmarshalXML 解析复合图元资源集合
|
||||
// 入参: d XML解码器, start 起始节点
|
||||
// 返回: error 解码错误
|
||||
func (units *CompositeGraphicUnits) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
|
||||
+22
-22
@@ -29,14 +29,14 @@ import (
|
||||
"golang.org/x/text/unicode/bidi"
|
||||
)
|
||||
|
||||
// richLayout 保存局部标准对象、含墨迹的边界及各片段的字体锚点序号。
|
||||
// richLayout 保存局部标准对象、含墨迹的边界及各片段的字体锚点序号
|
||||
type richLayout struct {
|
||||
objects []GraphicObject
|
||||
bounds Box
|
||||
anchors []int
|
||||
}
|
||||
|
||||
// richMetrics 保存片段字体、共用基线度量和实际需要的合成样式。
|
||||
// richMetrics 保存片段字体、共用基线度量和实际需要的合成样式
|
||||
type richMetrics struct {
|
||||
font FontMetrics
|
||||
ascent, descent, height float64
|
||||
@@ -44,7 +44,7 @@ type richMetrics struct {
|
||||
outlines map[uint16]GeometryPath
|
||||
}
|
||||
|
||||
// richSpan 保存方向和格式均一致的逻辑区间及行内视觉坐标。
|
||||
// richSpan 保存方向和格式均一致的逻辑区间及行内视觉坐标
|
||||
type richSpan struct {
|
||||
start, end, owner int
|
||||
rtl bool
|
||||
@@ -52,7 +52,7 @@ type richSpan struct {
|
||||
x, width float64
|
||||
}
|
||||
|
||||
// richContextShaper 将带上下文的区间塑形接入已有簇校验流程。
|
||||
// richContextShaper 将带上下文的区间塑形接入已有簇校验流程
|
||||
type richContextShaper struct {
|
||||
FontShaperRun
|
||||
text []rune
|
||||
@@ -60,12 +60,12 @@ type richContextShaper struct {
|
||||
options TextShapeOptions
|
||||
}
|
||||
|
||||
// LayoutRichText 生成局部坐标的标准文字和路径,不修改页面或编辑历史。
|
||||
// 边界包含内容的排版高度和溢出墨迹,不包含空余段落宽度,可能有负的X或Y。
|
||||
// 非标准RGB文档按需复用或注册RGB颜色空间,返回对象可直接用于当前文档。
|
||||
// 注册不产生修订或撤销记录;失败时不保留新增资源和资源编号。
|
||||
// 提交对象后应创建新的Reader;已有Reader是独立快照,不包含新注册的资源。
|
||||
// 返回值的资源编号属于当前资源视图,切换文档、版本或替换资源快照后须重新排版。
|
||||
// LayoutRichText 生成局部坐标的标准文字和路径,不修改页面或编辑历史
|
||||
// 边界包含内容的排版高度和溢出墨迹,不包含空余段落宽度,可能有负的X或Y
|
||||
// 非标准RGB文档按需复用或注册RGB颜色空间,返回对象可直接用于当前文档
|
||||
// 注册不产生修订或撤销记录;失败时不保留新增资源和资源编号
|
||||
// 提交对象后应创建新的Reader;已有Reader是独立快照,不包含新注册的资源
|
||||
// 返回值的资源编号属于当前资源视图,切换文档、版本或替换资源快照后须重新排版
|
||||
func (e *Editor) LayoutRichText(value RichText, width float64) ([]GraphicObject, Box, error) {
|
||||
result, err := e.layoutRichText(value, width)
|
||||
if err != nil {
|
||||
@@ -85,7 +85,7 @@ func (e *Editor) LayoutRichText(value RichText, width float64) ([]GraphicObject,
|
||||
return result.objects, result.bounds, nil
|
||||
}
|
||||
|
||||
// layoutRichText 统一计算段落换行、视觉顺序和基线,颜色资源由调用方绑定。
|
||||
// layoutRichText 统一计算段落换行、视觉顺序和基线,颜色资源由调用方绑定
|
||||
func (e *Editor) layoutRichText(value RichText, width float64) (richLayout, error) {
|
||||
if err := value.Validate(); err != nil {
|
||||
return richLayout{}, err
|
||||
@@ -327,7 +327,7 @@ func (e *Editor) layoutRichText(value RichText, width float64) (richLayout, erro
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// richParagraphOptions 以整段原文检测双向排版,与普通文字的自动模式一致。
|
||||
// richParagraphOptions 以整段原文检测双向排版,与普通文字的自动模式一致
|
||||
func richParagraphOptions(chars []rune, options TextLayout) TextLayout {
|
||||
if options.AutoBidi && !options.Shaping.Bidi {
|
||||
for _, char := range chars {
|
||||
@@ -341,7 +341,7 @@ func richParagraphOptions(chars []rune, options TextLayout) TextLayout {
|
||||
return options
|
||||
}
|
||||
|
||||
// richShapeLine 按方向、脚本和格式塑形整行,跨同格式片段合并,保留区间外的塑形上下文。
|
||||
// richShapeLine 按方向、脚本和格式塑形整行,跨同格式片段合并,保留区间外的塑形上下文
|
||||
func richShapeLine(chars []rune, owners []int, value RichText, metrics []richMetrics, options TextLayout, face *typefont.Face) ([]richSpan, []float64, error) {
|
||||
advances := make([]float64, len(chars))
|
||||
if len(chars) == 0 {
|
||||
@@ -430,7 +430,7 @@ func richShapeLine(chars []rune, owners []int, value RichText, metrics []richMet
|
||||
return spans, advances, nil
|
||||
}
|
||||
|
||||
// ShapeText 返回区间相对簇索引,同时保留完整行提供的上下文。
|
||||
// ShapeText 返回区间相对簇索引,同时保留完整行提供的上下文
|
||||
func (s richContextShaper) ShapeText(_ string, size float64) ([]ShapedGlyph, error) {
|
||||
glyphs, err := s.ShapeTextRun(s.text, s.start, s.end, size, s.options)
|
||||
if err != nil {
|
||||
@@ -442,7 +442,7 @@ func (s richContextShaper) ShapeText(_ string, size float64) ([]ShapedGlyph, err
|
||||
return glyphs, nil
|
||||
}
|
||||
|
||||
// richPositionSpans 按核心排版相同的方向分组规则排列片段,按视觉簇分配字距及两端对齐。
|
||||
// richPositionSpans 按核心排版相同的方向分组规则排列片段,按视觉簇分配字距及两端对齐
|
||||
func richPositionSpans(spans []richSpan, steps []float64, options TextLayout, x float64) error {
|
||||
order := make([]int, len(spans))
|
||||
for i := range order {
|
||||
@@ -490,7 +490,7 @@ func richPositionSpans(spans []richSpan, steps []float64, options TextLayout, x
|
||||
return nil
|
||||
}
|
||||
|
||||
// richSpanText 将逻辑原文和绝对视觉位置写为标准字符字形映射。
|
||||
// richSpanText 将逻辑原文和绝对视觉位置写为标准字符字形映射
|
||||
func richSpanText(chars []rune, span richSpan, format TextFormat, baseline float64) TextObject {
|
||||
text := richTextObject(format)
|
||||
code := TextCode{Value: escapeOFDText(string(chars[span.start:span.end]))}
|
||||
@@ -524,7 +524,7 @@ func richSpanText(chars []rune, span richSpan, format TextFormat, baseline float
|
||||
return text
|
||||
}
|
||||
|
||||
// richSpanOutline 使用现有字体及几何后端生成与文字合成样式一致的墨迹。
|
||||
// richSpanOutline 使用现有字体及几何后端生成与文字合成样式一致的墨迹
|
||||
func richSpanOutline(span richSpan, format TextFormat, metrics richMetrics, geometry GeometryBackend, baseline float64) (GeometryPath, error) {
|
||||
font, ok := metrics.font.(FontOutlines)
|
||||
if !ok {
|
||||
@@ -567,7 +567,7 @@ func richSpanOutline(span richSpan, format TextFormat, metrics richMetrics, geom
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// richSynthetic 与标准绘制一样避免对已加粗或倾斜的字体重复合成。
|
||||
// richSynthetic 与标准绘制一样避免对已加粗或倾斜的字体重复合成
|
||||
func (e *Editor) richSynthetic(format TextFormat) (bool, bool) {
|
||||
bold, italic := format.Bold, format.Italic
|
||||
var definition *Font
|
||||
@@ -590,7 +590,7 @@ func (e *Editor) richSynthetic(format TextFormat) (bool, bool) {
|
||||
return bold, italic
|
||||
}
|
||||
|
||||
// richTextObject 构造具有显式填充和标准字重的文字对象。
|
||||
// richTextObject 构造具有显式填充和标准字重的文字对象
|
||||
func richTextObject(format TextFormat) TextObject {
|
||||
fill, stroke := true, false
|
||||
weight := 400
|
||||
@@ -600,7 +600,7 @@ func richTextObject(format TextFormat) TextObject {
|
||||
return TextObject{Font: format.Font, Size: format.Size, Weight: weight, Italic: format.Italic, Fill: &fill, Stroke: &stroke, FillColor: &FillColor{Value: richColor(format.Color)}}
|
||||
}
|
||||
|
||||
// richColor 将空前景色转换为标准黑色。
|
||||
// richColor 将空前景色转换为标准黑色
|
||||
func richColor(value string) string {
|
||||
if value == "" {
|
||||
return "0 0 0"
|
||||
@@ -608,13 +608,13 @@ func richColor(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// richUnion 合并排版范围和墨迹范围,保留负坐标及零宽空行。
|
||||
// richUnion 合并排版范围和墨迹范围,保留负坐标及零宽空行
|
||||
func richUnion(a, b Box) Box {
|
||||
x, y := math.Min(a.X, b.X), math.Min(a.Y, b.Y)
|
||||
return Box{X: x, Y: y, W: math.Max(a.X+a.W, b.X+b.W) - x, H: math.Max(a.Y+a.H, b.Y+b.H) - y}
|
||||
}
|
||||
|
||||
// richRectangle 使用标准填充路径绘制背景或文字装饰。
|
||||
// richRectangle 使用标准填充路径绘制背景或文字装饰
|
||||
func richRectangle(box Box, color string) GraphicObject {
|
||||
fill, stroke := true, false
|
||||
return GraphicObject{Type: "PathObject", PathObject: PathObject{Boundary: editorBoxString(box), Fill: &fill, Stroke: &stroke, FillColor: &FillColor{Value: color}, AbbreviatedData: fmt.Sprintf("M 0 0 L %s 0 L %s %s L 0 %s C", ofdNumber(box.W), ofdNumber(box.W), ofdNumber(box.H), ofdNumber(box.H))}}
|
||||
|
||||
+18
-18
@@ -23,22 +23,22 @@ import (
|
||||
"github.com/go-text/typesetting/segmenter"
|
||||
)
|
||||
|
||||
// ErrNotRichText 表示对象没有富文本信息且不属于可提升的普通文字。
|
||||
// ErrNotRichText 表示对象没有富文本信息且不属于可提升的普通文字
|
||||
var ErrNotRichText = errors.New("object is not rich text")
|
||||
|
||||
// RichText 保存有序文字片段及段落选项,长度和范围使用Unicode码点而非字节。
|
||||
// RichText 保存有序文字片段及段落选项,长度和范围使用Unicode码点而非字节
|
||||
type RichText struct {
|
||||
Runs []RichTextRun `json:"runs"`
|
||||
Layout TextLayout `json:"layout"`
|
||||
}
|
||||
|
||||
// RichTextRun 保存文字及完整格式,换行使用LF,长度单位为毫米。
|
||||
// RichTextRun 保存文字及完整格式,换行使用LF,长度单位为毫米
|
||||
type RichTextRun struct {
|
||||
Text string `json:"text"`
|
||||
Format TextFormat `json:"format"`
|
||||
}
|
||||
|
||||
// TextFormat 保存完整文字格式,Font为当前文档字体资源标识,颜色使用OFD RGB分量。
|
||||
// TextFormat 保存完整文字格式,Font为当前文档字体资源标识,颜色使用OFD RGB分量
|
||||
type TextFormat struct {
|
||||
Font string `json:"font"`
|
||||
Size float64 `json:"size"`
|
||||
@@ -51,7 +51,7 @@ type TextFormat struct {
|
||||
Background string `json:"background"`
|
||||
}
|
||||
|
||||
// TextFormatPatch 保存格式增量,nil保持原值,false和空背景显式清除对应格式。
|
||||
// TextFormatPatch 保存格式增量,nil保持原值,false和空背景显式清除对应格式
|
||||
type TextFormatPatch struct {
|
||||
Font *string `json:"font,omitempty"`
|
||||
Size *float64 `json:"size,omitempty"`
|
||||
@@ -64,7 +64,7 @@ type TextFormatPatch struct {
|
||||
Background *string `json:"background,omitempty"`
|
||||
}
|
||||
|
||||
// Text 返回包含显式换行的原文,不包含排版产生的软换行。
|
||||
// Text 返回包含显式换行的原文,不包含排版产生的软换行
|
||||
// 返回: string 完整原文
|
||||
func (v RichText) Text() string {
|
||||
var text strings.Builder
|
||||
@@ -74,11 +74,11 @@ func (v RichText) Text() string {
|
||||
return text.String()
|
||||
}
|
||||
|
||||
// Len 返回原文的Unicode码点数量。
|
||||
// Len 返回原文的Unicode码点数量
|
||||
// 返回: int 码点数量
|
||||
func (v RichText) Len() int { return utf8.RuneCountInString(v.Text()) }
|
||||
|
||||
// Validate 校验文字、格式、段落选项及跨片段的字素边界,不加载字体。
|
||||
// Validate 校验文字、格式、段落选项及跨片段的字素边界,不加载字体
|
||||
// 返回: error 无效文字、格式或边界
|
||||
func (v RichText) Validate() error {
|
||||
if len(v.Runs) == 0 {
|
||||
@@ -104,7 +104,7 @@ func (v RichText) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate 校验完整文字格式,不解析文档资源。
|
||||
// validate 校验完整文字格式,不解析文档资源
|
||||
// 返回: error 格式错误
|
||||
func (f TextFormat) validate() error {
|
||||
if strings.TrimSpace(f.Font) == "" || !finite(f.Size) || f.Size <= 0 {
|
||||
@@ -126,7 +126,7 @@ func (f TextFormat) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateRichLayout 校验富文本段落的有限数值及枚举。
|
||||
// validateRichLayout 校验富文本段落的有限数值及枚举
|
||||
// 入参: v 段落选项
|
||||
// 返回: error 无效选项
|
||||
func validateRichLayout(v TextLayout) error {
|
||||
@@ -141,7 +141,7 @@ func validateRichLayout(v TextLayout) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// richTextBoundaries 返回包含首尾位置的扩展字素边界。
|
||||
// richTextBoundaries 返回包含首尾位置的扩展字素边界
|
||||
// 入参: value 原文
|
||||
// 返回: map[int]bool 有效码点边界
|
||||
func richTextBoundaries(value string) map[int]bool {
|
||||
@@ -156,7 +156,7 @@ func richTextBoundaries(value string) map[int]bool {
|
||||
return result
|
||||
}
|
||||
|
||||
// checkRange 检查半开范围及其字素边界。
|
||||
// checkRange 检查半开范围及其字素边界
|
||||
// 入参: start 起点, end 终点
|
||||
// 返回: error 无效范围
|
||||
func (v RichText) checkRange(start, end int) error {
|
||||
@@ -170,7 +170,7 @@ func (v RichText) checkRange(start, end int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Slice 返回半开范围的独立片段,空范围保留插入位置的格式。
|
||||
// Slice 返回半开范围的独立片段,空范围保留插入位置的格式
|
||||
// 入参: start 起点, end 终点
|
||||
// 返回: RichText 独立片段, error 无效范围
|
||||
func (v RichText) Slice(start, end int) (RichText, error) {
|
||||
@@ -193,7 +193,7 @@ func (v RichText) Slice(start, end int) (RichText, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// sliceRuns 提取已校验范围,不保留空片段。
|
||||
// sliceRuns 提取已校验范围,不保留空片段
|
||||
// 入参: start 起点, end 终点
|
||||
// 返回: []RichTextRun 独立片段
|
||||
func (v RichText) sliceRuns(start, end int) []RichTextRun {
|
||||
@@ -210,7 +210,7 @@ func (v RichText) sliceRuns(start, end int) []RichTextRun {
|
||||
return result
|
||||
}
|
||||
|
||||
// Replace 原子替换半开范围,空片段列表表示删除,相邻相同格式自动合并。
|
||||
// Replace 原子替换半开范围,空片段列表表示删除,相邻相同格式自动合并
|
||||
// 入参: start 起点, end 终点, runs 替换片段
|
||||
// 返回: error 范围或内容错误
|
||||
func (v *RichText) Replace(start, end int, runs []RichTextRun) error {
|
||||
@@ -235,7 +235,7 @@ func (v *RichText) Replace(start, end int, runs []RichTextRun) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyFormat 原子修改半开范围的格式,空范围不改变正文。
|
||||
// ApplyFormat 原子修改半开范围的格式,空范围不改变正文
|
||||
// 入参: start 起点, end 终点, patch 格式增量
|
||||
// 返回: error 范围或格式错误
|
||||
func (v *RichText) ApplyFormat(start, end int, patch TextFormatPatch) error {
|
||||
@@ -258,7 +258,7 @@ func (v *RichText) ApplyFormat(start, end int, patch TextFormatPatch) error {
|
||||
return v.Replace(start, end, part.Runs)
|
||||
}
|
||||
|
||||
// apply 应用显式给出的格式字段。
|
||||
// apply 应用显式给出的格式字段
|
||||
// 入参: f 完整原格式
|
||||
// 返回: TextFormat 修改后的格式
|
||||
func (p TextFormatPatch) apply(f TextFormat) TextFormat {
|
||||
@@ -292,7 +292,7 @@ func (p TextFormatPatch) apply(f TextFormat) TextFormat {
|
||||
return f
|
||||
}
|
||||
|
||||
// normalizeRichText 合并相同格式,保留空文档的插入格式。
|
||||
// normalizeRichText 合并相同格式,保留空文档的插入格式
|
||||
// 入参: v 完整富文本
|
||||
// 返回: RichText 归一化副本
|
||||
func normalizeRichText(v RichText) RichText {
|
||||
|
||||
+7
-3
@@ -30,7 +30,7 @@ const structureTypeOfficial = "od"
|
||||
const structureNamespaceEGov2013 = "http://www.nits.org.cn/eGovDoc/2013"
|
||||
|
||||
// Structure 保存自定义标引入口及结构树,单个文件解析失败时通过Error报告,不影响其他标引
|
||||
// CanBind表示已识别标引引用规则,包括公文标引及本库富文本标引,具体节点和目标仍由绑定接口校验
|
||||
// CanBind表示已识别标引引用规则,包括公文、富文本及发票标引,具体节点和目标仍由绑定接口校验
|
||||
// Internal标记本库内部编辑标引,不删除或重排入口;界面跳过此项时必须保留原始数组索引
|
||||
// 此标记不限制通用低级编辑接口,也不表示内部元数据内容已通过有效性校验
|
||||
type Structure struct {
|
||||
@@ -45,7 +45,7 @@ type Structure struct {
|
||||
|
||||
// StructureNode 保存标引节点的名称、属性、直接文本和有序子节点,不改变原始XML
|
||||
// Text按文档顺序汇总直接文本并保留空白,不包含子节点文本
|
||||
// References仅解释已识别公文标引或本库富文本标引中的ObjectRef;未知扩展保留结构,不推定其引用语义
|
||||
// References仅解释已识别公文、富文本或发票标引中的ObjectRef;未知扩展保留结构,不推定其引用语义
|
||||
type StructureNode struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
@@ -124,6 +124,7 @@ func (r *Reader) readStructureContext(ctx context.Context, tag CustomTag) (Struc
|
||||
}
|
||||
item.CanBind = structureOfficial(tag, item.Root.Namespace)
|
||||
item.Internal = tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace && item.Root.Namespace == richTextNamespace && item.Root.Name == "RichText"
|
||||
item.Internal = item.Internal || tag.TypeID == invoiceTagType && tag.NameSpace == invoiceNamespace && item.Root.Namespace == invoiceNamespace && item.Root.Name == "Invoice"
|
||||
return item, nil
|
||||
}
|
||||
|
||||
@@ -215,10 +216,13 @@ func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*Structure
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// structureOfficial 识别公文和本库富文本标引的明确引用规则,不推断未知行业语义
|
||||
// structureOfficial 识别公文、富文本及发票标引的明确引用规则,不推断未知行业语义
|
||||
// 入参: tag 标引入口, namespace 内容根节点命名空间
|
||||
// 返回: bool 是否使用公文引用规则
|
||||
func structureOfficial(tag CustomTag, namespace string) bool {
|
||||
if tag.TypeID == invoiceTagType && tag.NameSpace == invoiceNamespace && namespace == invoiceNamespace {
|
||||
return true
|
||||
}
|
||||
if tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace && namespace == richTextNamespace {
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user