From 53769b65abefea6c9c1c60cb6ec23922f88c07b8 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sat, 10 Oct 2026 18:32:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=AD=BE=E7=AB=A0=E6=A0=87=E8=AF=86):=20?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E7=AD=BE=E7=AB=A0=E6=A0=87=E8=AF=86=E4=B8=8E?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_editor_import.go | 97 +++++++++++++++++++++++++++++---------- ofdgo_sign_id.go | 43 +++++++++++++++++ ofdgo_sign_write.go | 25 +++------- ofdgo_sign_write_stamp.go | 26 +++++++++-- 4 files changed, 143 insertions(+), 48 deletions(-) create mode 100644 ofdgo_sign_id.go diff --git a/ofdgo_editor_import.go b/ofdgo_editor_import.go index 37a591e..3e21b58 100644 --- a/ofdgo_editor_import.go +++ b/ofdgo_editor_import.go @@ -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 } diff --git a/ofdgo_sign_id.go b/ofdgo_sign_id.go new file mode 100644 index 0000000..1dcafd1 --- /dev/null +++ b/ofdgo_sign_id.go @@ -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 +} diff --git a/ofdgo_sign_write.go b/ofdgo_sign_write.go index c2c6265..9325dd4 100644 --- a/ofdgo_sign_write.go +++ b/ofdgo_sign_write.go @@ -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 } diff --git a/ofdgo_sign_write_stamp.go b/ofdgo_sign_write_stamp.go index e551f6f..55fe10a 100644 --- a/ofdgo_sign_write_stamp.go +++ b/ofdgo_sign_write_stamp.go @@ -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 判断印章区域是否有限且尺寸为正