mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +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
|
var size int
|
||||||
switch binary.BigEndian.Uint16(data) {
|
switch binary.BigEndian.Uint16(data) {
|
||||||
case 0:
|
case 0:
|
||||||
// 旧式68字节表保持原样,不将截断的78字节表推断为旧式表。
|
// 旧式68字节表保持原样,不将截断的78字节表推断为旧式表
|
||||||
size = 78
|
size = 78
|
||||||
case 1:
|
case 1:
|
||||||
size = 86
|
size = 86
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ func (e *Editor) CopyObjectsContext(ctx context.Context, page int, objects []Gra
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyObjectsContext 复制标准对象及资源来源,由上层按需同步富文本标引。
|
// copyObjectsContext 复制标准对象及资源来源,由上层按需同步富文本标引
|
||||||
// 入参: ctx 取消上下文, page 目标页, objects 对象快照, dx 横向位移, dy 纵向位移
|
// 入参: ctx 取消上下文, page 目标页, objects 对象快照, dx 横向位移, dy 纵向位移
|
||||||
// 返回: []string 新对象标识, error 校验或取消错误
|
// 返回: []string 新对象标识, error 校验或取消错误
|
||||||
func (e *Editor) copyObjectsContext(ctx context.Context, page int, objects []GraphicObject, dx, dy float64) ([]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 对象标识
|
// 入参: page 页面索引, ids 对象标识
|
||||||
// 返回: error 校验或删除错误
|
// 返回: error 校验或删除错误
|
||||||
func (e *Editor) deleteObjects(page int, ids []string) error {
|
func (e *Editor) deleteObjects(page int, ids []string) error {
|
||||||
|
|||||||
+41
-41
@@ -25,8 +25,8 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bookmarks 按声明顺序读取当前文档版本的命名书签,返回独立副本,不读取跳转引用。
|
// Bookmarks 按声明顺序读取当前文档版本的命名书签,返回独立副本,不读取跳转引用
|
||||||
// 返回: []Bookmark 书签列表,无书签时为nil;error 读取、结构或名称重复错误。
|
// 返回: []Bookmark 书签列表,无书签时为nil;error 读取、结构或名称重复错误
|
||||||
func (e *Editor) Bookmarks() ([]Bookmark, error) {
|
func (e *Editor) Bookmarks() ([]Bookmark, error) {
|
||||||
if e.source == nil {
|
if e.source == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -42,30 +42,30 @@ func (e *Editor) Bookmarks() ([]Bookmark, error) {
|
|||||||
return editorBookmarks(data, root)
|
return editorBookmarks(data, root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddBookmark 在当前文档版本末尾添加名称唯一的书签,保留扩展内容并记录一次撤销。
|
// AddBookmark 在当前文档版本末尾添加名称唯一的书签,保留扩展内容并记录一次撤销
|
||||||
// 入参: name 非空且区分大小写的名称,不自动裁剪空白;dest 目标,PageID为当前页面标识而非页序号。
|
// 入参: name 非空且区分大小写的名称,不自动裁剪空白;dest 目标,PageID为当前页面标识而非页序号
|
||||||
// 返回: error 名称、目标、页面或提交错误。
|
// 返回: error 名称、目标、页面或提交错误
|
||||||
func (e *Editor) AddBookmark(name string, dest Dest) error {
|
func (e *Editor) AddBookmark(name string, dest Dest) error {
|
||||||
return e.editBookmark("", &Bookmark{Name: name, Dest: dest}, true)
|
return e.editBookmark("", &Bookmark{Name: name, Dest: dest}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateBookmark 按原名称更新书签,重命名同步当前文档全部标准跳转引用,整体可撤销。
|
// UpdateBookmark 按原名称更新书签,重命名同步当前文档全部标准跳转引用,整体可撤销
|
||||||
// 入参: name 原名称;next 完整的新书签,Dest.PageID为当前页面标识;名称不变时仅修改目标。
|
// 入参: name 原名称;next 完整的新书签,Dest.PageID为当前页面标识;名称不变时仅修改目标
|
||||||
// 返回: error 名称不存在、重名、目标无效、引用读取或提交错误;内容未变时不记录撤销。
|
// 返回: error 名称不存在、重名、目标无效、引用读取或提交错误;内容未变时不记录撤销
|
||||||
func (e *Editor) UpdateBookmark(name string, next Bookmark) error {
|
func (e *Editor) UpdateBookmark(name string, next Bookmark) error {
|
||||||
return e.editBookmark(name, &next, false)
|
return e.editBookmark(name, &next, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBookmark 删除当前文档版本的命名书签,存在标准跳转引用时拒绝删除。
|
// DeleteBookmark 删除当前文档版本的命名书签,存在标准跳转引用时拒绝删除
|
||||||
// 入参: name 书签名称;其他文档和历史版本的引用不阻止当前版本删除。
|
// 入参: name 书签名称;其他文档和历史版本的引用不阻止当前版本删除
|
||||||
// 返回: error 名称不存在、仍被引用、引用读取或提交错误。
|
// 返回: error 名称不存在、仍被引用、引用读取或提交错误
|
||||||
func (e *Editor) DeleteBookmark(name string) error {
|
func (e *Editor) DeleteBookmark(name string) error {
|
||||||
return e.editBookmark(name, nil, false)
|
return e.editBookmark(name, nil, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBookmarks 读取标准书签定义,拒绝重复集合、重名及缺失或重复目标,不解析扩展子树。
|
// editorBookmarks 读取标准书签定义,拒绝重复集合、重名及缺失或重复目标,不解析扩展子树
|
||||||
// 入参: data 文档原文;root 文档根节点。
|
// 入参: data 文档原文;root 文档根节点
|
||||||
// 返回: []Bookmark 独立定义列表;error 结构或解码错误。
|
// 返回: []Bookmark 独立定义列表;error 结构或解码错误
|
||||||
func editorBookmarks(data []byte, root *editorXML) ([]Bookmark, error) {
|
func editorBookmarks(data []byte, root *editorXML) ([]Bookmark, error) {
|
||||||
if root.childAt("Bookmarks", 1) != nil {
|
if root.childAt("Bookmarks", 1) != nil {
|
||||||
return nil, fmt.Errorf("multiple bookmark collections")
|
return nil, fmt.Errorf("multiple bookmark collections")
|
||||||
@@ -104,9 +104,9 @@ func editorBookmarks(data []byte, root *editorXML) ([]Bookmark, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateBookmarkName 校验非空名称及XML可无损表示的Unicode字符。
|
// validateBookmarkName 校验非空名称及XML可无损表示的Unicode字符
|
||||||
// 入参: name 名称。
|
// 入参: name 名称
|
||||||
// 返回: error 名称错误。
|
// 返回: error 名称错误
|
||||||
func validateBookmarkName(name string) error {
|
func validateBookmarkName(name string) error {
|
||||||
if strings.TrimSpace(name) == "" || !utf8.ValidString(name) {
|
if strings.TrimSpace(name) == "" || !utf8.ValidString(name) {
|
||||||
return fmt.Errorf("bookmark name must be nonempty valid UTF-8")
|
return fmt.Errorf("bookmark name must be nonempty valid UTF-8")
|
||||||
@@ -119,9 +119,9 @@ func validateBookmarkName(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateBookmark 校验书签名称、标准目标参数及当前编辑页树中的目标标识。
|
// validateBookmark 校验书签名称、标准目标参数及当前编辑页树中的目标标识
|
||||||
// 入参: bookmark 待写入定义。
|
// 入参: bookmark 待写入定义
|
||||||
// 返回: error 名称、目标或页面错误。
|
// 返回: error 名称、目标或页面错误
|
||||||
func (e *Editor) validateBookmark(bookmark Bookmark) error {
|
func (e *Editor) validateBookmark(bookmark Bookmark) error {
|
||||||
if err := validateBookmarkName(bookmark.Name); err != nil {
|
if err := validateBookmarkName(bookmark.Name); err != nil {
|
||||||
return err
|
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)
|
return fmt.Errorf("bookmark destination page %q does not exist", bookmark.Dest.PageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editBookmark 校验定义后原子提交局部XML补丁,重命名使用包含未保存编辑的完整快照。
|
// editBookmark 校验定义后原子提交局部XML补丁,重命名使用包含未保存编辑的完整快照
|
||||||
// 入参: name 原名称;next 新定义,nil表示删除;add 是否新增。
|
// 入参: name 原名称;next 新定义,nil表示删除;add 是否新增
|
||||||
// 返回: error 校验、引用扫描或提交错误。
|
// 返回: error 校验、引用扫描或提交错误
|
||||||
func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error {
|
func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error {
|
||||||
if !add {
|
if !add {
|
||||||
if err := validateBookmarkName(name); err != nil {
|
if err := validateBookmarkName(name); err != nil {
|
||||||
@@ -241,9 +241,9 @@ func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitBookmarkSnapshot 接入重命名后的包,保留已加载对象的会话排版、裁剪及尚未使用的注册资源。
|
// commitBookmarkSnapshot 接入重命名后的包,保留已加载对象的会话排版、裁剪及尚未使用的注册资源
|
||||||
// 入参: reader 已完成隔离和重命名的包快照。
|
// 入参: reader 已完成隔离和重命名的包快照
|
||||||
// 返回: error 快照重建或页面加载错误。
|
// 返回: error 快照重建或页面加载错误
|
||||||
func (e *Editor) commitBookmarkSnapshot(reader *Reader) error {
|
func (e *Editor) commitBookmarkSnapshot(reader *Reader) error {
|
||||||
before := e.transactionSnapshot()
|
before := e.transactionSnapshot()
|
||||||
if err := e.commitResourceSnapshot(context.Background(), reader); err != nil {
|
if err := e.commitResourceSnapshot(context.Background(), reader); err != nil {
|
||||||
@@ -282,9 +282,9 @@ func (e *Editor) commitBookmarkSnapshot(reader *Reader) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitBookmarkParts 隔离修改后复用注解快照提交,保守复制路径不覆盖未知扩展可能引用的原文件。
|
// commitBookmarkParts 隔离修改后复用注解快照提交,保守复制路径不覆盖未知扩展可能引用的原文件
|
||||||
// 入参: base 原包快照;parts 当前入口修改集。
|
// 入参: base 原包快照;parts 当前入口修改集
|
||||||
// 返回: error 依赖解析、隔离或提交错误。
|
// 返回: error 依赖解析、隔离或提交错误
|
||||||
func (e *Editor) commitBookmarkParts(base *editorSource, parts map[string][]byte) error {
|
func (e *Editor) commitBookmarkParts(base *editorSource, parts map[string][]byte) error {
|
||||||
changes, err := base.reader.versionChanges(context.Background(), parts)
|
changes, err := base.reader.versionChanges(context.Background(), parts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -303,9 +303,9 @@ func (e *Editor) commitBookmarkParts(base *editorSource, parts map[string][]byte
|
|||||||
return e.commitAnnotationParts(&source, nil)
|
return e.commitAnnotationParts(&source, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBookmarkIsolate 在无法完整分析其他入口时保护所有已有文件,复制修改文件及其标准引用祖先。
|
// editorBookmarkIsolate 在无法完整分析其他入口时保护所有已有文件,复制修改文件及其标准引用祖先
|
||||||
// 入参: reader 原包;changes 当前入口修改集。
|
// 入参: reader 原包;changes 当前入口修改集
|
||||||
// 返回: map[string][]byte 隔离后的修改集;error 当前入口依赖或版本清单错误。
|
// 返回: map[string][]byte 隔离后的修改集;error 当前入口依赖或版本清单错误
|
||||||
func editorBookmarkIsolate(reader *Reader, changes map[string][]byte) (map[string][]byte, error) {
|
func editorBookmarkIsolate(reader *Reader, changes map[string][]byte) (map[string][]byte, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
entry, err := reader.DocumentRootContext(ctx)
|
entry, err := reader.DocumentRootContext(ctx)
|
||||||
@@ -386,9 +386,9 @@ func editorBookmarkIsolate(reader *Reader, changes map[string][]byte) (map[strin
|
|||||||
return parts, nil
|
return parts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBookmarkDefinition 修改指定书签的标准字段,保留其扩展属性、扩展节点及其他定义原文。
|
// editorBookmarkDefinition 修改指定书签的标准字段,保留其扩展属性、扩展节点及其他定义原文
|
||||||
// 入参: data 文档原文;name 原名称;next 新定义,nil表示删除;add 是否新增。
|
// 入参: data 文档原文;name 原名称;next 新定义,nil表示删除;add 是否新增
|
||||||
// 返回: []byte 修改后的文档;error 结构或编码错误。
|
// 返回: []byte 修改后的文档;error 结构或编码错误
|
||||||
func editorBookmarkDefinition(data []byte, name string, next *Bookmark, add bool) ([]byte, error) {
|
func editorBookmarkDefinition(data []byte, name string, next *Bookmark, add bool) ([]byte, error) {
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXML(data)
|
||||||
if err != nil {
|
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)
|
return nil, fmt.Errorf("bookmark %q not found", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBookmarkReferences 扫描当前入口依赖中的标准书签跳转,拒绝带引用删除或生成重命名补丁。
|
// editorBookmarkReferences 扫描当前入口依赖中的标准书签跳转,拒绝带引用删除或生成重命名补丁
|
||||||
// 入参: reader 包含未保存修改的快照;name 原名称;next 新定义,nil表示删除。
|
// 入参: reader 包含未保存修改的快照;name 原名称;next 新定义,nil表示删除
|
||||||
// 返回: map[string][]byte 需要修改的结构文件;error 读取、解析或被引用错误。
|
// 返回: map[string][]byte 需要修改的结构文件;error 读取、解析或被引用错误
|
||||||
func editorBookmarkReferences(reader *Reader, name string, next *Bookmark) (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)
|
_, structures, err := reader.versionDependencies(context.Background(), reader.documentRoot, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -522,9 +522,9 @@ func editorBookmarkReferences(reader *Reader, name string, next *Bookmark) (map[
|
|||||||
return parts, nil
|
return parts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBookmarkReferenceXML 仅识别标准动作中的名称引用,保留扩展命名空间及不透明扩展负载。
|
// editorBookmarkReferenceXML 仅识别标准动作中的名称引用,保留扩展命名空间及不透明扩展负载
|
||||||
// 入参: data 结构原文;file 错误上下文中的文件名;name 原名称;next 新定义,nil表示检查删除。
|
// 入参: data 结构原文;file 错误上下文中的文件名;name 原名称;next 新定义,nil表示检查删除
|
||||||
// 返回: []byte 修改后的原文;error 解析、属性编码或被引用错误。
|
// 返回: []byte 修改后的原文;error 解析、属性编码或被引用错误
|
||||||
func editorBookmarkReferenceXML(data []byte, file, name string, next *Bookmark) ([]byte, error) {
|
func editorBookmarkReferenceXML(data []byte, file, name string, next *Bookmark) ([]byte, error) {
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXML(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
+22
-22
@@ -23,8 +23,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LayerInfo 描述普通页面图层,Order为同类型零基顺序,ObjectCount包含页块中的图元。
|
// LayerInfo 描述普通页面图层,Order为同类型零基顺序,ObjectCount包含页块中的图元
|
||||||
// ObjectIDs按层内文件顺序列出图元标识,包含PageBlock成员但不展开复合图元,空层返回空切片。
|
// ObjectIDs按层内文件顺序列出图元标识,包含PageBlock成员但不展开复合图元,空层返回空切片
|
||||||
type LayerInfo struct {
|
type LayerInfo struct {
|
||||||
ID string
|
ID string
|
||||||
Type string
|
Type string
|
||||||
@@ -34,7 +34,7 @@ type LayerInfo struct {
|
|||||||
ObjectIDs []string
|
ObjectIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layers 按文件顺序返回普通页面图层,省略的类型规范化为Body,不包含模板图层。
|
// Layers 按文件顺序返回普通页面图层,省略的类型规范化为Body,不包含模板图层
|
||||||
// 入参: page 零基页面索引
|
// 入参: page 零基页面索引
|
||||||
// 返回: []LayerInfo 独立图层清单, error 页面读取错误
|
// 返回: []LayerInfo 独立图层清单, error 页面读取错误
|
||||||
func (e *Editor) Layers(page int) ([]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
|
return editorLayerInfos(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layers 直接读取普通页面图层,不创建编辑器或分配新标识,返回值不持有页面可变数据。
|
// Layers 直接读取普通页面图层,不创建编辑器或分配新标识,返回值不持有页面可变数据
|
||||||
// 入参: page 零基页面索引
|
// 入参: page 零基页面索引
|
||||||
// 返回: []LayerInfo 按文件顺序排列的图层, error 页面读取错误
|
// 返回: []LayerInfo 按文件顺序排列的图层, error 页面读取错误
|
||||||
func (r *Reader) Layers(page int) ([]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
|
return editorLayerInfos(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorLayerInfos 将页面图层投影为不共享可变容器的管理清单。
|
// editorLayerInfos 将页面图层投影为不共享可变容器的管理清单
|
||||||
// 入参: p 已读取的页面
|
// 入参: p 已读取的页面
|
||||||
// 返回: []LayerInfo 图层清单
|
// 返回: []LayerInfo 图层清单
|
||||||
func editorLayerInfos(p *PageContent) []LayerInfo {
|
func editorLayerInfos(p *PageContent) []LayerInfo {
|
||||||
@@ -74,7 +74,7 @@ func editorLayerInfos(p *PageContent) []LayerInfo {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddLayer 追加空图层并返回新标识,空类型采用Body,空DrawParam表示不设置继承参数。
|
// AddLayer 追加空图层并返回新标识,空类型采用Body,空DrawParam表示不设置继承参数
|
||||||
// 入参: page 页面索引, layerType 图层类型, drawParam 绘制参数标识
|
// 入参: page 页面索引, layerType 图层类型, drawParam 绘制参数标识
|
||||||
// 返回: string 新图层标识, error 类型、资源或结构错误
|
// 返回: string 新图层标识, error 类型、资源或结构错误
|
||||||
func (e *Editor) AddLayer(page int, layerType, drawParam string) (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
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLayerType 设置图层类型,不改变图元坐标及绘制参数;同类型顺序由文件位置决定。
|
// SetLayerType 设置图层类型,不改变图元坐标及绘制参数;同类型顺序由文件位置决定
|
||||||
// 入参: page 页面索引, id 图层标识, layerType 图层类型
|
// 入参: page 页面索引, id 图层标识, layerType 图层类型
|
||||||
// 返回: error 标识、类型或结构错误
|
// 返回: error 标识、类型或结构错误
|
||||||
func (e *Editor) SetLayerType(page int, id, layerType string) 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 绘制参数标识
|
// 入参: page 页面索引, id 图层标识, drawParam 绘制参数标识
|
||||||
// 返回: error 标识、资源或结构错误
|
// 返回: error 标识、资源或结构错误
|
||||||
func (e *Editor) SetLayerDrawParam(page int, id, drawParam string) 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 同类型目标顺序
|
// 入参: page 页面索引, id 图层标识, order 同类型目标顺序
|
||||||
// 返回: error 标识、顺序或结构错误
|
// 返回: error 标识、顺序或结构错误
|
||||||
func (e *Editor) MoveLayer(page int, id string, order int) 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 图层标识
|
// 入参: page 页面索引, id 图层标识
|
||||||
// 返回: error 标识、非空图层或结构错误
|
// 返回: error 标识、非空图层或结构错误
|
||||||
func (e *Editor) DeleteLayer(page int, id string) 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 目标图层标识
|
// 入参: page 页面索引, ids 不重复的普通图元标识, layerID 目标图层标识
|
||||||
// 返回: error 选区、继承参数或结构错误
|
// 返回: error 选区、继承参数或结构错误
|
||||||
func (e *Editor) MoveObjectsToLayer(page int, ids []string, layerID string) 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 原类型
|
// 入参: value 原类型
|
||||||
// 返回: string 有效类型
|
// 返回: string 有效类型
|
||||||
func editorLayerType(value string) string {
|
func editorLayerType(value string) string {
|
||||||
@@ -305,14 +305,14 @@ func editorLayerType(value string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// validEditorLayerType 按正文表15校验可管理的图层类型。
|
// validEditorLayerType 按正文表15校验可管理的图层类型
|
||||||
// 入参: value 类型值
|
// 入参: value 类型值
|
||||||
// 返回: bool 是否为支持的标准类型
|
// 返回: bool 是否为支持的标准类型
|
||||||
func validEditorLayerType(value string) bool {
|
func validEditorLayerType(value string) bool {
|
||||||
return value == "Body" || value == "Background" || value == "Foreground"
|
return value == "Body" || value == "Background" || value == "Foreground"
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateLayerDrawParam 在目标页面资源环境中核验完整绘制参数继承链。
|
// validateLayerDrawParam 在目标页面资源环境中核验完整绘制参数继承链
|
||||||
// 入参: page 页面索引, id 绘制参数标识
|
// 入参: page 页面索引, id 绘制参数标识
|
||||||
// 返回: error 页面或资源错误
|
// 返回: error 页面或资源错误
|
||||||
func (e *Editor) validateLayerDrawParam(page int, id string) 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// editLayerPage 在完整包快照上编辑页面原文,事务失败回滚,提交时隔离共享页面及历史版本。
|
// editLayerPage 在完整包快照上编辑页面原文,事务失败回滚,提交时隔离共享页面及历史版本
|
||||||
// 入参: page 页面索引, edit 页面原文修改回调
|
// 入参: page 页面索引, edit 页面原文修改回调
|
||||||
// 返回: error 读取、修改或提交错误
|
// 返回: error 读取、修改或提交错误
|
||||||
func (e *Editor) editLayerPage(page int, edit func(*Reader, []byte, *editorXML) ([]byte, error)) 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 子元素名称
|
// 入参: root 容器节点, kind 子元素名称
|
||||||
// 返回: []*editorXML 文件顺序节点清单
|
// 返回: []*editorXML 文件顺序节点清单
|
||||||
func editorStructureChildren(root *editorXML, kind string) []*editorXML {
|
func editorStructureChildren(root *editorXML, kind string) []*editorXML {
|
||||||
@@ -395,7 +395,7 @@ func editorStructureChildren(root *editorXML, kind string) []*editorXML {
|
|||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorFindStructureID 按数值标识查找唯一直接子元素,拒绝缺失及歧义标识。
|
// editorFindStructureID 按数值标识查找唯一直接子元素,拒绝缺失及歧义标识
|
||||||
// 入参: root 容器节点, kind 元素名称, attribute 标识属性名, id 标识值
|
// 入参: root 容器节点, kind 元素名称, attribute 标识属性名, id 标识值
|
||||||
// 返回: *editorXML 唯一节点, error 标识错误
|
// 返回: *editorXML 唯一节点, error 标识错误
|
||||||
func editorFindStructureID(root *editorXML, kind, attribute, id string) (*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
|
return found, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorStructureAttribute 局部替换或删除无命名空间属性,保留扩展及其余原文。
|
// editorStructureAttribute 局部替换或删除无命名空间属性,保留扩展及其余原文
|
||||||
// 入参: data 原文, node 目标节点, name 属性名, value 新值,空值删除属性
|
// 入参: data 原文, node 目标节点, name 属性名, value 新值,空值删除属性
|
||||||
// 返回: []byte 更新后的完整原文, error XML错误
|
// 返回: []byte 更新后的完整原文, error XML错误
|
||||||
func editorStructureAttribute(data []byte, node *editorXML, name, value string) ([]byte, error) {
|
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
|
return editorPatchXML(data, []editorXMLPatch{patch}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorAppendStructure 向普通或自闭合容器追加已绑定命名空间的元素。
|
// editorAppendStructure 向普通或自闭合容器追加已绑定命名空间的元素
|
||||||
// 入参: data 原文, node 容器节点, fragment 待追加片段
|
// 入参: data 原文, node 容器节点, fragment 待追加片段
|
||||||
// 返回: []byte 更新后的完整原文
|
// 返回: []byte 更新后的完整原文
|
||||||
func editorAppendStructure(data []byte, node *editorXML, fragment []byte) []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}})
|
return editorPatchXML(data, []editorXMLPatch{{node.close, node.close, fragment}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorMoveStructure 在指定同类节点位置间移动完整原文,不触及夹杂的扩展元素。
|
// editorMoveStructure 在指定同类节点位置间移动完整原文,不触及夹杂的扩展元素
|
||||||
// 入参: data 原文, nodes 同类节点, node 被移动节点, order 目标零基顺序
|
// 入参: data 原文, nodes 同类节点, node 被移动节点, order 目标零基顺序
|
||||||
// 返回: []byte 更新后的完整原文, error 顺序错误
|
// 返回: []byte 更新后的完整原文, error 顺序错误
|
||||||
func editorMoveStructure(data []byte, nodes []*editorXML, node *editorXML, order int) ([]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
|
return editorPatchXML(data, patches), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitLayerTemplateParts 持久化结构与已注册资源,隔离共享文件并恢复未变图元的会话编辑状态。
|
// commitLayerTemplateParts 持久化结构与已注册资源,隔离共享文件并恢复未变图元的会话编辑状态
|
||||||
// 入参: reader 当前独立包快照, parts 文件修改集
|
// 入参: reader 当前独立包快照, parts 文件修改集
|
||||||
// 返回: error 资源、隔离或提交错误
|
// 返回: error 资源、隔离或提交错误
|
||||||
func (e *Editor) commitLayerTemplateParts(reader *Reader, parts map[string][]byte) error {
|
func (e *Editor) commitLayerTemplateParts(reader *Reader, parts map[string][]byte) error {
|
||||||
|
|||||||
+27
-27
@@ -28,13 +28,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// richTextTagType 标识富文本编辑标引,正文使用标准复合资源和图元。
|
// richTextTagType 标识富文本编辑标引,正文使用标准复合资源和图元
|
||||||
const richTextTagType = "OFDGo.RichText"
|
const richTextTagType = "OFDGo.RichText"
|
||||||
|
|
||||||
// richTextNamespace 标识富文本编辑语义的命名空间,版本仅随不兼容的语义变更升级。
|
// richTextNamespace 标识富文本编辑语义的命名空间,版本仅随不兼容的语义变更升级
|
||||||
const richTextNamespace = "https://ofdgo.aite.me/ns/rich-text/1"
|
const richTextNamespace = "https://ofdgo.aite.me/ns/rich-text/1"
|
||||||
|
|
||||||
// richTextMetadata 不保存字体编号,以标准主文字对象序号恢复当前文档字体。
|
// richTextMetadata 不保存字体编号,以标准主文字对象序号恢复当前文档字体
|
||||||
type richTextMetadata struct {
|
type richTextMetadata struct {
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
Value RichText `json:"value"`
|
Value RichText `json:"value"`
|
||||||
@@ -45,14 +45,14 @@ type richTextMetadata struct {
|
|||||||
Digest string `json:"digest"`
|
Digest string `json:"digest"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextEntry 保存标引入口及延迟报告的损坏诊断。
|
// richTextEntry 保存标引入口及延迟报告的损坏诊断
|
||||||
type richTextEntry struct {
|
type richTextEntry struct {
|
||||||
index int
|
index int
|
||||||
metadata richTextMetadata
|
metadata richTextMetadata
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextIndex 缓存当前文档修订的标引,不创建页面预览快照。
|
// richTextIndex 缓存当前文档修订的标引,不创建页面预览快照
|
||||||
type richTextIndex struct {
|
type richTextIndex struct {
|
||||||
source *editorSource
|
source *editorSource
|
||||||
revision uint64
|
revision uint64
|
||||||
@@ -60,7 +60,7 @@ type richTextIndex struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddRichText 原子添加标准组合及持久化编辑标引,成功时只产生一条历史。
|
// AddRichText 原子添加标准组合及持久化编辑标引,成功时只产生一条历史
|
||||||
// 入参: page 页面索引, box 页面编辑框, value 富文本内容
|
// 入参: page 页面索引, box 页面编辑框, value 富文本内容
|
||||||
// 返回: string 组合对象标识, error 排版、资源或持久化错误
|
// 返回: string 组合对象标识, error 排版、资源或持久化错误
|
||||||
func (e *Editor) AddRichText(page int, box Box, value RichText) (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
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRichText 原子替换富文本内容,保留对象标识、局部编辑框及页面变换。
|
// UpdateRichText 原子替换富文本内容,保留对象标识、局部编辑框及页面变换
|
||||||
// 安全普通文字显式重排后提升为组合;不修改共享的旧组合资源。
|
// 安全普通文字显式重排后提升为组合;不修改共享的旧组合资源
|
||||||
// 入参: page 页面索引, id 对象标识, value 完整富文本
|
// 入参: page 页面索引, id 对象标识, value 完整富文本
|
||||||
// 返回: error 识别、排版或提交错误
|
// 返回: error 识别、排版或提交错误
|
||||||
func (e *Editor) UpdateRichText(page int, id string, value RichText) error {
|
func (e *Editor) UpdateRichText(page int, id string, value RichText) error {
|
||||||
return e.updateRichTextFrame(page, id, value, 0, 0)
|
return e.updateRichTextFrame(page, id, value, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放。
|
// ResizeRichTextFrame 调整局部左边界与段落宽度并原子重排,保留页面方向和缩放
|
||||||
// 入参: page 页面索引, id 对象标识, offset 局部横向偏移, width 新的局部段落宽度
|
// 入参: page 页面索引, id 对象标识, offset 局部横向偏移, width 新的局部段落宽度
|
||||||
// 返回: error 尺寸、排版或提交错误
|
// 返回: error 尺寸、排版或提交错误
|
||||||
func (e *Editor) ResizeRichTextFrame(page int, id string, offset, width float64) 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)
|
return e.updateRichTextFrame(page, id, value, offset, width)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateRichTextFrame 统一提交正文或编辑框变化,width为零时保持原始宽度。
|
// updateRichTextFrame 统一提交正文或编辑框变化,width为零时保持原始宽度
|
||||||
// 入参: page 页面索引, id 对象标识, value 新内容, offset 局部偏移, width 可选宽度
|
// 入参: page 页面索引, id 对象标识, value 新内容, offset 局部偏移, width 可选宽度
|
||||||
// 返回: error 校验或提交错误
|
// 返回: error 校验或提交错误
|
||||||
func (e *Editor) updateRichTextFrame(page int, id string, value RichText, offset, width float64) 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 对象标识
|
// 入参: page 页面索引, id 对象标识
|
||||||
// 返回: error 识别或删除错误
|
// 返回: error 识别或删除错误
|
||||||
func (e *Editor) DeleteRichText(page int, id string) 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 对象标识
|
// 入参: page 页面索引, id 对象标识
|
||||||
// 返回: RichText 编辑内容, error 识别或持久化错误
|
// 返回: RichText 编辑内容, error 识别或持久化错误
|
||||||
func (e *Editor) RichText(page int, id string) (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
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RichTextFrame 返回原始局部编辑框及局部到页面的完整变换,不按组合缩放重新解释宽度。
|
// RichTextFrame 返回原始局部编辑框及局部到页面的完整变换,不按组合缩放重新解释宽度
|
||||||
// 入参: page 页面索引, id 富文本或安全普通文字对象标识
|
// 入参: page 页面索引, id 富文本或安全普通文字对象标识
|
||||||
// 返回: Box 局部编辑框, Matrix 页面变换, error 识别或几何错误
|
// 返回: Box 局部编辑框, Matrix 页面变换, error 识别或几何错误
|
||||||
func (e *Editor) RichTextFrame(page int, id string) (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
|
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 富文本
|
// 入参: box 页面或局部编辑框, value 富文本
|
||||||
// 返回: GraphicObject 标准组合, richTextMetadata 标引, error 准备错误
|
// 返回: GraphicObject 标准组合, richTextMetadata 标引, error 准备错误
|
||||||
func (e *Editor) prepareRichText(box Box, value RichText) (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
|
return object, meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextPlacement 根据局部逻辑坐标确定物理组合边界,负向装饰不会被资源裁剪。
|
// richTextPlacement 根据局部逻辑坐标确定物理组合边界,负向装饰不会被资源裁剪
|
||||||
// 入参: meta 编辑框与资源尺寸, matrix 逻辑坐标到页面变换
|
// 入参: meta 编辑框与资源尺寸, matrix 逻辑坐标到页面变换
|
||||||
// 返回: CompositeGraphicUnit 组合几何
|
// 返回: CompositeGraphicUnit 组合几何
|
||||||
func richTextPlacement(meta richTextMetadata, matrix 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()}
|
return CompositeGraphicUnit{Boundary: editorBoxString(box), CTM: TranslationMatrix(-box.X, -box.Y).Multiply(matrix).String()}
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextDigest 对已编码图元计算不含字体、颜色空间和字形编号的内容摘要。
|
// richTextDigest 对已编码图元计算不含字体、颜色空间和字形编号的内容摘要
|
||||||
// 入参: objects 标准子对象
|
// 入参: objects 标准子对象
|
||||||
// 返回: string 稳定内容摘要
|
// 返回: string 稳定内容摘要
|
||||||
func richTextDigest(objects []GraphicObject) string {
|
func richTextDigest(objects []GraphicObject) string {
|
||||||
@@ -363,7 +363,7 @@ func richTextDigest(objects []GraphicObject) string {
|
|||||||
return hex.EncodeToString(hash[:])
|
return hex.EncodeToString(hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextEntry 从当前文档标引缓存定位对象,损坏项不降级为普通组合。
|
// richTextEntry 从当前文档标引缓存定位对象,损坏项不降级为普通组合
|
||||||
// 入参: page 页面索引, id 组合标识
|
// 入参: page 页面索引, id 组合标识
|
||||||
// 返回: richTextEntry 标引, error 缺失或损坏错误
|
// 返回: richTextEntry 标引, error 缺失或损坏错误
|
||||||
func (e *Editor) richTextEntry(page int, id string) (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
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextMetadataIndex 按来源和修订缓存标引,仅读取标引文件,不生成Reader快照。
|
// richTextMetadataIndex 按来源和修订缓存标引,仅读取标引文件,不生成Reader快照
|
||||||
// 返回: *richTextIndex 只读缓存
|
// 返回: *richTextIndex 只读缓存
|
||||||
func (e *Editor) richTextMetadataIndex() *richTextIndex {
|
func (e *Editor) richTextMetadataIndex() *richTextIndex {
|
||||||
if index := e.richIndex; index != nil && index.source == e.source && index.revision == e.revision {
|
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
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
// putRichTextMetadata 添加或替换带有标准标引引用的富文本语义。
|
// putRichTextMetadata 添加或替换带有标准标引引用的富文本语义
|
||||||
// 入参: page 页面索引, id 对象标识, meta 编辑信息, index 原入口或-1
|
// 入参: page 页面索引, id 对象标识, meta 编辑信息, index 原入口或-1
|
||||||
// 返回: error 编码或提交错误
|
// 返回: error 编码或提交错误
|
||||||
func (e *Editor) putRichTextMetadata(page int, id string, meta richTextMetadata, index int) 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// promoteRichText 仅提升已有明确排版或可验证单行定位的普通文字。
|
// promoteRichText 仅提升已有明确排版或可验证单行定位的普通文字
|
||||||
// 入参: page 页面索引, object 普通文字快照
|
// 入参: page 页面索引, object 普通文字快照
|
||||||
// 返回: RichText 单片段, error 不安全提升错误
|
// 返回: RichText 单片段, error 不安全提升错误
|
||||||
func (e *Editor) promoteRichText(page int, object GraphicObject) (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()
|
return result, result.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// richPlainOffset 保存可提升单行原文的基线偏移,不推断复杂定位布局。
|
// richPlainOffset 保存可提升单行原文的基线偏移,不推断复杂定位布局
|
||||||
// 入参: text 已校验的普通文字
|
// 入参: text 已校验的普通文字
|
||||||
// 返回: float64 横向偏移, float64 基线偏移, error 无效坐标
|
// 返回: float64 横向偏移, float64 基线偏移, error 无效坐标
|
||||||
func (e *Editor) richPlainOffset(text TextObject) (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
|
return x, y - baseline, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyRichTextMetadata 复制已识别组合的语义,使用目标标准子对象的字体引用。
|
// copyRichTextMetadata 复制已识别组合的语义,使用目标标准子对象的字体引用
|
||||||
// 入参: source 来源编辑器, sourcePage 来源页, ids 来源标识, page 目标页, copied 目标标识
|
// 入参: source 来源编辑器, sourcePage 来源页, ids 来源标识, page 目标页, copied 目标标识
|
||||||
// 返回: error 元数据损坏或提交错误
|
// 返回: error 元数据损坏或提交错误
|
||||||
func (e *Editor) copyRichTextMetadata(source *Editor, sourcePage int, ids []string, page int, copied []string) 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextColors 为自产RGB颜色指定文档内有效颜色空间,不改变默认颜色空间。
|
// richTextColors 为自产RGB颜色指定文档内有效颜色空间,不改变默认颜色空间
|
||||||
// 入参: object 排版生成的标准图元
|
// 入参: object 排版生成的标准图元
|
||||||
// 返回: error 颜色或资源错误
|
// 返回: error 颜色或资源错误
|
||||||
func (e *Editor) richTextColors(object *GraphicObject) error {
|
func (e *Editor) richTextColors(object *GraphicObject) error {
|
||||||
@@ -612,7 +612,7 @@ func (e *Editor) richTextColors(object *GraphicObject) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// copiedRichTextMetadata 检查当前文档对象快照的标引,不将外部同号对象误认成本地对象。
|
// copiedRichTextMetadata 检查当前文档对象快照的标引,不将外部同号对象误认成本地对象
|
||||||
// 入参: objects 待复制快照
|
// 入参: objects 待复制快照
|
||||||
// 返回: map[int]richTextMetadata 需要复制的语义, error 损坏诊断
|
// 返回: map[int]richTextMetadata 需要复制的语义, error 损坏诊断
|
||||||
func (e *Editor) copiedRichTextMetadata(objects []GraphicObject) (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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// attachRichTextSnapshot 为普通对象快照附加不可变编辑语义,复制后不依赖原对象继续存在。
|
// attachRichTextSnapshot 为普通对象快照附加不可变编辑语义,复制后不依赖原对象继续存在
|
||||||
// 入参: page 页面索引, object 对象快照
|
// 入参: page 页面索引, object 对象快照
|
||||||
func (e *Editor) attachRichTextSnapshot(page int, object *GraphicObject) {
|
func (e *Editor) attachRichTextSnapshot(page int, object *GraphicObject) {
|
||||||
object.richText = nil
|
object.richText = nil
|
||||||
@@ -683,7 +683,7 @@ func (e *Editor) attachRichTextSnapshot(page int, object *GraphicObject) {
|
|||||||
object.richText = &meta
|
object.richText = &meta
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextTagsFontSafe 仅允许完整识别且不含资源编号的本库标引通过字体重绑定检查。
|
// richTextTagsFontSafe 仅允许完整识别且不含资源编号的本库标引通过字体重绑定检查
|
||||||
// 入参: ctx 取消上下文, reader 包阅读器, index 文档索引
|
// 入参: ctx 取消上下文, reader 包阅读器, index 文档索引
|
||||||
// 返回: bool 是否可以排除字体引用, error 读取或取消错误
|
// 返回: bool 是否可以排除字体引用, error 读取或取消错误
|
||||||
func richTextTagsFontSafe(ctx context.Context, reader *Reader, index int) (bool, error) {
|
func richTextTagsFontSafe(ctx context.Context, reader *Reader, index int) (bool, error) {
|
||||||
|
|||||||
+25
-25
@@ -21,15 +21,15 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemplateReference 普通页面的模板引用,保留原始模板标识及绑定层次。
|
// TemplateReference 普通页面的模板引用,保留原始模板标识及绑定层次
|
||||||
// PageIndex为当前文档的零基页面索引,PageID为页面标识;ZOrder为空表示沿用模板默认层次。
|
// PageIndex为当前文档的零基页面索引,PageID为页面标识;ZOrder为空表示沿用模板默认层次
|
||||||
type TemplateReference struct {
|
type TemplateReference struct {
|
||||||
PageIndex int
|
PageIndex int
|
||||||
PageID string
|
PageID string
|
||||||
Template
|
Template
|
||||||
}
|
}
|
||||||
|
|
||||||
// Templates 返回当前文档及所选版本的模板定义副本,省略的ZOrder返回Background。
|
// Templates 返回当前文档及所选版本的模板定义副本,省略的ZOrder返回Background
|
||||||
// 返回: []TemplatePage 独立模板清单, error 文档读取错误
|
// 返回: []TemplatePage 独立模板清单, error 文档读取错误
|
||||||
func (e *Editor) Templates() ([]TemplatePage, error) {
|
func (e *Editor) Templates() ([]TemplatePage, error) {
|
||||||
if e.source == nil {
|
if e.source == nil {
|
||||||
@@ -42,7 +42,7 @@ func (e *Editor) Templates() ([]TemplatePage, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Templates 直接返回当前文档及版本的模板清单,不创建编辑器,省略的ZOrder返回Background。
|
// Templates 直接返回当前文档及版本的模板清单,不创建编辑器,省略的ZOrder返回Background
|
||||||
// 返回: []TemplatePage 独立模板定义副本, error 文档读取错误
|
// 返回: []TemplatePage 独立模板定义副本, error 文档读取错误
|
||||||
func (r *Reader) Templates() ([]TemplatePage, error) {
|
func (r *Reader) Templates() ([]TemplatePage, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.Doc()
|
||||||
@@ -56,7 +56,7 @@ func (r *Reader) Templates() ([]TemplatePage, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageTemplates 按文件顺序返回普通页面模板引用,ZOrder为空表示沿用模板默认层次。
|
// PageTemplates 按文件顺序返回普通页面模板引用,ZOrder为空表示沿用模板默认层次
|
||||||
// 入参: page 零基页面索引
|
// 入参: page 零基页面索引
|
||||||
// 返回: []Template 独立引用清单, error 页面读取错误
|
// 返回: []Template 独立引用清单, error 页面读取错误
|
||||||
func (e *Editor) PageTemplates(page int) ([]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
|
return append([]Template{}, p.Template...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateReferences 返回当前文档及所选版本全部普通页面的模板引用副本,不包含模板页自身引用。
|
// TemplateReferences 返回当前文档及所选版本全部普通页面的模板引用副本,不包含模板页自身引用
|
||||||
// 清单按页面索引及页内引用文件顺序排列,无引用时返回空切片,读取失败时不返回部分结果。
|
// 清单按页面索引及页内引用文件顺序排列,无引用时返回空切片,读取失败时不返回部分结果
|
||||||
// 返回: []TemplateReference 引用定位及绑定层次清单, error 页面读取错误
|
// 返回: []TemplateReference 引用定位及绑定层次清单, error 页面读取错误
|
||||||
func (e *Editor) TemplateReferences() ([]TemplateReference, error) {
|
func (e *Editor) TemplateReferences() ([]TemplateReference, error) {
|
||||||
result := []TemplateReference{}
|
result := []TemplateReference{}
|
||||||
@@ -84,8 +84,8 @@ func (e *Editor) TemplateReferences() ([]TemplateReference, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateReferences 只读枚举当前文档及所选版本全部普通页面的模板引用,不创建编辑器或遍历其他文档、版本。
|
// TemplateReferences 只读枚举当前文档及所选版本全部普通页面的模板引用,不创建编辑器或遍历其他文档、版本
|
||||||
// 返回独立副本,按页面索引及页内引用文件顺序排列;无引用时返回空切片,读取失败时不返回部分结果。
|
// 返回独立副本,按页面索引及页内引用文件顺序排列;无引用时返回空切片,读取失败时不返回部分结果
|
||||||
// 返回: []TemplateReference 引用定位及绑定层次清单, error 文档或页面读取错误
|
// 返回: []TemplateReference 引用定位及绑定层次清单, error 文档或页面读取错误
|
||||||
func (r *Reader) TemplateReferences() ([]TemplateReference, error) {
|
func (r *Reader) TemplateReferences() ([]TemplateReference, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.Doc()
|
||||||
@@ -105,7 +105,7 @@ func (r *Reader) TemplateReferences() ([]TemplateReference, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindTemplate 在页面末尾绑定模板,空ZOrder沿用模板默认层次,重复绑定拒绝。
|
// BindTemplate 在页面末尾绑定模板,空ZOrder沿用模板默认层次,重复绑定拒绝
|
||||||
// 入参: page 页面索引, id 模板标识, zOrder 页面覆盖层次
|
// 入参: page 页面索引, id 模板标识, zOrder 页面覆盖层次
|
||||||
// 返回: error 标识、层次或重复绑定错误
|
// 返回: error 标识、层次或重复绑定错误
|
||||||
func (e *Editor) BindTemplate(page int, id, zOrder string) 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 模板标识
|
// 入参: page 页面索引, id 模板标识
|
||||||
// 返回: error 引用或结构错误
|
// 返回: error 引用或结构错误
|
||||||
func (e *Editor) UnbindTemplate(page int, id string) 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 页面覆盖层次
|
// 入参: page 页面索引, id 模板标识, zOrder 页面覆盖层次
|
||||||
// 返回: error 引用、层次或结构错误
|
// 返回: error 引用、层次或结构错误
|
||||||
func (e *Editor) SetPageTemplateZOrder(page int, id, zOrder string) 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 全部引用中的目标顺序
|
// 入参: page 页面索引, id 模板标识, order 全部引用中的目标顺序
|
||||||
// 返回: error 引用、顺序或结构错误
|
// 返回: error 引用、顺序或结构错误
|
||||||
func (e *Editor) MovePageTemplate(page int, id string, order int) 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。
|
// CreateTemplateFromPage 复制指定普通页面自身内容为模板,不改变源页或自动绑定,空层次采用Background
|
||||||
// 复用页面复制的标识重映射并保留原XML及资源;模板引用不递归复制,注解和签章不属于模板内容。
|
// 复用页面复制的标识重映射并保留原XML及资源;模板引用不递归复制,注解和签章不属于模板内容
|
||||||
// 模板编辑可使用普通页面副本及现有编辑接口,再通过ReplaceTemplateFromPage保持模板身份提交。
|
// 模板编辑可使用普通页面副本及现有编辑接口,再通过ReplaceTemplateFromPage保持模板身份提交
|
||||||
// 入参: page 源页面索引, name 模板名称, zOrder 默认前景背景层次
|
// 入参: page 源页面索引, name 模板名称, zOrder 默认前景背景层次
|
||||||
// 返回: string 新模板标识, error 页面、标识重映射或提交错误
|
// 返回: string 新模板标识, error 页面、标识重映射或提交错误
|
||||||
func (e *Editor) CreateTemplateFromPage(page int, name, zOrder string) (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
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenameTemplate 修改当前文档模板名称,空字符串清空名称,不修改共享模板内容。
|
// RenameTemplate 修改当前文档模板名称,空字符串清空名称,不修改共享模板内容
|
||||||
// 入参: id 模板标识, name 模板名称
|
// 入参: id 模板标识, name 模板名称
|
||||||
// 返回: error 标识或结构错误
|
// 返回: error 标识或结构错误
|
||||||
func (e *Editor) RenameTemplate(id, name string) 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 默认前景背景层次
|
// 入参: id 模板标识, zOrder 默认前景背景层次
|
||||||
// 返回: error 标识、层次或结构错误
|
// 返回: error 标识、层次或结构错误
|
||||||
func (e *Editor) SetTemplateZOrder(id, zOrder string) 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 普通源页面索引
|
// 入参: id 目标模板标识, page 普通源页面索引
|
||||||
// 返回: error 标识、复制或提交错误
|
// 返回: error 标识、复制或提交错误
|
||||||
func (e *Editor) ReplaceTemplateFromPage(id string, page int) 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 模板标识
|
// 入参: id 模板标识
|
||||||
// 返回: error 标识、被引用或结构错误
|
// 返回: error 标识、被引用或结构错误
|
||||||
func (e *Editor) DeleteTemplate(id string) error {
|
func (e *Editor) DeleteTemplate(id string) error {
|
||||||
@@ -406,7 +406,7 @@ func (e *Editor) DeleteTemplate(id string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorTemplateOrder 将省略的模板默认层次规范化为Background。
|
// editorTemplateOrder 将省略的模板默认层次规范化为Background
|
||||||
// 入参: value 原始层次
|
// 入参: value 原始层次
|
||||||
// 返回: string 有效默认层次
|
// 返回: string 有效默认层次
|
||||||
func editorTemplateOrder(value string) string {
|
func editorTemplateOrder(value string) string {
|
||||||
@@ -416,14 +416,14 @@ func editorTemplateOrder(value string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// validEditorTemplateOrder 校验模板前景背景枚举及未指定状态。
|
// validEditorTemplateOrder 校验模板前景背景枚举及未指定状态
|
||||||
// 入参: value 层次值
|
// 入参: value 层次值
|
||||||
// 返回: bool 是否有效
|
// 返回: bool 是否有效
|
||||||
func validEditorTemplateOrder(value string) bool {
|
func validEditorTemplateOrder(value string) bool {
|
||||||
return value == "" || value == "Background" || value == "Foreground"
|
return value == "" || value == "Background" || value == "Foreground"
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorTemplateDefinition 查找当前阅读器中的唯一模板定义,拒绝缺失及歧义标识。
|
// editorTemplateDefinition 查找当前阅读器中的唯一模板定义,拒绝缺失及歧义标识
|
||||||
// 入参: reader 当前阅读器, id 模板标识
|
// 入参: reader 当前阅读器, id 模板标识
|
||||||
// 返回: TemplatePage 定义副本, error 文档或标识错误
|
// 返回: TemplatePage 定义副本, error 文档或标识错误
|
||||||
func editorTemplateDefinition(reader *Reader, id string) (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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editTemplateDefinition 在事务快照中局部修改模板声明,保留命名空间、扩展及其他版本。
|
// editTemplateDefinition 在事务快照中局部修改模板声明,保留命名空间、扩展及其他版本
|
||||||
// 入参: id 模板标识, edit 文档原文修改回调
|
// 入参: id 模板标识, edit 文档原文修改回调
|
||||||
// 返回: error 读取、修改或提交错误
|
// 返回: error 读取、修改或提交错误
|
||||||
func (e *Editor) editTemplateDefinition(id string, edit func(*Reader, []byte, *editorXML) ([]byte, error)) 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"}})
|
report.Issues = append(report.Issues, InvoiceIssue{Code: "read-error", Message: err.Error(), Source: InvoiceSource{Kind: "tag"}})
|
||||||
}
|
}
|
||||||
for i, tag := range tags {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if err := read(InvoiceSource{Kind: "tag", ID: strconv.Itoa(i), FileLoc: tag.FileLoc}); err != nil {
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// invoiceXBRL 表示XBRL实例命名空间。
|
// invoiceXBRL 表示XBRL实例命名空间
|
||||||
const invoiceXBRL = "http://www.xbrl.org/2003/instance"
|
const invoiceXBRL = "http://www.xbrl.org/2003/instance"
|
||||||
|
|
||||||
// invoiceXSI 表示XML Schema实例命名空间。
|
// invoiceXSI 表示XML Schema实例命名空间
|
||||||
const invoiceXSI = "http://www.w3.org/2001/XMLSchema-instance"
|
const invoiceXSI = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
|
||||||
// invoiceDecimalPattern 仅接受XML十进制词法。
|
// invoiceDecimalPattern 仅接受XML十进制词法
|
||||||
var invoiceDecimalPattern = regexp.MustCompile(`^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$`)
|
var invoiceDecimalPattern = regexp.MustCompile(`^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$`)
|
||||||
|
|
||||||
// invoiceXMLNode 保存有界XML节点及原始文本切片。
|
// invoiceXMLNode 保存有界XML节点及原始文本切片
|
||||||
type invoiceXMLNode struct {
|
type invoiceXMLNode struct {
|
||||||
name xml.Name
|
name xml.Name
|
||||||
attrs []xml.Attr
|
attrs []xml.Attr
|
||||||
@@ -52,23 +52,23 @@ type invoiceXMLNode struct {
|
|||||||
parts []invoiceXMLPart
|
parts []invoiceXMLPart
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceXMLPart 保留混合内容顺序,供类型化维度规范化使用。
|
// invoiceXMLPart 保留混合内容顺序,供类型化维度规范化使用
|
||||||
type invoiceXMLPart struct {
|
type invoiceXMLPart struct {
|
||||||
text string
|
text string
|
||||||
child *invoiceXMLNode
|
child *invoiceXMLNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceDecimalEqual 精确比较十进制词法值,不接受指数或分数。
|
// invoiceDecimalEqual 精确比较十进制词法值,不接受指数或分数
|
||||||
// 入参: a、b 原始十进制文本。
|
// 入参: a、b 原始十进制文本
|
||||||
// 返回: bool 两个合法值是否相等。
|
// 返回: bool 两个合法值是否相等
|
||||||
func invoiceDecimalEqual(a, b string) bool {
|
func invoiceDecimalEqual(a, b string) bool {
|
||||||
x, y := invoiceDecimal(a), invoiceDecimal(b)
|
x, y := invoiceDecimal(a), invoiceDecimal(b)
|
||||||
return x != nil && y != nil && x.Cmp(y) == 0
|
return x != nil && y != nil && x.Cmp(y) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceDecimal 解析有界十进制文本。
|
// invoiceDecimal 解析有界十进制文本
|
||||||
// 入参: s 原始文本。
|
// 入参: s 原始文本
|
||||||
// 返回: *big.Rat 精确数值,非法时为空。
|
// 返回: *big.Rat 精确数值,非法时为空
|
||||||
func invoiceDecimal(s string) *big.Rat {
|
func invoiceDecimal(s string) *big.Rat {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
if len(s) > 256 || !invoiceDecimalPattern.MatchString(s) {
|
if len(s) > 256 || !invoiceDecimalPattern.MatchString(s) {
|
||||||
@@ -81,9 +81,9 @@ func invoiceDecimal(s string) *big.Rat {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceXMLAttr 按命名空间读取属性。
|
// invoiceXMLAttr 按命名空间读取属性
|
||||||
// 入参: n 节点,space 命名空间,name 属性名。
|
// 入参: n 节点,space 命名空间,name 属性名
|
||||||
// 返回: string 属性值。
|
// 返回: string 属性值
|
||||||
func invoiceXMLAttr(n *invoiceXMLNode, space, name string) string {
|
func invoiceXMLAttr(n *invoiceXMLNode, space, name string) string {
|
||||||
for _, a := range n.attrs {
|
for _, a := range n.attrs {
|
||||||
if a.Name.Space == space && a.Name.Local == name {
|
if a.Name.Space == space && a.Name.Local == name {
|
||||||
@@ -93,9 +93,9 @@ func invoiceXMLAttr(n *invoiceXMLNode, space, name string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceXMLHasAttr 区分缺省属性与空字符串属性。
|
// invoiceXMLHasAttr 区分缺省属性与空字符串属性
|
||||||
// 入参: n 节点,space 命名空间,name 属性名。
|
// 入参: n 节点,space 命名空间,name 属性名
|
||||||
// 返回: 属性是否存在。
|
// 返回: 属性是否存在
|
||||||
func invoiceXMLHasAttr(n *invoiceXMLNode, space, name string) bool {
|
func invoiceXMLHasAttr(n *invoiceXMLNode, space, name string) bool {
|
||||||
for _, a := range n.attrs {
|
for _, a := range n.attrs {
|
||||||
if a.Name == (xml.Name{Space: space, Local: name}) {
|
if a.Name == (xml.Name{Space: space, Local: name}) {
|
||||||
@@ -105,9 +105,9 @@ func invoiceXMLHasAttr(n *invoiceXMLNode, space, name string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceXMLTree 读取受限XML树,不解析外部实体。
|
// invoiceXMLTree 读取受限XML树,不解析外部实体
|
||||||
// 入参: ctx 取消上下文,data 文档字节。
|
// 入参: ctx 取消上下文,data 文档字节
|
||||||
// 返回: 根节点及解析错误。
|
// 返回: 根节点及解析错误
|
||||||
func invoiceXMLTree(ctx context.Context, data []byte) (*invoiceXMLNode, error) {
|
func invoiceXMLTree(ctx context.Context, data []byte) (*invoiceXMLNode, error) {
|
||||||
if len(data) > 16<<20 {
|
if len(data) > 16<<20 {
|
||||||
return nil, fmt.Errorf("invoice XML exceeds size limit")
|
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
|
return root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceTaxonomy 识别已核实的官方命名空间。
|
// invoiceTaxonomy 识别已核实的官方命名空间
|
||||||
// 入参: ns 命名空间URI。
|
// 入参: ns 命名空间URI
|
||||||
// 返回: 票种及版本,未知时为空。
|
// 返回: 票种及版本,未知时为空
|
||||||
func invoiceTaxonomy(ns string) (string, string) {
|
func invoiceTaxonomy(ns string) (string, string) {
|
||||||
switch ns {
|
switch ns {
|
||||||
case "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/inv":
|
case "http://xbrl.mof.gov.cn/taxonomy/2023-12-31/inv":
|
||||||
@@ -234,9 +234,9 @@ func invoiceTaxonomy(ns string) (string, string) {
|
|||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseInvoiceXML 解析明确识别的发票格式,未知格式仅产生诊断。
|
// parseInvoiceXML 解析明确识别的发票格式,未知格式仅产生诊断
|
||||||
// 入参: ctx 取消上下文,data XML数据,source 来源。
|
// 入参: ctx 取消上下文,data XML数据,source 来源
|
||||||
// 返回: InvoiceReport 独立候选与诊断,error XML或限额错误。
|
// 返回: InvoiceReport 独立候选与诊断,error XML或限额错误
|
||||||
func parseInvoiceXML(ctx context.Context, data []byte, source InvoiceSource) (InvoiceReport, error) {
|
func parseInvoiceXML(ctx context.Context, data []byte, source InvoiceSource) (InvoiceReport, error) {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
@@ -498,9 +498,9 @@ func parseInvoiceXML(ctx context.Context, data []byte, source InvoiceSource) (In
|
|||||||
return report, nil
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceXMLField 映射官方分类标准明确规定的概念。
|
// invoiceXMLField 映射官方分类标准明确规定的概念
|
||||||
// 入参: inv 发票,kind 标准类别,name 概念名。
|
// 入参: inv 发票,kind 标准类别,name 概念名
|
||||||
// 返回: 对应字段地址,未映射时为空。
|
// 返回: 对应字段地址,未映射时为空
|
||||||
func invoiceXMLField(inv *Invoice, kind, name string) *string {
|
func invoiceXMLField(inv *Invoice, kind, name string) *string {
|
||||||
if kind == "inv" {
|
if kind == "inv" {
|
||||||
switch name {
|
switch name {
|
||||||
@@ -588,9 +588,9 @@ func invoiceXMLField(inv *Invoice, kind, name string) *string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceSortedNodes 为定义生成稳定顺序。
|
// invoiceSortedNodes 为定义生成稳定顺序
|
||||||
// 入参: nodes 定义索引。
|
// 入参: nodes 定义索引
|
||||||
// 返回: 排序后的标识。
|
// 返回: 排序后的标识
|
||||||
func invoiceSortedNodes(nodes map[string]*invoiceXMLNode) []string {
|
func invoiceSortedNodes(nodes map[string]*invoiceXMLNode) []string {
|
||||||
ids := make([]string, 0, len(nodes))
|
ids := make([]string, 0, len(nodes))
|
||||||
for id := range nodes {
|
for id := range nodes {
|
||||||
@@ -600,9 +600,9 @@ func invoiceSortedNodes(nodes map[string]*invoiceXMLNode) []string {
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceQName 依据节点处的前缀绑定展开QName。
|
// invoiceQName 依据节点处的前缀绑定展开QName
|
||||||
// 入参: n 节点,value QName文本。
|
// 入参: n 节点,value QName文本
|
||||||
// 返回: 展开名称及有效标志。
|
// 返回: 展开名称及有效标志
|
||||||
func invoiceQName(n *invoiceXMLNode, value string) (xml.Name, bool) {
|
func invoiceQName(n *invoiceXMLNode, value string) (xml.Name, bool) {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
parts := strings.Split(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
|
return xml.Name{Space: ns, Local: local}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceNCName 验证XML 1.0不含冒号的名称。
|
// invoiceNCName 验证XML 1.0不含冒号的名称
|
||||||
// 入参: name 名称文本。
|
// 入参: name 名称文本
|
||||||
// 返回: 是否满足NCName词法。
|
// 返回: 是否满足NCName词法
|
||||||
func invoiceNCName(name string) bool {
|
func invoiceNCName(name string) bool {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return false
|
return false
|
||||||
@@ -642,9 +642,9 @@ func invoiceNCName(name string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoicePeriodTime 解析标准期间的日期或日期时间。
|
// invoicePeriodTime 解析标准期间的日期或日期时间
|
||||||
// 入参: value 日期文本。
|
// 入参: value 日期文本
|
||||||
// 返回: 时刻及有效标志。
|
// 返回: 时刻及有效标志
|
||||||
func invoicePeriodTime(value string) (time.Time, bool) {
|
func invoicePeriodTime(value string) (time.Time, bool) {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
if len(value) > 64 {
|
if len(value) > 64 {
|
||||||
@@ -668,14 +668,14 @@ func invoicePeriodTime(value string) (time.Time, bool) {
|
|||||||
return time.Time{}, false
|
return time.Time{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceExpanded 返回不依赖前缀的名称表示。
|
// invoiceExpanded 返回不依赖前缀的名称表示
|
||||||
// 入参: name 展开名称。
|
// 入参: name 展开名称
|
||||||
// 返回: Clark名称。
|
// 返回: Clark名称
|
||||||
func invoiceExpanded(name xml.Name) string { return "{" + name.Space + "}" + name.Local }
|
func invoiceExpanded(name xml.Name) string { return "{" + name.Space + "}" + name.Local }
|
||||||
|
|
||||||
// invoiceCanonicalXML 以展开名称序列化类型化维度,保留混合文本顺序。
|
// invoiceCanonicalXML 以展开名称序列化类型化维度,保留混合文本顺序
|
||||||
// 入参: n 类型化维度域元素。
|
// 入参: n 类型化维度域元素
|
||||||
// 返回: 不依赖输入元素前缀的XML。
|
// 返回: 不依赖输入元素前缀的XML
|
||||||
func invoiceCanonicalXML(n *invoiceXMLNode) string {
|
func invoiceCanonicalXML(n *invoiceXMLNode) string {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
e := xml.NewEncoder(&b)
|
e := xml.NewEncoder(&b)
|
||||||
@@ -709,9 +709,9 @@ func invoiceCanonicalXML(n *invoiceXMLNode) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceChildren 按展开名称选择直接子节点。
|
// invoiceChildren 按展开名称选择直接子节点
|
||||||
// 入参: n 父节点,space 命名空间,local 本地名。
|
// 入参: n 父节点,space 命名空间,local 本地名
|
||||||
// 返回: 匹配节点。
|
// 返回: 匹配节点
|
||||||
func invoiceChildren(n *invoiceXMLNode, space, local string) []*invoiceXMLNode {
|
func invoiceChildren(n *invoiceXMLNode, space, local string) []*invoiceXMLNode {
|
||||||
var result []*invoiceXMLNode
|
var result []*invoiceXMLNode
|
||||||
for _, c := range n.children {
|
for _, c := range n.children {
|
||||||
@@ -722,9 +722,9 @@ func invoiceChildren(n *invoiceXMLNode, space, local string) []*invoiceXMLNode {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceContext 校验主体、期间及维度,不把不同上下文合并。
|
// invoiceContext 校验主体、期间及维度,不把不同上下文合并
|
||||||
// 入参: n 上下文节点。
|
// 入参: n 上下文节点
|
||||||
// 返回: 维度及有效标志。
|
// 返回: 维度及有效标志
|
||||||
func invoiceContext(n *invoiceXMLNode) ([]InvoiceDimension, bool) {
|
func invoiceContext(n *invoiceXMLNode) ([]InvoiceDimension, bool) {
|
||||||
entities, periods := invoiceChildren(n, invoiceXBRL, "entity"), invoiceChildren(n, invoiceXBRL, "period")
|
entities, periods := invoiceChildren(n, invoiceXBRL, "entity"), invoiceChildren(n, invoiceXBRL, "period")
|
||||||
if len(entities) != 1 || len(periods) != 1 {
|
if len(entities) != 1 || len(periods) != 1 {
|
||||||
@@ -788,9 +788,9 @@ func invoiceContext(n *invoiceXMLNode) ([]InvoiceDimension, bool) {
|
|||||||
return dims, valid
|
return dims, valid
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceUnit 校验单位结构并展开所有度量QName。
|
// invoiceUnit 校验单位结构并展开所有度量QName
|
||||||
// 入参: n 单位定义。
|
// 入参: n 单位定义
|
||||||
// 返回: 规范单位表达及有效标志。
|
// 返回: 规范单位表达及有效标志
|
||||||
func invoiceUnit(n *invoiceXMLNode) (string, bool) {
|
func invoiceUnit(n *invoiceXMLNode) (string, bool) {
|
||||||
measures := func(c *invoiceXMLNode) (string, bool) {
|
measures := func(c *invoiceXMLNode) (string, bool) {
|
||||||
if len(c.children) == 0 {
|
if len(c.children) == 0 {
|
||||||
@@ -823,9 +823,9 @@ func invoiceUnit(n *invoiceXMLNode) (string, bool) {
|
|||||||
return measures(n)
|
return measures(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceNumeric 判断已核实标准中的十进制概念。
|
// invoiceNumeric 判断已核实标准中的十进制概念
|
||||||
// 入参: kind 标准类别,name 概念名称。
|
// 入参: kind 标准类别,name 概念名称
|
||||||
// 返回: 是否为数值概念。
|
// 返回: 是否为数值概念
|
||||||
func invoiceNumeric(kind, name string) bool {
|
func invoiceNumeric(kind, name string) bool {
|
||||||
if name == "RecordedAmount" {
|
if name == "RecordedAmount" {
|
||||||
return true
|
return true
|
||||||
@@ -860,9 +860,9 @@ func invoiceNumeric(kind, name string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceAccuracy 校验XBRL精度属性,precision允许零。
|
// invoiceAccuracy 校验XBRL精度属性,precision允许零
|
||||||
// 入参: value 原文,nonnegative 是否要求非负整数。
|
// 入参: value 原文,nonnegative 是否要求非负整数
|
||||||
// 返回: 是否为整数或INF。
|
// 返回: 是否为整数或INF
|
||||||
func invoiceAccuracy(value string, nonnegative bool) bool {
|
func invoiceAccuracy(value string, nonnegative bool) bool {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
if value == "INF" {
|
if value == "INF" {
|
||||||
@@ -888,16 +888,16 @@ func invoiceAccuracy(value string, nonnegative bool) bool {
|
|||||||
return !nonnegative || !negative || !nonzero
|
return !nonnegative || !negative || !nonzero
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceMonetary 区分货币事实与数量、百分率。
|
// invoiceMonetary 区分货币事实与数量、百分率
|
||||||
// 入参: kind 类别,name 概念名称。
|
// 入参: kind 类别,name 概念名称
|
||||||
// 返回: 是否为已映射货币概念。
|
// 返回: 是否为已映射货币概念
|
||||||
func invoiceMonetary(kind, name string) bool {
|
func invoiceMonetary(kind, name string) bool {
|
||||||
return invoiceNumeric(kind, name) && name != "QuantityOfGoodsOrServices" && name != "VatRate" && name != "TaxRate"
|
return invoiceNumeric(kind, name) && name != "QuantityOfGoodsOrServices" && name != "VatRate" && name != "TaxRate"
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceCurrencyUnit 检查单一ISO币种度量,不猜测币种。
|
// invoiceCurrencyUnit 检查单一ISO币种度量,不猜测币种
|
||||||
// 入参: unit 展开的单位表示。
|
// 入参: unit 展开的单位表示
|
||||||
// 返回: 是否为三位大写币种度量。
|
// 返回: 是否为三位大写币种度量
|
||||||
func invoiceCurrencyUnit(unit string) bool {
|
func invoiceCurrencyUnit(unit string) bool {
|
||||||
const prefix = "{http://www.xbrl.org/2003/iso4217}"
|
const prefix = "{http://www.xbrl.org/2003/iso4217}"
|
||||||
if !strings.HasPrefix(unit, prefix) {
|
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'
|
return len(code) == 3 && code[0] >= 'A' && code[0] <= 'Z' && code[1] >= 'A' && code[1] <= 'Z' && code[2] >= 'A' && code[2] <= 'Z'
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceItemTuple 识别明细元组,不把会计分录当作商品。
|
// invoiceItemTuple 识别明细元组,不把会计分录当作商品
|
||||||
// 入参: kind 标准类别,name 元组名。
|
// 入参: kind 标准类别,name 元组名
|
||||||
// 返回: 是否为明细。
|
// 返回: 是否为明细
|
||||||
func invoiceItemTuple(kind, name string) bool {
|
func invoiceItemTuple(kind, name string) bool {
|
||||||
return kind == "atr" && name == "DetailInformationOfAirTicketTuple" || kind == "inv" && (name == "InformationOfTaxableGoodsOrServicesDetailItemsTuple" || name == "InformationOfTollFeeDetailItemsTuple")
|
return kind == "atr" && name == "DetailInformationOfAirTicketTuple" || kind == "inv" && (name == "InformationOfTaxableGoodsOrServicesDetailItemsTuple" || name == "InformationOfTollFeeDetailItemsTuple")
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceItemField 映射增值税商品明细公共字段。
|
// invoiceItemField 映射增值税商品明细公共字段
|
||||||
// 入参: item 明细,kind 类别,name 概念名。
|
// 入参: item 明细,kind 类别,name 概念名
|
||||||
// 返回: 字段地址,其他明细仅保留事实。
|
// 返回: 字段地址,其他明细仅保留事实
|
||||||
func invoiceItemField(item *InvoiceItem, kind, name string) *string {
|
func invoiceItemField(item *InvoiceItem, kind, name string) *string {
|
||||||
if kind != "inv" {
|
if kind != "inv" {
|
||||||
return nil
|
return nil
|
||||||
@@ -942,9 +942,9 @@ func invoiceItemField(item *InvoiceItem, kind, name string) *string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoiceAmountCheck 精确检查明确的价税合计关系,不推算航空费用。
|
// invoiceAmountCheck 精确检查明确的价税合计关系,不推算航空费用
|
||||||
// 入参: inv 发票,report 诊断目标,source 来源。
|
// 入参: inv 发票,report 诊断目标,source 来源
|
||||||
// 返回: 无。
|
// 返回: 无
|
||||||
func invoiceAmountCheck(inv *Invoice, report *InvoiceReport, source InvoiceSource) {
|
func invoiceAmountCheck(inv *Invoice, report *InvoiceReport, source InvoiceSource) {
|
||||||
if inv.Kind != "inv" && inv.Kind != "einv" {
|
if inv.Kind != "inv" && inv.Kind != "einv" {
|
||||||
return
|
return
|
||||||
@@ -955,9 +955,9 @@ func invoiceAmountCheck(inv *Invoice, report *InvoiceReport, source InvoiceSourc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoicePlainXML 解析官方数电发票指南中的无命名空间0.3版本。
|
// invoicePlainXML 解析官方数电发票指南中的无命名空间0.3版本
|
||||||
// 入参: ctx 上下文,root 根节点,source 来源。
|
// 入参: ctx 上下文,root 根节点,source 来源
|
||||||
// 返回: 发票及诊断,取消错误。
|
// 返回: 发票及诊断,取消错误
|
||||||
func invoicePlainXML(ctx context.Context, root *invoiceXMLNode, source InvoiceSource) (InvoiceReport, error) {
|
func invoicePlainXML(ctx context.Context, root *invoiceXMLNode, source InvoiceSource) (InvoiceReport, error) {
|
||||||
report := InvoiceReport{}
|
report := InvoiceReport{}
|
||||||
headers := invoiceChildren(root, "", "Header")
|
headers := invoiceChildren(root, "", "Header")
|
||||||
@@ -1044,9 +1044,9 @@ func invoicePlainXML(ctx context.Context, root *invoiceXMLNode, source InvoiceSo
|
|||||||
return report, nil
|
return report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoicePlainField 按官方样例中的完整路径映射字段。
|
// invoicePlainField 按官方样例中的完整路径映射字段
|
||||||
// 入参: inv 发票,item 当前明细,path 完整无命名空间路径。
|
// 入参: inv 发票,item 当前明细,path 完整无命名空间路径
|
||||||
// 返回: 字段地址及数值标志。
|
// 返回: 字段地址及数值标志
|
||||||
func invoicePlainField(inv *Invoice, item *InvoiceItem, path string) (*string, bool) {
|
func invoicePlainField(inv *Invoice, item *InvoiceItem, path string) (*string, bool) {
|
||||||
switch path {
|
switch path {
|
||||||
case "/EInvoice/TaxSupervisionInfo/InvoiceNumber":
|
case "/EInvoice/TaxSupervisionInfo/InvoiceNumber":
|
||||||
|
|||||||
+9
-9
@@ -140,7 +140,7 @@ type CompositeGraphicUnit struct {
|
|||||||
extentSet bool
|
extentSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析资源文件,隔离外部命名空间的同名集合。
|
// UnmarshalXML 解析资源文件,隔离外部命名空间的同名集合
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (res *Res) UnmarshalXML(d *xml.Decoder, start xml.StartElement) 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 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (spaces *ColorSpaces) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (spaces *ColorSpaces) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
return decodeOFDCollection(d, start, "ColorSpace", &spaces.ColorSpace)
|
return decodeOFDCollection(d, start, "ColorSpace", &spaces.ColorSpace)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析颜色空间属性及调色板。
|
// UnmarshalXML 解析颜色空间属性及调色板
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (space *ColorSpace) UnmarshalXML(d *xml.Decoder, start xml.StartElement) 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 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (fonts *Fonts) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (fonts *Fonts) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
return decodeOFDCollection(d, start, "Font", &fonts.Font)
|
return decodeOFDCollection(d, start, "Font", &fonts.Font)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析字体声明及内嵌字体路径。
|
// UnmarshalXML 解析字体声明及内嵌字体路径
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (font *Font) UnmarshalXML(d *xml.Decoder, start xml.StartElement) 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 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (media *MultiMedias) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (media *MultiMedias) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
return decodeOFDCollection(d, start, "MultiMedia", &media.MultiMedia)
|
return decodeOFDCollection(d, start, "MultiMedia", &media.MultiMedia)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析多媒体声明及包内文件路径。
|
// UnmarshalXML 解析多媒体声明及包内文件路径
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (media *MultiMedia) UnmarshalXML(d *xml.Decoder, start xml.StartElement) 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 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (params *DrawParams) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (params *DrawParams) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
return decodeOFDCollection(d, start, "DrawParam", ¶ms.DrawParam)
|
return decodeOFDCollection(d, start, "DrawParam", ¶ms.DrawParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalXML 解析复合图元资源集合。
|
// UnmarshalXML 解析复合图元资源集合
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解码错误
|
// 返回: error 解码错误
|
||||||
func (units *CompositeGraphicUnits) UnmarshalXML(d *xml.Decoder, start xml.StartElement) 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"
|
"golang.org/x/text/unicode/bidi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// richLayout 保存局部标准对象、含墨迹的边界及各片段的字体锚点序号。
|
// richLayout 保存局部标准对象、含墨迹的边界及各片段的字体锚点序号
|
||||||
type richLayout struct {
|
type richLayout struct {
|
||||||
objects []GraphicObject
|
objects []GraphicObject
|
||||||
bounds Box
|
bounds Box
|
||||||
anchors []int
|
anchors []int
|
||||||
}
|
}
|
||||||
|
|
||||||
// richMetrics 保存片段字体、共用基线度量和实际需要的合成样式。
|
// richMetrics 保存片段字体、共用基线度量和实际需要的合成样式
|
||||||
type richMetrics struct {
|
type richMetrics struct {
|
||||||
font FontMetrics
|
font FontMetrics
|
||||||
ascent, descent, height float64
|
ascent, descent, height float64
|
||||||
@@ -44,7 +44,7 @@ type richMetrics struct {
|
|||||||
outlines map[uint16]GeometryPath
|
outlines map[uint16]GeometryPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// richSpan 保存方向和格式均一致的逻辑区间及行内视觉坐标。
|
// richSpan 保存方向和格式均一致的逻辑区间及行内视觉坐标
|
||||||
type richSpan struct {
|
type richSpan struct {
|
||||||
start, end, owner int
|
start, end, owner int
|
||||||
rtl bool
|
rtl bool
|
||||||
@@ -52,7 +52,7 @@ type richSpan struct {
|
|||||||
x, width float64
|
x, width float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// richContextShaper 将带上下文的区间塑形接入已有簇校验流程。
|
// richContextShaper 将带上下文的区间塑形接入已有簇校验流程
|
||||||
type richContextShaper struct {
|
type richContextShaper struct {
|
||||||
FontShaperRun
|
FontShaperRun
|
||||||
text []rune
|
text []rune
|
||||||
@@ -60,12 +60,12 @@ type richContextShaper struct {
|
|||||||
options TextShapeOptions
|
options TextShapeOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// LayoutRichText 生成局部坐标的标准文字和路径,不修改页面或编辑历史。
|
// LayoutRichText 生成局部坐标的标准文字和路径,不修改页面或编辑历史
|
||||||
// 边界包含内容的排版高度和溢出墨迹,不包含空余段落宽度,可能有负的X或Y。
|
// 边界包含内容的排版高度和溢出墨迹,不包含空余段落宽度,可能有负的X或Y
|
||||||
// 非标准RGB文档按需复用或注册RGB颜色空间,返回对象可直接用于当前文档。
|
// 非标准RGB文档按需复用或注册RGB颜色空间,返回对象可直接用于当前文档
|
||||||
// 注册不产生修订或撤销记录;失败时不保留新增资源和资源编号。
|
// 注册不产生修订或撤销记录;失败时不保留新增资源和资源编号
|
||||||
// 提交对象后应创建新的Reader;已有Reader是独立快照,不包含新注册的资源。
|
// 提交对象后应创建新的Reader;已有Reader是独立快照,不包含新注册的资源
|
||||||
// 返回值的资源编号属于当前资源视图,切换文档、版本或替换资源快照后须重新排版。
|
// 返回值的资源编号属于当前资源视图,切换文档、版本或替换资源快照后须重新排版
|
||||||
func (e *Editor) LayoutRichText(value RichText, width float64) ([]GraphicObject, Box, error) {
|
func (e *Editor) LayoutRichText(value RichText, width float64) ([]GraphicObject, Box, error) {
|
||||||
result, err := e.layoutRichText(value, width)
|
result, err := e.layoutRichText(value, width)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,7 +85,7 @@ func (e *Editor) LayoutRichText(value RichText, width float64) ([]GraphicObject,
|
|||||||
return result.objects, result.bounds, nil
|
return result.objects, result.bounds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// layoutRichText 统一计算段落换行、视觉顺序和基线,颜色资源由调用方绑定。
|
// layoutRichText 统一计算段落换行、视觉顺序和基线,颜色资源由调用方绑定
|
||||||
func (e *Editor) layoutRichText(value RichText, width float64) (richLayout, error) {
|
func (e *Editor) layoutRichText(value RichText, width float64) (richLayout, error) {
|
||||||
if err := value.Validate(); err != nil {
|
if err := value.Validate(); err != nil {
|
||||||
return richLayout{}, err
|
return richLayout{}, err
|
||||||
@@ -327,7 +327,7 @@ func (e *Editor) layoutRichText(value RichText, width float64) (richLayout, erro
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// richParagraphOptions 以整段原文检测双向排版,与普通文字的自动模式一致。
|
// richParagraphOptions 以整段原文检测双向排版,与普通文字的自动模式一致
|
||||||
func richParagraphOptions(chars []rune, options TextLayout) TextLayout {
|
func richParagraphOptions(chars []rune, options TextLayout) TextLayout {
|
||||||
if options.AutoBidi && !options.Shaping.Bidi {
|
if options.AutoBidi && !options.Shaping.Bidi {
|
||||||
for _, char := range chars {
|
for _, char := range chars {
|
||||||
@@ -341,7 +341,7 @@ func richParagraphOptions(chars []rune, options TextLayout) TextLayout {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
// richShapeLine 按方向、脚本和格式塑形整行,跨同格式片段合并,保留区间外的塑形上下文。
|
// richShapeLine 按方向、脚本和格式塑形整行,跨同格式片段合并,保留区间外的塑形上下文
|
||||||
func richShapeLine(chars []rune, owners []int, value RichText, metrics []richMetrics, options TextLayout, face *typefont.Face) ([]richSpan, []float64, error) {
|
func richShapeLine(chars []rune, owners []int, value RichText, metrics []richMetrics, options TextLayout, face *typefont.Face) ([]richSpan, []float64, error) {
|
||||||
advances := make([]float64, len(chars))
|
advances := make([]float64, len(chars))
|
||||||
if len(chars) == 0 {
|
if len(chars) == 0 {
|
||||||
@@ -430,7 +430,7 @@ func richShapeLine(chars []rune, owners []int, value RichText, metrics []richMet
|
|||||||
return spans, advances, nil
|
return spans, advances, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShapeText 返回区间相对簇索引,同时保留完整行提供的上下文。
|
// ShapeText 返回区间相对簇索引,同时保留完整行提供的上下文
|
||||||
func (s richContextShaper) ShapeText(_ string, size float64) ([]ShapedGlyph, error) {
|
func (s richContextShaper) ShapeText(_ string, size float64) ([]ShapedGlyph, error) {
|
||||||
glyphs, err := s.ShapeTextRun(s.text, s.start, s.end, size, s.options)
|
glyphs, err := s.ShapeTextRun(s.text, s.start, s.end, size, s.options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -442,7 +442,7 @@ func (s richContextShaper) ShapeText(_ string, size float64) ([]ShapedGlyph, err
|
|||||||
return glyphs, nil
|
return glyphs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// richPositionSpans 按核心排版相同的方向分组规则排列片段,按视觉簇分配字距及两端对齐。
|
// richPositionSpans 按核心排版相同的方向分组规则排列片段,按视觉簇分配字距及两端对齐
|
||||||
func richPositionSpans(spans []richSpan, steps []float64, options TextLayout, x float64) error {
|
func richPositionSpans(spans []richSpan, steps []float64, options TextLayout, x float64) error {
|
||||||
order := make([]int, len(spans))
|
order := make([]int, len(spans))
|
||||||
for i := range order {
|
for i := range order {
|
||||||
@@ -490,7 +490,7 @@ func richPositionSpans(spans []richSpan, steps []float64, options TextLayout, x
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// richSpanText 将逻辑原文和绝对视觉位置写为标准字符字形映射。
|
// richSpanText 将逻辑原文和绝对视觉位置写为标准字符字形映射
|
||||||
func richSpanText(chars []rune, span richSpan, format TextFormat, baseline float64) TextObject {
|
func richSpanText(chars []rune, span richSpan, format TextFormat, baseline float64) TextObject {
|
||||||
text := richTextObject(format)
|
text := richTextObject(format)
|
||||||
code := TextCode{Value: escapeOFDText(string(chars[span.start:span.end]))}
|
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
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
// richSpanOutline 使用现有字体及几何后端生成与文字合成样式一致的墨迹。
|
// richSpanOutline 使用现有字体及几何后端生成与文字合成样式一致的墨迹
|
||||||
func richSpanOutline(span richSpan, format TextFormat, metrics richMetrics, geometry GeometryBackend, baseline float64) (GeometryPath, error) {
|
func richSpanOutline(span richSpan, format TextFormat, metrics richMetrics, geometry GeometryBackend, baseline float64) (GeometryPath, error) {
|
||||||
font, ok := metrics.font.(FontOutlines)
|
font, ok := metrics.font.(FontOutlines)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -567,7 +567,7 @@ func richSpanOutline(span richSpan, format TextFormat, metrics richMetrics, geom
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// richSynthetic 与标准绘制一样避免对已加粗或倾斜的字体重复合成。
|
// richSynthetic 与标准绘制一样避免对已加粗或倾斜的字体重复合成
|
||||||
func (e *Editor) richSynthetic(format TextFormat) (bool, bool) {
|
func (e *Editor) richSynthetic(format TextFormat) (bool, bool) {
|
||||||
bold, italic := format.Bold, format.Italic
|
bold, italic := format.Bold, format.Italic
|
||||||
var definition *Font
|
var definition *Font
|
||||||
@@ -590,7 +590,7 @@ func (e *Editor) richSynthetic(format TextFormat) (bool, bool) {
|
|||||||
return bold, italic
|
return bold, italic
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextObject 构造具有显式填充和标准字重的文字对象。
|
// richTextObject 构造具有显式填充和标准字重的文字对象
|
||||||
func richTextObject(format TextFormat) TextObject {
|
func richTextObject(format TextFormat) TextObject {
|
||||||
fill, stroke := true, false
|
fill, stroke := true, false
|
||||||
weight := 400
|
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)}}
|
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 {
|
func richColor(value string) string {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return "0 0 0"
|
return "0 0 0"
|
||||||
@@ -608,13 +608,13 @@ func richColor(value string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// richUnion 合并排版范围和墨迹范围,保留负坐标及零宽空行。
|
// richUnion 合并排版范围和墨迹范围,保留负坐标及零宽空行
|
||||||
func richUnion(a, b Box) Box {
|
func richUnion(a, b Box) Box {
|
||||||
x, y := math.Min(a.X, b.X), math.Min(a.Y, b.Y)
|
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}
|
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 {
|
func richRectangle(box Box, color string) GraphicObject {
|
||||||
fill, stroke := true, false
|
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))}}
|
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"
|
"github.com/go-text/typesetting/segmenter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNotRichText 表示对象没有富文本信息且不属于可提升的普通文字。
|
// ErrNotRichText 表示对象没有富文本信息且不属于可提升的普通文字
|
||||||
var ErrNotRichText = errors.New("object is not rich text")
|
var ErrNotRichText = errors.New("object is not rich text")
|
||||||
|
|
||||||
// RichText 保存有序文字片段及段落选项,长度和范围使用Unicode码点而非字节。
|
// RichText 保存有序文字片段及段落选项,长度和范围使用Unicode码点而非字节
|
||||||
type RichText struct {
|
type RichText struct {
|
||||||
Runs []RichTextRun `json:"runs"`
|
Runs []RichTextRun `json:"runs"`
|
||||||
Layout TextLayout `json:"layout"`
|
Layout TextLayout `json:"layout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RichTextRun 保存文字及完整格式,换行使用LF,长度单位为毫米。
|
// RichTextRun 保存文字及完整格式,换行使用LF,长度单位为毫米
|
||||||
type RichTextRun struct {
|
type RichTextRun struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Format TextFormat `json:"format"`
|
Format TextFormat `json:"format"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextFormat 保存完整文字格式,Font为当前文档字体资源标识,颜色使用OFD RGB分量。
|
// TextFormat 保存完整文字格式,Font为当前文档字体资源标识,颜色使用OFD RGB分量
|
||||||
type TextFormat struct {
|
type TextFormat struct {
|
||||||
Font string `json:"font"`
|
Font string `json:"font"`
|
||||||
Size float64 `json:"size"`
|
Size float64 `json:"size"`
|
||||||
@@ -51,7 +51,7 @@ type TextFormat struct {
|
|||||||
Background string `json:"background"`
|
Background string `json:"background"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextFormatPatch 保存格式增量,nil保持原值,false和空背景显式清除对应格式。
|
// TextFormatPatch 保存格式增量,nil保持原值,false和空背景显式清除对应格式
|
||||||
type TextFormatPatch struct {
|
type TextFormatPatch struct {
|
||||||
Font *string `json:"font,omitempty"`
|
Font *string `json:"font,omitempty"`
|
||||||
Size *float64 `json:"size,omitempty"`
|
Size *float64 `json:"size,omitempty"`
|
||||||
@@ -64,7 +64,7 @@ type TextFormatPatch struct {
|
|||||||
Background *string `json:"background,omitempty"`
|
Background *string `json:"background,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text 返回包含显式换行的原文,不包含排版产生的软换行。
|
// Text 返回包含显式换行的原文,不包含排版产生的软换行
|
||||||
// 返回: string 完整原文
|
// 返回: string 完整原文
|
||||||
func (v RichText) Text() string {
|
func (v RichText) Text() string {
|
||||||
var text strings.Builder
|
var text strings.Builder
|
||||||
@@ -74,11 +74,11 @@ func (v RichText) Text() string {
|
|||||||
return text.String()
|
return text.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Len 返回原文的Unicode码点数量。
|
// Len 返回原文的Unicode码点数量
|
||||||
// 返回: int 码点数量
|
// 返回: int 码点数量
|
||||||
func (v RichText) Len() int { return utf8.RuneCountInString(v.Text()) }
|
func (v RichText) Len() int { return utf8.RuneCountInString(v.Text()) }
|
||||||
|
|
||||||
// Validate 校验文字、格式、段落选项及跨片段的字素边界,不加载字体。
|
// Validate 校验文字、格式、段落选项及跨片段的字素边界,不加载字体
|
||||||
// 返回: error 无效文字、格式或边界
|
// 返回: error 无效文字、格式或边界
|
||||||
func (v RichText) Validate() error {
|
func (v RichText) Validate() error {
|
||||||
if len(v.Runs) == 0 {
|
if len(v.Runs) == 0 {
|
||||||
@@ -104,7 +104,7 @@ func (v RichText) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate 校验完整文字格式,不解析文档资源。
|
// validate 校验完整文字格式,不解析文档资源
|
||||||
// 返回: error 格式错误
|
// 返回: error 格式错误
|
||||||
func (f TextFormat) validate() error {
|
func (f TextFormat) validate() error {
|
||||||
if strings.TrimSpace(f.Font) == "" || !finite(f.Size) || f.Size <= 0 {
|
if strings.TrimSpace(f.Font) == "" || !finite(f.Size) || f.Size <= 0 {
|
||||||
@@ -126,7 +126,7 @@ func (f TextFormat) validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateRichLayout 校验富文本段落的有限数值及枚举。
|
// validateRichLayout 校验富文本段落的有限数值及枚举
|
||||||
// 入参: v 段落选项
|
// 入参: v 段落选项
|
||||||
// 返回: error 无效选项
|
// 返回: error 无效选项
|
||||||
func validateRichLayout(v TextLayout) error {
|
func validateRichLayout(v TextLayout) error {
|
||||||
@@ -141,7 +141,7 @@ func validateRichLayout(v TextLayout) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// richTextBoundaries 返回包含首尾位置的扩展字素边界。
|
// richTextBoundaries 返回包含首尾位置的扩展字素边界
|
||||||
// 入参: value 原文
|
// 入参: value 原文
|
||||||
// 返回: map[int]bool 有效码点边界
|
// 返回: map[int]bool 有效码点边界
|
||||||
func richTextBoundaries(value string) map[int]bool {
|
func richTextBoundaries(value string) map[int]bool {
|
||||||
@@ -156,7 +156,7 @@ func richTextBoundaries(value string) map[int]bool {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkRange 检查半开范围及其字素边界。
|
// checkRange 检查半开范围及其字素边界
|
||||||
// 入参: start 起点, end 终点
|
// 入参: start 起点, end 终点
|
||||||
// 返回: error 无效范围
|
// 返回: error 无效范围
|
||||||
func (v RichText) checkRange(start, end int) error {
|
func (v RichText) checkRange(start, end int) error {
|
||||||
@@ -170,7 +170,7 @@ func (v RichText) checkRange(start, end int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slice 返回半开范围的独立片段,空范围保留插入位置的格式。
|
// Slice 返回半开范围的独立片段,空范围保留插入位置的格式
|
||||||
// 入参: start 起点, end 终点
|
// 入参: start 起点, end 终点
|
||||||
// 返回: RichText 独立片段, error 无效范围
|
// 返回: RichText 独立片段, error 无效范围
|
||||||
func (v RichText) Slice(start, end int) (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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sliceRuns 提取已校验范围,不保留空片段。
|
// sliceRuns 提取已校验范围,不保留空片段
|
||||||
// 入参: start 起点, end 终点
|
// 入参: start 起点, end 终点
|
||||||
// 返回: []RichTextRun 独立片段
|
// 返回: []RichTextRun 独立片段
|
||||||
func (v RichText) sliceRuns(start, end int) []RichTextRun {
|
func (v RichText) sliceRuns(start, end int) []RichTextRun {
|
||||||
@@ -210,7 +210,7 @@ func (v RichText) sliceRuns(start, end int) []RichTextRun {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace 原子替换半开范围,空片段列表表示删除,相邻相同格式自动合并。
|
// Replace 原子替换半开范围,空片段列表表示删除,相邻相同格式自动合并
|
||||||
// 入参: start 起点, end 终点, runs 替换片段
|
// 入参: start 起点, end 终点, runs 替换片段
|
||||||
// 返回: error 范围或内容错误
|
// 返回: error 范围或内容错误
|
||||||
func (v *RichText) Replace(start, end int, runs []RichTextRun) 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyFormat 原子修改半开范围的格式,空范围不改变正文。
|
// ApplyFormat 原子修改半开范围的格式,空范围不改变正文
|
||||||
// 入参: start 起点, end 终点, patch 格式增量
|
// 入参: start 起点, end 终点, patch 格式增量
|
||||||
// 返回: error 范围或格式错误
|
// 返回: error 范围或格式错误
|
||||||
func (v *RichText) ApplyFormat(start, end int, patch TextFormatPatch) 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)
|
return v.Replace(start, end, part.Runs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply 应用显式给出的格式字段。
|
// apply 应用显式给出的格式字段
|
||||||
// 入参: f 完整原格式
|
// 入参: f 完整原格式
|
||||||
// 返回: TextFormat 修改后的格式
|
// 返回: TextFormat 修改后的格式
|
||||||
func (p TextFormatPatch) apply(f TextFormat) TextFormat {
|
func (p TextFormatPatch) apply(f TextFormat) TextFormat {
|
||||||
@@ -292,7 +292,7 @@ func (p TextFormatPatch) apply(f TextFormat) TextFormat {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeRichText 合并相同格式,保留空文档的插入格式。
|
// normalizeRichText 合并相同格式,保留空文档的插入格式
|
||||||
// 入参: v 完整富文本
|
// 入参: v 完整富文本
|
||||||
// 返回: RichText 归一化副本
|
// 返回: RichText 归一化副本
|
||||||
func normalizeRichText(v RichText) RichText {
|
func normalizeRichText(v RichText) RichText {
|
||||||
|
|||||||
+7
-3
@@ -30,7 +30,7 @@ const structureTypeOfficial = "od"
|
|||||||
const structureNamespaceEGov2013 = "http://www.nits.org.cn/eGovDoc/2013"
|
const structureNamespaceEGov2013 = "http://www.nits.org.cn/eGovDoc/2013"
|
||||||
|
|
||||||
// Structure 保存自定义标引入口及结构树,单个文件解析失败时通过Error报告,不影响其他标引
|
// Structure 保存自定义标引入口及结构树,单个文件解析失败时通过Error报告,不影响其他标引
|
||||||
// CanBind表示已识别标引引用规则,包括公文标引及本库富文本标引,具体节点和目标仍由绑定接口校验
|
// CanBind表示已识别标引引用规则,包括公文、富文本及发票标引,具体节点和目标仍由绑定接口校验
|
||||||
// Internal标记本库内部编辑标引,不删除或重排入口;界面跳过此项时必须保留原始数组索引
|
// Internal标记本库内部编辑标引,不删除或重排入口;界面跳过此项时必须保留原始数组索引
|
||||||
// 此标记不限制通用低级编辑接口,也不表示内部元数据内容已通过有效性校验
|
// 此标记不限制通用低级编辑接口,也不表示内部元数据内容已通过有效性校验
|
||||||
type Structure struct {
|
type Structure struct {
|
||||||
@@ -45,7 +45,7 @@ type Structure struct {
|
|||||||
|
|
||||||
// StructureNode 保存标引节点的名称、属性、直接文本和有序子节点,不改变原始XML
|
// StructureNode 保存标引节点的名称、属性、直接文本和有序子节点,不改变原始XML
|
||||||
// Text按文档顺序汇总直接文本并保留空白,不包含子节点文本
|
// Text按文档顺序汇总直接文本并保留空白,不包含子节点文本
|
||||||
// References仅解释已识别公文标引或本库富文本标引中的ObjectRef;未知扩展保留结构,不推定其引用语义
|
// References仅解释已识别公文、富文本或发票标引中的ObjectRef;未知扩展保留结构,不推定其引用语义
|
||||||
type StructureNode struct {
|
type StructureNode struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Namespace string `json:"namespace,omitempty"`
|
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.CanBind = structureOfficial(tag, item.Root.Namespace)
|
||||||
item.Internal = tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace && item.Root.Namespace == richTextNamespace && item.Root.Name == "RichText"
|
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
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,10 +216,13 @@ func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*Structure
|
|||||||
return root, nil
|
return root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// structureOfficial 识别公文和本库富文本标引的明确引用规则,不推断未知行业语义
|
// structureOfficial 识别公文、富文本及发票标引的明确引用规则,不推断未知行业语义
|
||||||
// 入参: tag 标引入口, namespace 内容根节点命名空间
|
// 入参: tag 标引入口, namespace 内容根节点命名空间
|
||||||
// 返回: bool 是否使用公文引用规则
|
// 返回: bool 是否使用公文引用规则
|
||||||
func structureOfficial(tag CustomTag, namespace string) 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 {
|
if tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace && namespace == richTextNamespace {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user