feat(电子发票): 补齐发票读写校验与票面同步
build.yaml / build (push) Successful in 55s

This commit is contained in:
xiaoqidun committed 2026-10-16 12:07:42 +08:00
1 parent d3a713f504
commit 528162a276
16 files changed
+3998 -254

No files matched your search

+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+583
View File
@@ -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()
}
+305
View File
@@ -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
View File
@@ -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
View File
@@ -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", &params.DrawParam) return decodeOFDCollection(d, start, "DrawParam", &params.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
View File
@@ -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
View File
@@ -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
View File
@@ -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
} }