feat(签章标识): 规范签章标识与导入合并
build.yaml / build (push) Successful in 44s

This commit is contained in:
xiaoqidun committed 2026-10-10 18:32:03 +08:00
1 parent 341ad7cd7e
commit 53769b65ab
4 files changed
+143 -48

No files matched your search

+73 -24
View File
@@ -56,6 +56,7 @@ type editorPageImport struct {
defaultCS string
attachments map[string][]byte
attachmentIDs map[string]bool
signatureIDs *signatureIDSequence
copyPage bool
strictLinks bool
progress editorProgress
@@ -961,8 +962,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
if name == "Attachment" {
value, err = m.attachmentID(value)
} else if name == "Signature" || name == "StampAnnot" {
value = name + ":" + value
value = m.id(value)
value, err = m.signatureID(entry.name, name, value)
} else {
value = m.id(value)
}
@@ -984,7 +984,11 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
} else {
directory := "Res"
if name == "Signature" {
directory = path.Join("Signs", "Sign_"+m.id("Signature:"+node.attr("ID")))
id, idErr := m.signatureID(entry.name, name, node.attr("ID"))
if idErr != nil {
return nil, idErr
}
directory = path.Join("Signs", "Sign_"+id)
}
value, err = m.copyXML(loc, path.Join(m.directory, directory, path.Base(loc)))
}
@@ -1236,6 +1240,59 @@ func (m *editorPageImport) attachmentID(id string) (string, error) {
return mapped, nil
}
// signatureID 分配导入签章的文本标识,同名签章注释按来源文件区分
// 入参: source 来源文件, kind 节点类型, id 来源标识
// 返回: string 目标标识, error 签章索引或编号错误
func (m *editorPageImport) signatureID(source, kind, id string) (string, error) {
key := kind + ":" + id
if kind == "StampAnnot" {
key = kind + ":" + source + ":" + id
}
if mapped := m.ids[key]; mapped != "" {
return mapped, nil
}
if m.signatureIDs == nil {
var sequence signatureIDSequence
doc, err := m.target.Doc()
if err != nil {
return "", err
}
if doc.Signatures != "" {
name := m.target.ResPath(doc.Signatures)
data, err := m.target.readFile(name)
if err != nil {
return "", err
}
var list Signatures
if err := xml.Unmarshal(data, &list); err != nil {
return "", err
}
sequence.observe(list.MaxSignID)
for _, ref := range list.List {
sequence.observe(ref.ID)
data, err := m.target.readFile(signatureRefPath(name, ref.BaseLoc))
if err != nil {
return "", err
}
file, err := parseSignatureFile(data)
if err != nil {
return "", err
}
for _, stamp := range file.SignedInfo.StampAnnot {
sequence.observe(stamp.ID)
}
}
}
m.signatureIDs = &sequence
}
mapped, err := m.signatureIDs.next()
if err != nil {
return "", err
}
m.ids[key] = mapped
return mapped, nil
}
// signatures 复制所选页面的签章及原始验证凭据,过滤其他页面的签章位置
// 返回: []byte 签名索引条目, error 错误信息
func (m *editorPageImport) signatures() ([]byte, error) {
@@ -1401,6 +1458,19 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
if err != nil {
return nil, err
}
indexName := cleanPackagePath(loc)
indexData := files[indexName]
indexRoot, err := parseEditorXML(indexData)
if err != nil {
return nil, err
}
maximum := "s" + strconv.FormatUint(uint64(*m.signatureIDs), 10)
value := indexRoot.textXML("MaxSignId", maximum)
patch := editorXMLPatch{indexRoot.open, indexRoot.open, value}
if node := indexRoot.child("MaxSignId"); node != nil {
patch = editorXMLPatch{node.start, node.end, value}
}
files[indexName] = editorPatchXML(indexData, []editorXMLPatch{patch})
if base.document.Signatures == "" {
ofd, err := base.reader.readFile("OFD.xml")
if err != nil {
@@ -1483,27 +1553,6 @@ func mergeImportIndex(reader *Reader, files map[string][]byte, loc, fallback, ro
if err != nil {
return "", err
}
if rootName == "Signatures" {
root, err := parseEditorXML(data)
if err != nil {
return "", err
}
maximum := 0
for _, node := range root.children {
value := node.attr("ID")
if node.name.Local == "MaxSignId" {
value = editorImportText(data, node)
}
id, _ := strconv.Atoi(value)
maximum = max(maximum, id)
}
value := root.textXML("MaxSignId", strconv.Itoa(maximum))
patch := editorXMLPatch{root.open, root.open, value}
if node := root.child("MaxSignId"); node != nil {
patch = editorXMLPatch{node.start, node.end, value}
}
data = editorPatchXML(data, []editorXMLPatch{patch})
}
files[name] = data
return "/" + name, nil
}
+43
View File
@@ -0,0 +1,43 @@
// 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 (
"fmt"
"strconv"
"strings"
)
// signatureIDSequence 签名及签章注释共用的编号上限,与文档图元编号相互独立
type signatureIDSequence uint64
// observe 记录已有签章标识的编号上限,其他命名形式保持不变
// 入参: id 已有标识
func (s *signatureIDSequence) observe(id string) {
n, err := strconv.ParseUint(strings.TrimPrefix(id, "s"), 10, 64)
if err == nil && n > uint64(*s) {
*s = signatureIDSequence(n)
}
}
// next 分配采用sNNN格式的签章标识
// 返回: string 新标识, error 编号耗尽错误
func (s *signatureIDSequence) next() (string, error) {
if uint64(*s) == ^uint64(0) {
return "", fmt.Errorf("signature ID exhausted")
}
*s++
return "s" + strconv.FormatUint(uint64(*s), 10), nil
}
+6 -19
View File
@@ -471,20 +471,14 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption
// 返回: string 签名标识, []SignatureStamp 外观, error 错误信息
func signatureWriteIDs(list *Signatures, listPath string, parts map[string][]byte, stamps []SignatureStamp) (string, []SignatureStamp, error) {
used := make(map[string]bool)
var maximum uint64
track := func(id string) {
n, err := strconv.ParseUint(strings.TrimPrefix(id, "s"), 10, 64)
if err == nil && n > maximum {
maximum = n
}
}
track(list.MaxSignID)
var sequence signatureIDSequence
sequence.observe(list.MaxSignID)
add := func(id string) error {
if !signatureWriteIDValid(id) || used[id] {
return fmt.Errorf("invalid or duplicate signature ID: %s", id)
}
used[id] = true
track(id)
sequence.observe(id)
return nil
}
for _, ref := range list.List {
@@ -508,26 +502,19 @@ func signatureWriteIDs(list *Signatures, listPath string, parts map[string][]byt
}
}
}
next := func() (string, error) {
if maximum == ^uint64(0) {
return "", fmt.Errorf("signature ID exhausted")
}
maximum++
return "s" + strconv.FormatUint(maximum, 10), nil
}
id, err := next()
id, err := sequence.next()
if err != nil {
return "", nil, err
}
for i := range stamps {
if stamps[i].ID == "" {
stamps[i].ID, err = next()
stamps[i].ID, err = sequence.next()
if err != nil {
return "", nil, err
}
}
}
list.MaxSignID = "s" + strconv.FormatUint(maximum, 10)
list.MaxSignID = "s" + strconv.FormatUint(uint64(sequence), 10)
return id, stamps, nil
}
+21 -5
View File
@@ -18,7 +18,7 @@ import (
"fmt"
"math"
"strconv"
"strings"
"unicode/utf8"
)
// SignaturePageStamps 生成所选页面相同毫米坐标的印章位置并检查页内范围
@@ -142,13 +142,29 @@ func signatureWriteStamps(r *Reader, stamps []SignatureStamp, seal bool) ([]Sign
return result, nil
}
// signatureWriteIDValid 检查可互通的签章标识字符
// signatureWriteIDValid 按XML 1.0的NCName规则校验签章标识
// 入参: id 标识
// 返回: bool 是否有效
func signatureWriteIDValid(id string) bool {
return id != "" && len(id) <= 128 && strings.IndexFunc(id, func(c rune) bool {
return !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-' || c == '.')
}) == -1
if id == "" || !utf8.ValidString(id) {
return false
}
for i, c := range id {
if c == '_' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' ||
c >= 0xC0 && c <= 0xD6 || c >= 0xD8 && c <= 0xF6 ||
c >= 0xF8 && c <= 0x2FF || c >= 0x370 && c <= 0x37D ||
c >= 0x37F && c <= 0x1FFF || c >= 0x200C && c <= 0x200D ||
c >= 0x2070 && c <= 0x218F || c >= 0x2C00 && c <= 0x2FEF ||
c >= 0x3001 && c <= 0xD7FF || c >= 0xF900 && c <= 0xFDCF ||
c >= 0xFDF0 && c <= 0xFFFD || c >= 0x10000 && c <= 0xEFFFF {
continue
}
if i == 0 || !(c == '-' || c == '.' || c >= '0' && c <= '9' ||
c == 0xB7 || c >= 0x300 && c <= 0x36F || c >= 0x203F && c <= 0x2040) {
return false
}
}
return true
}
// signatureWriteBoxValid 判断印章区域是否有限且尺寸为正