From 492adccb81e8bd540e36a2b7018cb0309d7e6650 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sat, 10 Oct 2026 20:42:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=8F=82=E7=85=A7=E6=A0=87=E5=87=86):=20?= =?UTF-8?q?=E8=A7=84=E8=8C=83=E8=B5=84=E6=BA=90=E5=A4=84=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=89=88=E6=9C=AC=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_color_profile.go | 2 +- ofdgo_compression.go | 11 +- ofdgo_compression_images.go | 5 +- ofdgo_compression_resources.go | 14 +- ofdgo_crypto_package.go | 35 +-- ofdgo_editor_action.go | 2 +- ofdgo_editor_annot.go | 2 +- ofdgo_editor_annot_content.go | 9 +- ofdgo_editor_annot_ops.go | 2 +- ofdgo_editor_attachment.go | 6 +- ofdgo_editor_documents.go | 33 +-- ofdgo_editor_font_binding.go | 15 +- ofdgo_editor_font_scan.go | 53 +++-- ofdgo_editor_import.go | 28 ++- ofdgo_editor_open.go | 3 +- ofdgo_editor_outline.go | 2 +- ofdgo_editor_package.go | 34 ++- ofdgo_editor_pages.go | 9 +- ofdgo_editor_reference.go | 20 +- ofdgo_editor_resources.go | 26 +- ofdgo_format.go | 2 +- ofdgo_meta.go | 18 ++ ofdgo_package.go | 12 +- ofdgo_reader.go | 62 ++--- ofdgo_reader_dependencies.go | 12 +- ofdgo_sign_policy.go | 29 +-- ofdgo_sign_verify.go | 23 +- ofdgo_sign_write.go | 21 +- ofdgo_sign_write_stamp.go | 28 +-- ofdgo_version.go | 185 +++++++++++++++ ofdgo_version_edit.go | 422 +++++++++++++++++++++++++++++++++ ofdgo_version_extract.go | 140 +++++++++++ ofdgo_version_reader.go | 127 ++++++++++ ofdgo_writer.go | 2 +- ofdgo_xml.go | 27 +++ 35 files changed, 1182 insertions(+), 239 deletions(-) create mode 100644 ofdgo_version.go create mode 100644 ofdgo_version_edit.go create mode 100644 ofdgo_version_extract.go create mode 100644 ofdgo_version_reader.go diff --git a/ofdgo_color_profile.go b/ofdgo_color_profile.go index 554ba59..ca2fcdb 100644 --- a/ofdgo_color_profile.go +++ b/ofdgo_color_profile.go @@ -68,7 +68,7 @@ func (r *Reader) colorProfile(space *ColorSpace) (*colorProfile, error) { } name := r.ResPath(space.Profile) for _, other := range r.colorSpaceCache { - if other != nil && other.profile != nil && strings.EqualFold(other.Type, space.Type) && strings.EqualFold(r.ResPath(other.Profile), name) { + if other != nil && other.profile != nil && strings.EqualFold(other.Type, space.Type) && r.ResPath(other.Profile) == name { space.profile = other.profile return space.profile, space.profile.err } diff --git a/ofdgo_compression.go b/ofdgo_compression.go index 27616aa..10f23e7 100644 --- a/ofdgo_compression.go +++ b/ofdgo_compression.go @@ -136,7 +136,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi } for _, resource := range e.resources { if resource.image != nil { - snapshot.output.images[strings.ToLower(cleanPackagePath(resource.name))] = true + snapshot.output.images[cleanPackagePath(resource.name)] = true } } if e.source != nil { @@ -154,7 +154,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi return nil, err } for _, img := range images { - snapshot.output.images[strings.ToLower(cleanPackagePath(img.Location))] = true + snapshot.output.images[cleanPackagePath(img.Location)] = true } } if options.Compression.Mode == CompressionLossy { @@ -188,9 +188,8 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) } safe = false } - for key := range dependencies { - name, exists := r.fileNamesFold[key] - if !exists { + for name := range dependencies { + if !r.fileNames[name] { safe = false break } @@ -269,7 +268,7 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) paths := make(map[string]bool) for _, img := range images { if !safe || masks[editorResourceID(img.ID)] { - paths[strings.ToLower(cleanPackagePath(img.Location))] = true + paths[cleanPackagePath(img.Location)] = true } } return paths, safe && geometry, ctx.Err() diff --git a/ofdgo_compression_images.go b/ofdgo_compression_images.go index a3ea3d7..c561b8c 100644 --- a/ofdgo_compression_images.go +++ b/ofdgo_compression_images.go @@ -19,7 +19,6 @@ import ( "fmt" "image" "math" - "strings" ) // compressionImages 记录图片各处使用的最大像素需求 @@ -71,7 +70,7 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp return nil, nil, err } for _, img := range catalog { - name := strings.ToLower(cleanPackagePath(img.Location)) + name := cleanPackagePath(img.Location) if old, ok := images[name]; !ok || old { images[name] = !masks[name] } @@ -150,7 +149,7 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo, } result := make(map[string]image.Point) for _, img := range images { - name := strings.ToLower(cleanPackagePath(img.Location)) + name := cleanPackagePath(img.Location) size := visitor.sizes[editorResourceID(img.ID)] old := result[name] result[name] = image.Pt(max(old.X, size.X), max(old.Y, size.Y)) diff --git a/ofdgo_compression_resources.go b/ofdgo_compression_resources.go index 9119fbd..543f420 100644 --- a/ofdgo_compression_resources.go +++ b/ofdgo_compression_resources.go @@ -76,7 +76,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea if err := e.output.ctx.Err(); err != nil { return err } - key := strings.ToLower(cleanPackagePath(name)) + key := cleanPackagePath(name) if removed[key] || strings.HasSuffix(name, "/") { continue } @@ -126,6 +126,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea if err != nil { return err } + preserved := refs.versions[cleanPackagePath(name)] for _, group := range root.children { for _, node := range group.children { field := "" @@ -147,11 +148,11 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea return err } key := editorResourceLocation(name, root.attr("BaseLoc"), value) - if field == "MediaFile" && node.attr("Type") != "Image" { + if preserved || field == "MediaFile" && node.attr("Type") != "Image" { refs.files[key] = true continue } - if key != strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))) { + if key != cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)) { return nil } if actual[key] != "" && !refs.files[key] { @@ -259,10 +260,10 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea } stem := strings.TrimSuffix(actual[key], ext) name := stem + ".jpg" - for n := 1; reserved[strings.ToLower(name)]; n++ { + for n := 1; reserved[name]; n++ { name = fmt.Sprintf("%s-%d.jpg", stem, n) } - reserved[strings.ToLower(name)] = true + reserved[name] = true locations[key] = name parts[name] = data delete(parts, actual[key]) @@ -286,6 +287,9 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea } } for _, name := range resources { + if refs.versions[cleanPackagePath(name)] { + continue + } data, err := read(name) if err != nil { return err diff --git a/ofdgo_crypto_package.go b/ofdgo_crypto_package.go index 2c67526..22eb40a 100644 --- a/ofdgo_crypto_package.go +++ b/ofdgo_crypto_package.go @@ -124,7 +124,7 @@ func (r *Reader) open(options []ReaderOption) error { if err := r.indexPackage(); err != nil { return err } - if _, ok := r.fileNamesFold["encryptions.xml"]; ok { + if r.fileNames["Encryptions.xml"] { if err := r.decryptPackage(settings); err != nil { return err } @@ -135,8 +135,8 @@ func (r *Reader) open(options []ReaderOption) error { // packageData 获取包内所有条目的实际字节,保留未识别文件 // 返回: map[string][]byte 包内条目, error 错误信息 func (r *Reader) packageData() (map[string][]byte, error) { - parts := make(map[string][]byte, len(r.fileNamesFold)) - for _, name := range r.fileNamesFold { + parts := make(map[string][]byte, len(r.fileNames)) + for name := range r.fileNames { data, err := r.readFile(name) if err != nil { return nil, err @@ -198,8 +198,7 @@ func (r *Reader) decryptPackage(settings readerOptions) error { extraSeeds := make(map[string]bool) for _, entry := range entries.Entries { name := cleanPackagePath(entry.Path) - fold := strings.ToLower(name) - if !validPackagePath(name) || used[fold] { + if !validPackagePath(name) || used[name] { clear(key) return fmt.Errorf("ambiguous encrypted package path: %q", entry.Path) } @@ -231,7 +230,7 @@ func (r *Reader) decryptPackage(settings readerOptions) error { return ErrInvalidCredentials } delete(parts, actual) - used[fold] = true + used[name] = true } clear(key) delete(parts, seedName) @@ -239,16 +238,7 @@ func (r *Reader) decryptPackage(settings readerOptions) error { for name := range extraSeeds { delete(parts, name) } - remaining := make(map[string]string, len(parts)) - for name := range parts { - remaining[strings.ToLower(name)] = name - } - for name, plain := range decrypted { - if existing, ok := remaining[strings.ToLower(name)]; ok { - delete(parts, existing) - } - parts[name] = plain - } + maps.Copy(parts, decrypted) state.info.Method = seed.Method for _, user := range seed.Users { state.info.Users = append(state.info.Users, user.Name) @@ -257,11 +247,7 @@ func (r *Reader) decryptPackage(settings readerOptions) error { state.options = inheritedEncryptionOptions(seed, credential, settings.provider) } } - for name := range parts { - if strings.ToLower(name) == "encryptions.xml" { - delete(parts, name) - } - } + delete(parts, "Encryptions.xml") r.Zip, r.files, r.encryption = nil, parts, state return nil } @@ -289,7 +275,7 @@ func decodeEncryptionEntries(data, key, iv []byte, provider CryptoProvider) (enc return entries, nil } -// encryptionPart 按统一大小写规则读取加密元数据引用 +// encryptionPart 按大小写敏感路径读取加密元数据引用 // 入参: parts 包内条目, name 引用路径 // 返回: []byte 条目内容, string 实际路径, error 错误信息 func encryptionPart(parts map[string][]byte, name string) ([]byte, string, error) { @@ -300,11 +286,6 @@ func encryptionPart(parts map[string][]byte, name string) ([]byte, string, error if data, ok := parts[name]; ok { return data, name, nil } - for actual, data := range parts { - if strings.ToLower(actual) == strings.ToLower(name) { - return data, actual, nil - } - } return nil, "", fmt.Errorf("missing encrypted entry: %s", name) } diff --git a/ofdgo_editor_action.go b/ofdgo_editor_action.go index 093bee5..2212c0c 100644 --- a/ofdgo_editor_action.go +++ b/ofdgo_editor_action.go @@ -49,7 +49,7 @@ func (e *Editor) SetDocumentActions(actions []Action) error { return err } } - name := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot) + name := base.reader.documentRoot data, err := base.reader.readFile(name) if err != nil { return err diff --git a/ofdgo_editor_annot.go b/ofdgo_editor_annot.go index 47789ba..a76a8f4 100644 --- a/ofdgo_editor_annot.go +++ b/ofdgo_editor_annot.go @@ -269,7 +269,7 @@ func (e *Editor) editAnnotations(index int, ids []string, edit func([]byte, *edi if len(patches) != 0 { parts[name] = editorPatchXML(data, patches) if remaining == 0 { - docName, err := editorPageLocation(reader, nil, "", "/"+cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot)) + docName, err := editorPageLocation(reader, nil, "", "/"+reader.documentRoot) if err != nil { return err } diff --git a/ofdgo_editor_annot_content.go b/ofdgo_editor_annot_content.go index 0fe164d..636fed0 100644 --- a/ofdgo_editor_annot_content.go +++ b/ofdgo_editor_annot_content.go @@ -16,6 +16,7 @@ package ofdgo import ( "bytes" + "context" "encoding/xml" "fmt" "maps" @@ -379,7 +380,7 @@ func (e *Editor) appendAnnotations(index int, annotations []byte, generated bool } parts := map[string][]byte{name: data, file: content} if base.document.Annotations == "" { - docName := cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot) + docName := reader.documentRoot docData, err := reader.readFile(docName) if err != nil { return err @@ -448,6 +449,10 @@ func (e *Editor) annotationFiles() []string { // 入参: base 原包, parts 修改的文件 // 返回: error 错误信息 func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]byte) error { + parts, err := base.reader.versionChanges(context.Background(), parts) + if err != nil { + return err + } files := maps.Clone(base.reader.files) if files == nil { files = make(map[string][]byte) @@ -458,7 +463,7 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by } files[name] = data } - reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex} + reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex, selectedVersion: base.reader.selectedVersion} if err := reader.initRoot(); err != nil { return err } diff --git a/ofdgo_editor_annot_ops.go b/ofdgo_editor_annot_ops.go index 5ebc24b..62d2c17 100644 --- a/ofdgo_editor_annot_ops.go +++ b/ofdgo_editor_annot_ops.go @@ -141,7 +141,7 @@ func (e *Editor) PasteAnnotations(page int, selection *AnnotationSelection, dx, edit.source.annotationStates = states if len(selection.resources) != 0 { reader := edit.source.reader - name := cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot) + name := reader.documentRoot data, err := reader.readFile(name) if err != nil { return err diff --git a/ofdgo_editor_attachment.go b/ofdgo_editor_attachment.go index 0273c96..111d16d 100644 --- a/ofdgo_editor_attachment.go +++ b/ofdgo_editor_attachment.go @@ -73,7 +73,7 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) { return "", err } entry = bytes.TrimPrefix(entry, []byte(xml.Header)) - docName := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot) + docName := base.reader.documentRoot docData, err := base.reader.readFile(docName) if err != nil { return "", err @@ -220,7 +220,7 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s if base == nil { return fmt.Errorf("attachment not found: %s", id) } - name := cleanPackagePath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot) + name := base.reader.documentRoot external := strings.TrimSpace(base.document.Attachments.Path) != "" if external { name = base.reader.ResPath(base.document.Attachments.Path) @@ -277,7 +277,7 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s if retired == nil { retired = make(map[string]bool) } - retired[strings.ToLower(cleanPackagePath(base.reader.ResPath(location)))] = true + retired[editorResourceLocation(name, "", location)] = true e.source.retiredAttachments = retired } } diff --git a/ofdgo_editor_documents.go b/ofdgo_editor_documents.go index 2742bce..0eb9f09 100644 --- a/ofdgo_editor_documents.go +++ b/ofdgo_editor_documents.go @@ -93,7 +93,7 @@ func (e *Editor) SetDocumentInfo(index int, info DocInfo) error { if err != nil { return 0, err } - reader.files[reader.fileNamesFold["ofd.xml"]] = updated + reader.files["OFD.xml"] = updated return reader.DocumentIndex(), nil }) } @@ -116,8 +116,8 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int, for number := 0; ; number++ { candidate := fmt.Sprintf("Doc_%d", number) occupied := false - for name := range reader.fileNamesFold { - if name == strings.ToLower(candidate) || strings.HasPrefix(name, strings.ToLower(candidate)+"/") { + for name := range reader.fileNames { + if name == candidate || strings.HasPrefix(name, candidate+"/") { occupied = true break } @@ -156,7 +156,7 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int, if existing := root.childAt("DocBody", at); existing != nil { position = existing.start } - reader.files[reader.fileNamesFold["ofd.xml"]] = editorPatchXML(data, []editorXMLPatch{{position, position, bytes.TrimPrefix(body, []byte(xml.Header))}}) + reader.files["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{{position, position, bytes.TrimPrefix(body, []byte(xml.Header))}}) return at, nil }) if err != nil { @@ -199,7 +199,7 @@ func (e *Editor) MoveDocument(from, to int) error { body := root.childAt("DocBody", index) patches = append(patches, editorXMLPatch{body.start, body.end, content}) } - reader.files[reader.fileNamesFold["ofd.xml"]] = editorPatchXML(data, patches) + reader.files["OFD.xml"] = editorPatchXML(data, patches) selected := reader.DocumentIndex() if selected == from { selected = to @@ -232,10 +232,10 @@ func (e *Editor) DeleteDocument(index int) error { directory := path.Dir(cleanPackagePath(reader.OFD.DocBody[index].DocRoot)) if directory != "." && !slices.ContainsFunc(reader.OFD.DocBody, func(body DocBody) bool { name := cleanPackagePath(body.DocRoot) - return name != cleanPackagePath(reader.OFD.DocBody[index].DocRoot) && strings.HasPrefix(strings.ToLower(name), strings.ToLower(directory)+"/") + return name != cleanPackagePath(reader.OFD.DocBody[index].DocRoot) && strings.HasPrefix(name, directory+"/") }) { - for name := range reader.fileNamesFold { - if strings.HasPrefix(name, strings.ToLower(directory)+"/") { + for name := range reader.fileNames { + if strings.HasPrefix(name, directory+"/") { removed[name] = true } } @@ -252,19 +252,19 @@ func (e *Editor) DeleteDocument(index int) error { delete(removed, name) } } - delete(removed, "ofd.xml") + delete(removed, "OFD.xml") for name := range reader.files { - if removed[strings.ToLower(name)] { + if removed[name] { delete(reader.files, name) } } if reader.Zip != nil { reader.Zip = &zip.Reader{Comment: reader.Zip.Comment, File: slices.DeleteFunc(slices.Clone(reader.Zip.File), func(file *zip.File) bool { - return removed[strings.ToLower(cleanPackagePath(file.Name))] + return removed[cleanPackagePath(file.Name)] })} } body := root.childAt("DocBody", index) - reader.files[reader.fileNamesFold["ofd.xml"]] = editorPatchXML(data, []editorXMLPatch{{start: body.start, end: body.end}}) + reader.files["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{{start: body.start, end: body.end}}) selected := reader.DocumentIndex() if selected > index { selected-- @@ -292,6 +292,7 @@ func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, erro return err } reader.files = maps.Clone(reader.files) + previous := reader.OFD.DocBody[reader.documentIndex] index, err := edit(reader, data, root) if err != nil { return err @@ -301,10 +302,14 @@ func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, erro if err := reader.initRoot(); err != nil { return err } + current := reader.OFD.DocBody[index] + if previous.DocRoot != current.DocRoot || previous.DocInfo.DocID != current.DocInfo.DocID { + reader.selectedVersion = nil + } if _, err := reader.Doc(); err != nil { return err } - if e.source != nil && e.documentRoot() == reader.OFD.DocBody[index].DocRoot && e.Info.DocID == reader.OFD.DocBody[index].DocInfo.DocID { + if e.source != nil && e.documentRoot() == reader.documentRoot && e.Info.DocID == reader.OFD.DocBody[index].DocInfo.DocID { source := *e.source source.reader = reader e.source = &source @@ -331,7 +336,7 @@ func (e *Editor) documentRoot() string { if e.source == nil { return "" } - return e.source.reader.OFD.DocBody[e.DocumentIndex()].DocRoot + return e.source.reader.documentRoot } // sameDocument 判断快照是否属于同一文档,区别删除后复用目录的新文档 diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index d4a99e2..1961ad9 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -407,6 +407,9 @@ func sameBindingFont(source, target []byte) bool { // 返回: FontFile 独立字体, error 匹配或解析错误 func (e *Editor) resolveBindingFont(scan *editorFontDocument, declaration *editorFontDeclaration) (FontFile, error) { reader := &Reader{Zip: scan.reader.Zip, files: maps.Clone(scan.reader.files), documentIndex: scan.index} + if scan.index == scan.reader.documentIndex { + reader.selectedVersion = scan.reader.selectedVersion + } if err := reader.initRoot(); err != nil { return FontFile{}, err } @@ -501,7 +504,7 @@ func (e *Editor) commitFontSnapshot(ctx context.Context, reader *Reader) error { if err := ctx.Err(); err != nil { return err } - clean := &Reader{Zip: reader.Zip, files: maps.Clone(reader.files), documentIndex: reader.DocumentIndex(), encryption: e.encryption} + clean := &Reader{Zip: reader.Zip, files: maps.Clone(reader.files), documentIndex: reader.DocumentIndex(), encryption: e.encryption, selectedVersion: reader.selectedVersion} if err := clean.initRoot(); err != nil { return err } @@ -544,7 +547,7 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s } removed := make(map[string]bool, len(candidates)) for _, candidate := range candidates { - if key := strings.ToLower(cleanPackagePath(candidate)); key != "" { + if key := cleanPackagePath(candidate); key != "" { removed[key] = true } } @@ -566,7 +569,7 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s return nil } } - for _, name := range reader.fileNamesFold { + for name := range reader.fileNames { if err := ctx.Err(); err != nil { return err } @@ -597,7 +600,7 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s } if node.matchesOFD("FontFile") { location := resolveResourcePath(name, base, strings.TrimSpace(editorImportText(data, node))) - delete(removed, strings.ToLower(cleanPackagePath(location))) + delete(removed, cleanPackagePath(location)) } return slices.ContainsFunc(node.children, retains) } @@ -609,12 +612,12 @@ func pruneExternalFontFiles(ctx context.Context, reader *Reader, candidates ...s return err } for name := range reader.files { - if removed[strings.ToLower(cleanPackagePath(name))] { + if removed[cleanPackagePath(name)] { delete(reader.files, name) } } if reader.Zip != nil { - reader.Zip = &zip.Reader{Comment: reader.Zip.Comment, File: slices.DeleteFunc(slices.Clone(reader.Zip.File), func(file *zip.File) bool { return removed[strings.ToLower(cleanPackagePath(file.Name))] })} + reader.Zip = &zip.Reader{Comment: reader.Zip.Comment, File: slices.DeleteFunc(slices.Clone(reader.Zip.File), func(file *zip.File) bool { return removed[cleanPackagePath(file.Name)] })} } return reader.initRoot() } diff --git a/ofdgo_editor_font_scan.go b/ofdgo_editor_font_scan.go index 388def4..a25de2b 100644 --- a/ofdgo_editor_font_scan.go +++ b/ofdgo_editor_font_scan.go @@ -62,16 +62,17 @@ type editorFontDeclaration struct { // 返回: *editorFontDocument 字体引用, error 读取或结构错误 func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFontDocument, error) { scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration)} - queue := []string{cleanPackagePath(reader.OFD.DocBody[index].DocRoot)} + entry, err := reader.documentEntry(index) + if err != nil { + return nil, err + } + queue := []string{entry} for len(queue) != 0 { if err := ctx.Err(); err != nil { return nil, err } name := queue[0] queue = queue[1:] - if actual := reader.fileNamesFold[strings.ToLower(name)]; actual != "" { - name = actual - } if scan.files[name] != nil { continue } @@ -148,9 +149,6 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon } if value != "" { target := resolveResourcePath(name, base, value) - if actual := reader.fileNamesFold[strings.ToLower(target)]; actual != "" { - target = actual - } file.links = append(file.links, editorFontLink{node: node, attribute: attribute, target: target}) queue = append(queue, target) } @@ -172,6 +170,29 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon // 入参: ctx 取消上下文, changes 按原文件路径保存的修改 // 返回: error 引用扫描或XML修改错误 func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byte) error { + if s.reader.OFD.DocBody[s.index].versioned { + view := *s.reader + if s.index != view.documentIndex { + view.selectedVersion = nil + } + view.documentIndex = s.index + view.documentRoot, view.versionInfo = "", nil + parts, err := view.versionChanges(ctx, changes) + if err != nil { + return err + } + next := *s.reader + next.files = maps.Clone(next.files) + if next.files == nil { + next.files = make(map[string][]byte) + } + maps.Copy(next.files, parts) + if err := next.initRoot(); err != nil { + return err + } + *s.reader = next + return nil + } shared := make(map[string]bool) if s.reader.DocumentCount() > 1 { for index := range s.reader.DocumentCount() { @@ -187,14 +208,14 @@ func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byt } paths := make(map[string]string) for name := range changes { - if shared[strings.ToLower(name)] { + if shared[name] { paths[name] = packageAvailableName(s.reader, changes, name+".font.xml") } } for changed := true; changed; { changed = false for name, file := range s.files { - if paths[name] != "" || !shared[strings.ToLower(name)] { + if paths[name] != "" || !shared[name] { continue } for _, link := range file.links { @@ -236,7 +257,11 @@ func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byt if err != nil { return err } - patches = append(patches, editorXMLPatch{node.start, node.open, fragment[:header.open]}) + if node.open == node.end { + patches = append(patches, editorXMLPatch{node.start, node.end, fragment}) + } else { + patches = append(patches, editorXMLPatch{node.start, node.open, fragment[:header.open]}) + } break } if link.attribute == "" && editorImportText(data, node) == editorImportText(file.data, link.node) { @@ -261,9 +286,9 @@ func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byt changes[name] = data } } - rootName := cleanPackagePath(s.reader.OFD.DocBody[s.index].DocRoot) - if actual := s.reader.fileNamesFold[strings.ToLower(rootName)]; actual != "" { - rootName = actual + rootName, err := s.reader.documentEntry(s.index) + if err != nil { + return err } if target := paths[rootName]; target != "" { data, err := s.reader.readFile("OFD.xml") @@ -275,7 +300,7 @@ func (s *editorFontDocument) apply(ctx context.Context, changes map[string][]byt return err } node := root.childAt("DocBody", s.index).child("DocRoot") - changes[s.reader.fileNamesFold["ofd.xml"]] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, editorFontXMLText("/"+target))}) + changes["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, editorFontXMLText("/"+target))}) } for name, data := range changes { if target := paths[name]; target != "" { diff --git a/ofdgo_editor_import.go b/ofdgo_editor_import.go index 3e21b58..e01a138 100644 --- a/ofdgo_editor_import.go +++ b/ofdgo_editor_import.go @@ -16,6 +16,7 @@ package ofdgo import ( "bytes" + "context" "encoding/xml" "fmt" "io" @@ -229,6 +230,7 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag // ImportPages 将来源当前文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作 // 页面索引从0开始,at可等于当前页数;复制关联模板、注释、签章外观和实际引用的资源 // 不导入来源元数据和目录,指向未选页面的跳转被移除;签名数据仅保留原始凭据,不代表合并后文档有效 +// 目标文档含版本列表时暂不支持导入带签章页面,返回错误且不修改目标 // 返回后可关闭来源Reader,目标原Reader的生命周期要求不变 // 入参: source 来源阅读器, indexes 来源页面索引,不可重复, at 目标插入位置 // 返回: []string 新页面标识, error 错误信息 @@ -455,7 +457,7 @@ func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[strin for _, resource := range e.resources { m.files[resource.name] = nil } - m.documentXML, err = m.readFile(source.ResPath(source.OFD.DocBody[source.documentIndex].DocRoot)) + m.documentXML, err = m.readFile(source.documentRoot) if err != nil { return nil, nil, err } @@ -535,7 +537,7 @@ func (m *editorPageImport) readFile(name string) ([]byte, error) { // outlines 筛选指向导入页面的目录树并复用标准动作迁移 // 返回: []byte 目录片段, error 错误信息 func (m *editorPageImport) outlines() ([]byte, error) { - name := m.reader.ResPath(m.reader.OFD.DocBody[m.reader.documentIndex].DocRoot) + name := m.reader.documentRoot data, err := m.readFile(name) if err != nil { return nil, err @@ -838,7 +840,7 @@ func (m *editorPageImport) copyData(name, relative string, evidence bool) (strin paths = m.evidence relative = path.Join("Signs", "Signed", path.Base(name)) } - key := strings.ToLower(cleanPackagePath(name)) + key := cleanPackagePath(name) if target := paths[key]; target != "" { return "/" + target, nil } @@ -858,7 +860,7 @@ func (m *editorPageImport) copyData(name, relative string, evidence bool) (strin // 入参: name 来源路径, candidate 目标候选路径 // 返回: string 新路径, error 错误信息 func (m *editorPageImport) copyXML(name, candidate string) (string, error) { - key := strings.ToLower(cleanPackagePath(name)) + key := cleanPackagePath(name) if target := m.paths[key]; target != "" { return "/" + target, nil } @@ -1188,7 +1190,7 @@ func (m *editorPageImport) attachment(id string) (string, error) { if _, exists := m.attachments[id]; exists { return m.attachmentID(id) } - name := m.reader.ResPath(m.reader.OFD.DocBody[m.reader.documentIndex].DocRoot) + name := m.reader.documentRoot if m.doc.Attachments.Path != "" { name = m.reader.ResPath(m.doc.Attachments.Path) } @@ -1350,7 +1352,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s files = make(map[string][]byte) } maps.Copy(files, m.files) - name := base.reader.ResPath(base.reader.OFD.DocBody[base.reader.documentIndex].DocRoot) + name := base.reader.documentRoot if file, ok := base.reader.packageFile(name); ok { name = cleanPackagePath(file.Name) } @@ -1454,6 +1456,9 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s } files[name] = data if len(signatures) != 0 { + if base.reader.OFD.DocBody[base.reader.documentIndex].versioned { + return nil, fmt.Errorf("signature import into a versioned document is not supported") + } loc, err := mergeImportIndex(base.reader, files, base.document.Signatures, path.Join(m.directory, "Signs", "Signatures.xml"), "Signatures", signatures) if err != nil { return nil, err @@ -1490,7 +1495,16 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s } } } - reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex} + parts, err := base.reader.versionChanges(context.Background(), files) + if err != nil { + return nil, err + } + files = maps.Clone(base.reader.files) + if files == nil { + files = make(map[string][]byte) + } + maps.Copy(files, parts) + reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex, selectedVersion: base.reader.selectedVersion} if err := reader.initRoot(); err != nil { return nil, err } diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index ea309ba..5aa4246 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -113,6 +113,7 @@ type PageCapabilities struct { // Editor 打开当前文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑 // 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名 // 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader +// 版本共享文件在提交修改时隔离,保留其他版本及其文件清单 // 返回: *Editor 编辑器, error 错误信息 func (r *Reader) Editor() (*Editor, error) { _, err := r.Doc() @@ -419,7 +420,7 @@ func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bo // 返回: error 引用歧义或读取错误 func (e *Editor) checkDuplicateObjectReferences(duplicates map[string]bool) error { reader := e.source.reader - for _, name := range reader.fileNamesFold { + for name := range reader.fileNames { if !strings.EqualFold(path.Ext(name), ".xml") { continue } diff --git a/ofdgo_editor_outline.go b/ofdgo_editor_outline.go index 97c1a8f..fa04d70 100644 --- a/ofdgo_editor_outline.go +++ b/ofdgo_editor_outline.go @@ -319,7 +319,7 @@ func (e *Editor) outlineXML() ([]byte, error) { } if e.source != nil { reader := e.source.reader - data, err := reader.readFile(reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot)) + data, err := reader.readFile(reader.documentRoot) if err != nil { return nil, err } diff --git a/ofdgo_editor_package.go b/ofdgo_editor_package.go index da01244..6e9d75c 100644 --- a/ofdgo_editor_package.go +++ b/ofdgo_editor_package.go @@ -17,6 +17,7 @@ package ofdgo import ( "archive/zip" "bytes" + "context" "encoding/xml" "fmt" "io" @@ -31,7 +32,11 @@ import ( // 入参: progress 保存进度回调,预览时为nil // 返回: map[string][]byte 替换及新增条目, error 错误信息 func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) { - return e.sourcePartsReferences(progress, nil) + parts, err := e.sourcePartsReferences(progress, nil) + if err != nil { + return nil, err + } + return e.source.reader.versionChanges(context.Background(), parts) } // sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用 @@ -49,6 +54,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri var stager editorPageStager source := e.source reader := source.reader + stage = stage && !reader.OFD.DocBody[reader.documentIndex].versioned pageRefs := make([]Page, len(e.pages)) originalCount := 0 for i, page := range e.pages { @@ -111,7 +117,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri commonData := source.document.CommonData for _, name := range append(slices.Clone(commonData.DocumentRes), commonData.PublicRes...) { resourceFiles = slices.DeleteFunc(resourceFiles, func(file string) bool { - return strings.EqualFold(file, reader.ResPath(name)) + return file == reader.ResPath(name) }) } if len(fonts)+len(images)+len(spaces) != 0 { @@ -134,7 +140,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri } pagesChanged := !slices.Equal(pageRefs, source.document.Pages.Page) if pagesChanged || len(resourceFiles) != 0 || len(parts) != 0 && e.maxID != source.document.CommonData.MaxUnitID { - name := reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot) + name := reader.documentRoot data, err := reader.readFile(name) if err != nil { return nil, err @@ -232,7 +238,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri parts["OFD.xml"] = data } if e.outlines != nil { - name := reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot) + name := reader.documentRoot data, ok := parts[name] if !ok { var err error @@ -586,6 +592,14 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress if err := e.compressResourceReferences(parts, reader, removed, generated); err != nil { return 0, err } + ctx := context.Background() + if e.output != nil { + ctx = e.output.ctx + } + parts, err = reader.versionChanges(ctx, parts) + if err != nil { + return 0, err + } if len(parts) != 0 || len(removed) != 0 || e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected { name := "OFD.xml" if file, ok := reader.packageFile(name); ok { @@ -610,7 +624,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress } maps.Copy(remaining, parts) for name := range remaining { - if removed[strings.ToLower(cleanPackagePath(name))] { + if removed[cleanPackagePath(name)] { delete(remaining, name) } } @@ -631,7 +645,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress continue } name := cleanPackagePath(file.Name) - if removed[strings.ToLower(name)] { + if removed[name] { continue } if data, ok := remaining[name]; ok { @@ -710,8 +724,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) { maps.Copy(reader.files, parts) reader.encryption = e.encryption for name := range reader.pageHeaderCache { - actual := reader.fileNamesFold[strings.ToLower(name)] - if _, changed := parts[actual]; changed { + if _, changed := parts[name]; changed { delete(reader.pageHeaderCache, name) } } @@ -725,7 +738,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) { files = make(map[string][]byte) } maps.Copy(files, parts) - reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex} + reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption, documentIndex: e.source.reader.documentIndex, selectedVersion: e.source.reader.selectedVersion} if err := reader.initRoot(); err != nil { return nil, err } @@ -733,8 +746,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) { return nil, err } for name, header := range e.source.reader.pageHeaderCache { - actual := e.source.reader.fileNamesFold[strings.ToLower(name)] - if _, changed := parts[actual]; !changed { + if _, changed := parts[name]; !changed { reader.pageHeaderCache[name] = header } } diff --git a/ofdgo_editor_pages.go b/ofdgo_editor_pages.go index fb1ea6a..205135f 100644 --- a/ofdgo_editor_pages.go +++ b/ofdgo_editor_pages.go @@ -23,6 +23,7 @@ import ( // prunePageReferences 在输出快照中删除已移除页面的标准引用,不修改历史快照或其他文档 // 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据 +// 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据 // 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil // 返回: error 读取或解析错误 func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error { @@ -50,8 +51,8 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri bookmarks[bookmark.Name] = true } } - queue := []string{reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot)} - if name := e.source.document.Signatures; name != "" { + queue := []string{reader.documentRoot} + if name := e.source.document.Signatures; name != "" && !reader.OFD.DocBody[reader.documentIndex].versioned { queue = append(queue, reader.ResPath(name)) } seen := make(map[string]bool) @@ -63,10 +64,10 @@ func (e *Editor) prunePageReferences(parts map[string][]byte, generated map[stri } name := queue[0] queue = queue[1:] - if seen[strings.ToLower(name)] { + if seen[name] { continue } - seen[strings.ToLower(name)] = true + seen[name] = true data, ok := parts[name] if !ok { var err error diff --git a/ofdgo_editor_reference.go b/ofdgo_editor_reference.go index 24f9562..4f03c72 100644 --- a/ofdgo_editor_reference.go +++ b/ofdgo_editor_reference.go @@ -164,7 +164,11 @@ func (s *editorReferenceScan) accept(token xml.Token) bool { s.refs.reference(s.name, s.base, attr.Name.Local, attr.Value, s.resource) } if s.objects != nil && editorObjectReference(attr.Name.Local) { - s.objects[attr.Value] = true + id := attr.Value + if key := editorResourceID(id); key != "" { + id = key + } + s.objects[id] = true } if attr.Name.Local == "Font" { usage = s.refs.fontUsage(attr.Value) @@ -189,6 +193,14 @@ func (s *editorReferenceScan) accept(token xml.Token) bool { return false } s.refs.reference(s.name, s.base, token.Name.Local, s.text.String(), s.resource) + if len(s.stack) == 3 && s.stack[0] == "DocVersion" && s.stack[1] == "FileList" && token.Name.Local == "File" { + if name := editorResourceLocation(s.name, "", s.text.String()); name != "" { + if s.refs.versions == nil { + s.refs.versions = make(map[string]bool) + } + s.refs.versions[name] = true + } + } if s.objects != nil && (token.Name.Local == "Thumbnail" || token.Name.Local == "Substitution") { if id := editorResourceID(s.text.String()); id != "" { s.objects[id] = true @@ -219,6 +231,12 @@ func (s *editorReferenceScan) accept(token xml.Token) bool { func (r *editorResourceRefs) merge(other *editorResourceRefs) { maps.Copy(r.ids, other.ids) maps.Copy(r.files, other.files) + if len(other.versions) != 0 { + if r.versions == nil { + r.versions = make(map[string]bool) + } + maps.Copy(r.versions, other.versions) + } for id, usage := range other.fonts { r.fontUsage(id).merge(usage, false) } diff --git a/ofdgo_editor_resources.go b/ofdgo_editor_resources.go index 4ff6f04..272cf6e 100644 --- a/ofdgo_editor_resources.go +++ b/ofdgo_editor_resources.go @@ -36,9 +36,10 @@ const ( // editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活 type editorResourceRefs struct { - ids map[string]bool - files map[string]bool - fonts map[string]*editorFontUsage + ids map[string]bool + files map[string]bool + fonts map[string]*editorFontUsage + versions map[string]bool } // compactSourceReferences 合并自产页面的已知引用,其他条目仍完整扫描 @@ -135,6 +136,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito if err != nil { return nil, nil } + preserved := refs.versions[cleanPackagePath(name)] var patches []editorXMLPatch for _, group := range root.children { var members []editorXMLPatch @@ -149,18 +151,18 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito continue } id := editorResourceID(node.attr("ID")) - keep := id == "" || refs.ids[id] || field == "MediaFile" && node.attr("Type") != "Image" + keep := preserved || id == "" || refs.ids[id] || field == "MediaFile" && node.attr("Type") != "Image" if file := node.child(field); file != nil { var value string if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil { return nil, nil } if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" { - alternate := strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))) + alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)) if field == "FontFile" { usage := refs.fonts[id] charset := node.attr("Charset") - unsafe := id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate + unsafe := preserved || id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate for _, file := range []string{location, alternate} { if fontFiles[file] == nil { fontFiles[file] = newEditorFontUsage() @@ -200,7 +202,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito if err := progress.report("fonts", i, len(ordered)); err != nil { return nil, err } - key := strings.ToLower(cleanPackagePath(name)) + key := cleanPackagePath(name) usage := fontFiles[key] if e.backends.FontResources == nil || usage == nil || usage.unsafe || refs.files[key] || removed[key] || parts[name] != nil { continue @@ -222,7 +224,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito } maps.Copy(parts, updates) for name := range parts { - if removed[strings.ToLower(cleanPackagePath(name))] { + if removed[cleanPackagePath(name)] { delete(parts, name) } } @@ -279,7 +281,7 @@ func (r *editorResourceRefs) reference(name, base, key, value string, resource b return } r.files[editorResourceLocation(name, base, value)] = true - r.files[strings.ToLower(cleanPackagePath(resolveResourcePath(name, base, value)))] = true + r.files[cleanPackagePath(resolveResourcePath(name, base, value))] = true } // editorResourceReferenceKind 区分标准标识和文件引用,其他文本不参与资源扫描 @@ -340,7 +342,7 @@ func editorResourceNode(root *editorXML, group, kind, id string) *editorXML { return node } -// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,大小写折叠用于保守保留共享文件 +// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,保留路径大小写 // 入参: name 所在XML, base 资源目录, value 路径值 // 返回: string 包内路径 func editorResourceLocation(name, base, value string) string { @@ -349,7 +351,7 @@ func editorResourceLocation(name, base, value string) string { return "" } if strings.HasPrefix(value, "/") { - return strings.ToLower(cleanPackagePath(value)) + return cleanPackagePath(value) } base = strings.ReplaceAll(base, "\\", "/") dir := path.Dir(name) @@ -358,5 +360,5 @@ func editorResourceLocation(name, base, value string) string { } else { dir = path.Join(dir, base) } - return strings.ToLower(cleanPackagePath(path.Join(dir, value))) + return cleanPackagePath(path.Join(dir, value)) } diff --git a/ofdgo_format.go b/ofdgo_format.go index 730b223..6eab94e 100644 --- a/ofdgo_format.go +++ b/ofdgo_format.go @@ -49,7 +49,7 @@ func DetectFormat(source io.ReaderAt, size int64) (string, error) { if err := reader.indexPackage(); err != nil { return "", err } - if reader.fileIndexFold["ofd.xml"] != nil || reader.fileIndexFold["encryptions.xml"] != nil { + if reader.fileNames["OFD.xml"] || reader.fileNames["Encryptions.xml"] { return "ofd", nil } } else { diff --git a/ofdgo_meta.go b/ofdgo_meta.go index 0c3aa1a..6e96948 100644 --- a/ofdgo_meta.go +++ b/ofdgo_meta.go @@ -34,6 +34,24 @@ type DocBody struct { DocInfo DocInfo `xml:"DocInfo"` DocRoot string `xml:"DocRoot"` Signatures string `xml:"Signatures"` + versioned bool +} + +// UnmarshalXML 读取文档入口并记录版本列表是否存在,版本描述在选择时读取 +// 入参: decoder XML解码器, start 起始节点 +// 返回: error 解析错误 +func (body *DocBody) UnmarshalXML(decoder *xml.Decoder, start xml.StartElement) error { + type docBody DocBody + var value struct { + docBody + Versions *struct{} `xml:"Versions"` + } + if err := decoder.DecodeElement(&value, &start); err != nil { + return err + } + *body = DocBody(value.docBody) + body.versioned = value.Versions != nil + return nil } // DocInfo 文档元数据 diff --git a/ofdgo_package.go b/ofdgo_package.go index 835b476..17be51f 100644 --- a/ofdgo_package.go +++ b/ofdgo_package.go @@ -53,7 +53,7 @@ func (e *Editor) packageName(relative string) string { return packageAvailableName(reader, files, path.Join(e.packageDirectory(), relative)) } -// packageAvailableName 仅在包内同名条目冲突时添加序号,忽略大小写差异 +// packageAvailableName 仅在包内同名条目冲突时添加序号,路径区分大小写 // 入参: reader 原包,可为nil, files 待写入条目, name 期望路径 // 返回: string 未占用路径 func packageAvailableName(reader *Reader, files map[string][]byte, name string) string { @@ -66,13 +66,11 @@ func packageAvailableName(reader *Reader, files map[string][]byte, name string) occupied := false if reader != nil { _, occupied = reader.packageFile(candidate) - for file := range reader.files { - occupied = occupied || strings.EqualFold(cleanPackagePath(file), candidate) - } - } - for file := range files { - occupied = occupied || strings.EqualFold(cleanPackagePath(file), candidate) + _, cached := reader.files[candidate] + occupied = occupied || cached } + _, pending := files[candidate] + occupied = occupied || pending if !occupied { return candidate } diff --git a/ofdgo_reader.go b/ofdgo_reader.go index b8644c6..9fc576e 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -48,13 +48,15 @@ type Reader struct { doc *Document relatedPending bool documentIndex int + documentRoot string + selectedVersion *string + versionInfo *DocumentVersionInfo Stamps map[string][]Stamp Annots map[string][]Annotation annotationFiles map[string][]string fileIndex map[string]*zip.File - fileIndexFold map[string]*zip.File files map[string][]byte - fileNamesFold map[string]string + fileNames map[string]bool encryption *encryptionState } @@ -94,7 +96,11 @@ func (r *Reader) DocumentPageCount(index int) (int, error) { if index == r.documentIndex && r.doc != nil { return len(r.doc.Pages.Page), nil } - data, err := r.readFile(r.OFD.DocBody[index].DocRoot) + root, err := r.documentEntry(index) + if err != nil { + return 0, err + } + data, err := r.readFile(root) if err != nil { return 0, err } @@ -151,7 +157,10 @@ func (r *Reader) docStructure() (*Document, error) { return nil, fmt.Errorf("no docbody found") } docAttr := r.OFD.DocBody[r.documentIndex] - docRootPath := docAttr.DocRoot + docRootPath, err := r.DocumentRoot() + if err != nil { + return nil, err + } r.RootDir = path.Dir(docRootPath) data, err := r.readFile(docRootPath) if err != nil { @@ -162,7 +171,7 @@ func (r *Reader) docStructure() (*Document, error) { return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err) } if doc.Signatures == "" { - doc.Signatures = docAttr.Signatures + doc.Signatures = r.versionSignatures(docAttr.Signatures) } for _, res := range doc.CommonData.DocumentRes { r.loadRes(res) @@ -459,6 +468,11 @@ func (r *Reader) initRoot() error { return fmt.Errorf("failed to unmarshal ofd.xml: %w", err) } r.OFD = &ofd + r.doc, r.versionInfo = nil, nil + r.documentRoot, r.RootDir = "", "" + r.relatedPending, r.fontResourcesRead = false, false + r.imageCatalog = nil + r.Stamps, r.Annots, r.annotationFiles = nil, nil, nil r.ResMap = make(map[string]string) r.resourcesRead = make(map[string]bool) r.mediaCache = make(map[string]MultiMedia) @@ -472,7 +486,7 @@ func (r *Reader) initRoot() error { return nil } -// indexPackage 建立包路径索引,拒绝会使显示与验签产生歧义的同名条目 +// indexPackage 按大小写敏感路径建立包索引,拒绝规范化后重复的ZIP条目 // 返回: error 错误信息 func (r *Reader) indexPackage() error { count := 0 @@ -480,8 +494,7 @@ func (r *Reader) indexPackage() error { count = len(r.Zip.File) } r.fileIndex = make(map[string]*zip.File, count) - r.fileIndexFold = make(map[string]*zip.File, count) - r.fileNamesFold = make(map[string]string, count+len(r.files)) + r.fileNames = make(map[string]bool, count+len(r.files)) if r.Zip != nil { for _, f := range r.Zip.File { if f.FileInfo().IsDir() { @@ -491,24 +504,18 @@ func (r *Reader) indexPackage() error { if !validPackagePath(name) { return fmt.Errorf("invalid package path: %q", f.Name) } - fold := strings.ToLower(name) - if previous, ok := r.fileIndexFold[fold]; ok { + if previous, ok := r.fileIndex[name]; ok { return fmt.Errorf("ambiguous package paths: %q and %q", previous.Name, f.Name) } r.fileIndex[name] = f - r.fileIndexFold[fold] = f - r.fileNamesFold[fold] = name + r.fileNames[name] = true } } for name := range r.files { if name != cleanPackagePath(name) || !validPackagePath(name) { return fmt.Errorf("invalid package path: %q", name) } - fold := strings.ToLower(name) - if previous, ok := r.fileNamesFold[fold]; ok && previous != name { - return fmt.Errorf("ambiguous package paths: %q and %q", previous, name) - } - r.fileNamesFold[fold] = name + r.fileNames[name] = true } return nil } @@ -544,16 +551,10 @@ func (r *Reader) readFileView(name string) ([]byte, error) { // 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息 func (r *Reader) readFileData(name string) ([]byte, bool, error) { name = cleanPackagePath(name) - if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok { - name = actual - } if data, ok := r.files[name]; ok { return data, true, nil } if f, ok := r.packageFile(name); ok { - if data, ok := r.files[cleanPackagePath(f.Name)]; ok { - return data, true, nil - } data, err := readZipFile(f) return data, false, err } @@ -565,16 +566,10 @@ func (r *Reader) readFileData(name string) ([]byte, bool, error) { // 返回: io.ReadCloser 文件流, error 错误信息 func (r *Reader) openFile(name string) (io.ReadCloser, error) { name = cleanPackagePath(name) - if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok { - name = actual - } if data, ok := r.files[name]; ok { return io.NopCloser(bytes.NewReader(data)), nil } if f, ok := r.packageFile(name); ok { - if data, ok := r.files[cleanPackagePath(f.Name)]; ok { - return io.NopCloser(bytes.NewReader(data)), nil - } return f.Open() } return nil, fmt.Errorf("file not found: %s", name) @@ -593,13 +588,8 @@ func cleanPackagePath(name string) string { // 入参: name 文件路径 // 返回: *zip.File 压缩包文件, bool 是否存在 func (r *Reader) packageFile(name string) (*zip.File, bool) { - if f, ok := r.fileIndex[name]; ok { - return f, true - } - if f, ok := r.fileIndexFold[strings.ToLower(name)]; ok { - return f, true - } - return nil, false + f, ok := r.fileIndex[cleanPackagePath(name)] + return f, ok } // readZipFile 读取zip文件内容 diff --git a/ofdgo_reader_dependencies.go b/ofdgo_reader_dependencies.go index 76ea1ab..b636533 100644 --- a/ofdgo_reader_dependencies.go +++ b/ofdgo_reader_dependencies.go @@ -26,7 +26,7 @@ import ( // documentFiles 按标准路径遍历独立文档依赖,签名摘要引用不作为内容依赖 // 入参: ctx 取消上下文, index 文档索引 -// 返回: map[string]bool 大小写折叠的文件路径, error 读取或取消错误 +// 返回: map[string]bool 包内文件路径, error 读取或取消错误 func (r *Reader) documentFiles(ctx context.Context, index int) (map[string]bool, error) { if err := ctx.Err(); err != nil { return nil, err @@ -67,15 +67,13 @@ func (r *Reader) documentFiles(ctx context.Context, index int) (map[string]bool, if err := ctx.Err(); err != nil { return nil, err } - name := queue[0] + actual := queue[0] queue = queue[1:] - key := strings.ToLower(name) - if seen[key] { + if seen[actual] { continue } - seen[key] = true - actual, exists := r.fileNamesFold[key] - if !exists { + seen[actual] = true + if !r.fileNames[actual] { continue } input, err := r.openFile(actual) diff --git a/ofdgo_sign_policy.go b/ofdgo_sign_policy.go index dcd02f3..0ce8156 100644 --- a/ofdgo_sign_policy.go +++ b/ofdgo_sign_policy.go @@ -219,7 +219,7 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath covered := make(map[string]bool) for _, ref := range report.References { if ref.Checked && ref.OK { - covered[r.signatureCoveragePath(ref.Path)] = true + covered[cleanPackagePath(ref.Path)] = true } } for name := range covered { @@ -232,12 +232,12 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath report.CoverageChecked, report.CoverageOK = true, true required := make(map[string]bool) for _, name := range o.Policy.RequiredFiles { - required[r.signatureCoveragePath(name)] = true + required[cleanPackagePath(name)] = true } if o.Policy.RequireDocumentCoverage { - required[r.signatureCoveragePath("OFD.xml")] = true - excluded := map[string]bool{r.signatureCoveragePath(listPath): true} - name := r.signatureCoveragePath(signatureRefPath(listPath, report.BaseLoc)) + required["OFD.xml"] = true + excluded := map[string]bool{cleanPackagePath(listPath): true} + name := cleanPackagePath(signatureRefPath(listPath, report.BaseLoc)) data, err := r.readFile(name) if err != nil { report.CoverageOK = false @@ -252,9 +252,9 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath } excluded[name] = true if file.SignedValue != "" { - excluded[r.signatureCoveragePath(signatureRefPath(name, file.SignedValue))] = true + excluded[cleanPackagePath(signatureRefPath(name, file.SignedValue))] = true } - root := path.Dir(r.signatureCoveragePath(o.DocRoot)) + root := path.Dir(cleanPackagePath(o.DocRoot)) include := func(name string) { if (root == "." || strings.HasPrefix(name, root+"/")) && !excluded[name] && !strings.HasSuffix(name, "/") { required[name] = true @@ -281,22 +281,11 @@ func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath report.CoverageOK = report.CoverageOK && len(report.UncoveredFiles) == 0 } -// signatureCoveragePath 复用阅读器大小写索引获取同一文件身份 -// 入参: name 包内路径 -// 返回: string 实际包内路径 -func (r *Reader) signatureCoveragePath(name string) string { - name = cleanPackagePath(name) - if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok { - return actual - } - return name -} - // signatureCoverageReferences 收集标准OFD引用的传递闭包且包含跨文档资源 // 入参: root 文档入口, required 待补充的必需文件集合 // 返回: error 引用文件无法读取或解析时的错误 func (r *Reader) signatureCoverageReferences(root string, required map[string]bool) error { - queue := []string{r.signatureCoveragePath(root)} + queue := []string{cleanPackagePath(root)} seen := make(map[string]bool) for len(queue) != 0 { name := queue[0] @@ -332,7 +321,7 @@ func (r *Reader) signatureCoverageReferences(root string, required map[string]bo if resource { resBase = base } - resolved := r.signatureCoveragePath(resolveResourcePath(name, resBase, loc)) + resolved := cleanPackagePath(resolveResourcePath(name, resBase, loc)) required[resolved] = true if !seen[resolved] { queue = append(queue, resolved) diff --git a/ofdgo_sign_verify.go b/ofdgo_sign_verify.go index 8213144..b8992d9 100644 --- a/ofdgo_sign_verify.go +++ b/ofdgo_sign_verify.go @@ -299,11 +299,22 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti } } body := r.OFD.DocBody[index] - options.DocIndex, options.DocRoot = index, r.signatureCoveragePath(body.DocRoot) if index == r.documentIndex && r.doc != nil && r.doc.Signatures == "" { return nil, nil } - data, err := r.readFile(body.DocRoot) + view := &Reader{Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index} + if index == r.documentIndex { + view.selectedVersion = r.selectedVersion + } + if err := view.initRoot(); err != nil { + return nil, err + } + docRoot, err := view.DocumentRoot() + if err != nil { + return nil, err + } + options.DocIndex, options.DocRoot = index, docRoot + data, err := view.readFile(docRoot) if err != nil { return nil, err } @@ -312,16 +323,12 @@ func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOpti return nil, err } if doc.Signatures == "" { - doc.Signatures = body.Signatures + doc.Signatures = view.versionSignatures(body.Signatures) } if doc.Signatures == "" { return nil, nil } - view := &Reader{Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index} - if err := view.initRoot(); err != nil { - return nil, err - } - view.doc, view.RootDir = &doc, path.Dir(body.DocRoot) + view.doc, view.RootDir = &doc, path.Dir(docRoot) sigListPath := view.ResPath(doc.Signatures) data, err = view.readFile(sigListPath) if err != nil { diff --git a/ofdgo_sign_write.go b/ofdgo_sign_write.go index 9325dd4..c391be8 100644 --- a/ofdgo_sign_write.go +++ b/ofdgo_sign_write.go @@ -78,6 +78,7 @@ type signatureWriteInfo struct { } // SignPackage 签署最终OFD包,失败时返回nil且不修改输入 +// 含版本列表的文档需先通过ExtractVersion提取后签署 // 入参: data 最终包字节, options 签署选项 // 返回: []byte 签署后的完整包, error 错误信息 func SignPackage(data []byte, options SignatureWriteOptions) ([]byte, error) { @@ -89,6 +90,7 @@ func SignPackage(data []byte, options SignatureWriteOptions) ([]byte, error) { } // SignEditorTo 将编辑器序列化为最终文件后签署,保留或显式覆盖加密策略 +// 含版本列表的文档需先通过ExtractVersion提取后签署 // 不修改编辑器状态;OnWriteProgress可在准备、签署和加密阶段取消,准备失败不写入目标 // 输出目标不得覆盖源文件;目标自身写入失败时调用方应丢弃部分输出 // 入参: writer 输出目标, editor 编辑器, options 签署选项 @@ -121,6 +123,7 @@ func SignEditorTo(writer io.Writer, editor *Editor, options SignatureWriteOption } // SignTo 从阅读器包内容创建独立快照,完成签署和回验后写出 +// 含版本列表的文档需先通过ExtractVersion提取后签署 // 不采用可变的Doc/OFD模型;调用方应先将Editor写出为最终包 // 准备失败不写入writer;writer自身失败可能已写入部分输出,不保证目标原子替换 // 入参: writer 独立的输出目标,不得覆盖源文件, options 签署选项 @@ -147,6 +150,9 @@ func signatureWriteOutput(r *Reader, options SignatureWriteOptions) ([]byte, err if r == nil { return nil, fmt.Errorf("nil signature reader") } + if r.documentIndex >= 0 && r.documentIndex < r.DocumentCount() && r.OFD.DocBody[r.documentIndex].versioned { + return nil, fmt.Errorf("in-package version signing is not supported; extract the version before signing") + } encryption := options.Encryption if encryption == nil && r.encryption != nil { encryption = r.encryption.options @@ -275,7 +281,7 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption return nil, err } for name := range parts { - if removed[strings.ToLower(cleanPackagePath(name))] { + if removed[cleanPackagePath(name)] { delete(parts, name) } } @@ -364,7 +370,7 @@ func signatureWritePackage(parts map[string][]byte, options SignatureWriteOption directory = path.Join(path.Dir(listPath), "Sign_"+strconv.Itoa(n)) occupied := false for name := range parts { - if strings.EqualFold(name, directory) || strings.HasPrefix(strings.ToLower(name), strings.ToLower(directory)+"/") { + if name == directory || strings.HasPrefix(name, directory+"/") { occupied = true break } @@ -474,7 +480,7 @@ func signatureWriteIDs(list *Signatures, listPath string, parts map[string][]byt var sequence signatureIDSequence sequence.observe(list.MaxSignID) add := func(id string) error { - if !signatureWriteIDValid(id) || used[id] { + if !ofdXMLIDValid(id) || used[id] { return fmt.Errorf("invalid or duplicate signature ID: %s", id) } used[id] = true @@ -534,14 +540,7 @@ func signatureWriteXML(name string, value any) ([]byte, error) { func signatureWriteAvailable(parts map[string][]byte, name string) string { original := name for i := 1; ; i++ { - found := false - for existing := range parts { - if strings.EqualFold(existing, name) { - found = true - break - } - } - if !found { + if _, found := parts[name]; !found { return name } name = path.Join(path.Dir(original), strconv.Itoa(i)+"_"+path.Base(original)) diff --git a/ofdgo_sign_write_stamp.go b/ofdgo_sign_write_stamp.go index 55fe10a..6330f30 100644 --- a/ofdgo_sign_write_stamp.go +++ b/ofdgo_sign_write_stamp.go @@ -18,7 +18,6 @@ import ( "fmt" "math" "strconv" - "unicode/utf8" ) // SignaturePageStamps 生成所选页面相同毫米坐标的印章位置并检查页内范围 @@ -110,7 +109,7 @@ func signatureWriteStamps(r *Reader, stamps []SignatureStamp, seal bool) ([]Sign if stamp.ID == "" { continue } - if !signatureWriteIDValid(stamp.ID) || ids[stamp.ID] { + if !ofdXMLIDValid(stamp.ID) || ids[stamp.ID] { return nil, fmt.Errorf("invalid or duplicate stamp ID: %s", stamp.ID) } ids[stamp.ID] = true @@ -142,31 +141,6 @@ func signatureWriteStamps(r *Reader, stamps []SignatureStamp, seal bool) ([]Sign return result, nil } -// signatureWriteIDValid 按XML 1.0的NCName规则校验签章标识 -// 入参: id 标识 -// 返回: bool 是否有效 -func signatureWriteIDValid(id string) bool { - 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 判断印章区域是否有限且尺寸为正 // 入参: box 区域 // 返回: bool 是否有效 diff --git a/ofdgo_version.go b/ofdgo_version.go new file mode 100644 index 0000000..472ceaf --- /dev/null +++ b/ofdgo_version.go @@ -0,0 +1,185 @@ +// 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" +) + +// DocumentVersionInfo 文档版本信息,Location、DocRoot及Files中的路径均相对于包根目录 +// Index为版本号,Version为该版本适用的格式版本,CreationDate保留原始日期文本 +type DocumentVersionInfo struct { + ID string + Index int32 + Current bool + Location string + Version string + Name string + CreationDate string + DocRoot string + Files []VersionFile +} + +// VersionFile 文档版本中的文件标识与包内路径 +type VersionFile struct { + ID string + Location string +} + +// DocumentVersions 按入口顺序读取当前文档的版本信息,不切换文档或读取文件清单中的内容 +// 返回独立副本;没有版本列表时返回nil,不以版本号推测默认版本 +// 返回: []DocumentVersionInfo 版本信息, error 读取或描述错误 +func (r *Reader) DocumentVersions() ([]DocumentVersionInfo, error) { + entries, err := r.documentVersionEntries() + if err != nil { + return nil, err + } + for i, entry := range entries { + info, err := r.readDocumentVersion(entry.ID, entry.Location) + if err != nil { + return nil, fmt.Errorf("version %s: %w", entry.ID, err) + } + info.Index, info.Current = entry.Index, entry.Current + entries[i] = info + } + return entries, nil +} + +// documentVersionEntries 读取当前文档的版本入口,不提前加载各版本描述 +// 返回: []DocumentVersionInfo 入口属性, error 读取或描述错误 +func (r *Reader) documentVersionEntries() ([]DocumentVersionInfo, error) { + if r.documentIndex < 0 || r.documentIndex >= r.DocumentCount() { + return nil, fmt.Errorf("no docbody found") + } + data, err := r.readFile("OFD.xml") + if err != nil { + return nil, err + } + root, err := parseEditorXML(data) + if err != nil { + return nil, err + } + body := root.childAt("DocBody", r.documentIndex) + if body == nil { + return nil, fmt.Errorf("document body not found: %d", r.documentIndex) + } + versions := body.child("Versions") + if versions == nil { + return nil, nil + } + if body.childAt("Versions", 1) != nil { + return nil, fmt.Errorf("multiple version lists") + } + var result []DocumentVersionInfo + ids := make(map[string]bool) + for _, node := range versions.children { + if !node.matchesOFD("Version") { + continue + } + id := strings.TrimSpace(node.attr("ID")) + if !ofdXMLIDValid(id) || ids[id] { + return nil, fmt.Errorf("invalid or duplicate version ID %q", id) + } + ids[id] = true + index, err := strconv.ParseInt(strings.TrimSpace(node.attr("Index")), 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid version Index for %s: %w", id, err) + } + current := false + if node.hasAttr("Current") { + switch strings.TrimSpace(node.attr("Current")) { + case "true", "1": + current = true + case "false", "0": + default: + return nil, fmt.Errorf("invalid version Current for %s", id) + } + } + location, err := versionFileLocation("OFD.xml", node.attr("BaseLoc")) + if err != nil { + return nil, fmt.Errorf("version %s: %w", id, err) + } + result = append(result, DocumentVersionInfo{ID: id, Index: int32(index), Current: current, Location: location}) + } + if len(result) == 0 { + return nil, fmt.Errorf("empty version list") + } + return result, nil +} + +// readDocumentVersion 读取版本描述,按描述文件位置解析文件清单和文档入口 +// 入参: id 入口中的版本标识, location 描述文件路径 +// 返回: DocumentVersionInfo 版本信息, error 读取或描述错误 +func (r *Reader) readDocumentVersion(id, location string) (DocumentVersionInfo, error) { + var info DocumentVersionInfo + data, err := r.readFile(location) + if err != nil { + return info, err + } + root, err := parseEditorXML(data) + if err != nil { + return info, err + } + if root.name.Local != "DocVersion" || strings.TrimSpace(root.attr("ID")) != id { + return info, fmt.Errorf("invalid version descriptor %s", location) + } + list, docRoot := root.child("FileList"), root.child("DocRoot") + if list == nil || docRoot == nil { + return info, fmt.Errorf("missing version FileList or DocRoot in %s", location) + } + if root.childAt("FileList", 1) != nil || root.childAt("DocRoot", 1) != nil { + return info, fmt.Errorf("duplicate version FileList or DocRoot in %s", location) + } + entry, err := versionFileLocation(location, editorImportText(data, docRoot)) + if err != nil { + return info, err + } + info = DocumentVersionInfo{ID: id, Location: location, Version: root.attr("Version"), Name: root.attr("Name"), CreationDate: root.attr("CreationDate"), DocRoot: entry} + ids := map[string]bool{id: true} + found := false + for _, node := range list.children { + if !node.matchesOFD("File") { + continue + } + fileID := strings.TrimSpace(node.attr("ID")) + if !ofdXMLIDValid(fileID) || ids[fileID] { + return DocumentVersionInfo{}, fmt.Errorf("invalid or duplicate version file ID %q", fileID) + } + ids[fileID] = true + name, err := versionFileLocation(location, editorImportText(data, node)) + if err != nil { + return DocumentVersionInfo{}, err + } + found = found || name == entry + info.Files = append(info.Files, VersionFile{ID: fileID, Location: name}) + } + if !found { + return DocumentVersionInfo{}, fmt.Errorf("version DocRoot absent from FileList: %s", entry) + } + return info, nil +} + +// versionFileLocation 解析版本描述中的ST_Loc,拒绝空路径和包外路径 +// 入参: owner 所在XML路径, value 引用路径 +// 返回: string 包内路径, error 路径错误 +func versionFileLocation(owner, value string) (string, error) { + name := resolveResourcePath(owner, "", value) + if name == "" || !validPackagePath(name) { + return "", fmt.Errorf("invalid version file location %q", value) + } + return name, nil +} diff --git a/ofdgo_version_edit.go b/ofdgo_version_edit.go new file mode 100644 index 0000000..bfc0446 --- /dev/null +++ b/ofdgo_version_edit.go @@ -0,0 +1,422 @@ +// 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" + "encoding/xml" + "fmt" + "maps" + "path" + "slices" + "strconv" + "strings" +) + +// versionEditFile 保存版本内容XML及标准文件引用,不解析字体、图片或附件数据 +type versionEditFile struct { + data []byte + links []versionEditLink +} + +// versionEditLink 记录文件引用的位置与包内目标 +type versionEditLink struct { + node *editorXML + attribute string + target string +} + +// versionEditGraph 保存一个入口的结构文件与全部直接、间接文件依赖 +type versionEditGraph struct { + files map[string]*versionEditFile + used map[string]bool +} + +// versionEditGraph 沿标准文件引用建立版本依赖图,二进制资源只记录路径 +// 入参: ctx 取消上下文, entry 文档入口, parts 待修改条目 +// 返回: *versionEditGraph 依赖图, error 读取或结构错误 +func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[string][]byte) (*versionEditGraph, error) { + graph := &versionEditGraph{files: make(map[string]*versionEditFile), used: make(map[string]bool)} + queue := []string{entry} + for len(queue) != 0 { + if err := ctx.Err(); err != nil { + return nil, err + } + name := queue[0] + queue = queue[1:] + if graph.files[name] != nil { + continue + } + data, ok := parts[name] + if !ok { + var err error + data, err = r.readFile(name) + if err != nil { + return nil, err + } + } + root, err := parseEditorXML(data) + if err != nil { + return nil, fmt.Errorf("version file %s: %w", name, err) + } + file := &versionEditFile{data: data} + graph.files[name], graph.used[name] = file, true + base := "" + if root.name.Local == "Res" { + base = root.attr("BaseLoc") + } + var visit func(*editorXML) + visit = func(node *editorXML) { + if node != root && !node.matchesOFD(node.name.Local) { + return + } + add := func(attribute, value string, structure bool) { + if strings.TrimSpace(value) == "" { + return + } + target := resolveResourcePath(name, base, value) + file.links = append(file.links, versionEditLink{node: node, attribute: attribute, target: target}) + graph.used[target] = true + if structure { + queue = append(queue, target) + } + } + switch node.name.Local { + case "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "Signatures", "CustomTags", "Extensions": + if len(node.children) == 0 { + add("", editorImportText(data, node), true) + } + case "Page", "TemplatePage", "CompositeGraphicUnit", "Signature": + add("BaseLoc", node.attr("BaseLoc"), true) + case "PageAnnot": + add("FileLoc", node.attr("FileLoc"), true) + case "DrawParam": + add("Link", node.attr("Link"), true) + case "ColorSpace": + add("Profile", node.attr("Profile"), false) + case "FontFile", "MediaFile", "Profile", "SignedValue", "SchemaLoc", "ExtendData": + add("", editorImportText(data, node), false) + case "FileLoc": + add("", editorImportText(data, node), root.name.Local == "Annotations") + } + for _, child := range node.children { + visit(child) + } + } + visit(root) + } + return graph, nil +} + +// versionSharedFiles 汇总其他版本、主入口及其他文档依赖,保护共享条目不被覆盖 +// 入参: ctx 取消上下文 +// 返回: map[string]bool 受保护路径, error 读取或结构错误 +func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error) { + shared := make(map[string]bool) + data, err := r.readFile("OFD.xml") + if err != nil { + return nil, err + } + root, err := parseEditorXML(data) + if err != nil { + return nil, err + } + body := root.childAt("DocBody", r.documentIndex) + if info := body.child("DocInfo"); info != nil { + if cover := info.child("Cover"); cover != nil { + name, err := versionFileLocation("OFD.xml", editorImportText(data, cover)) + if err != nil { + return nil, err + } + shared[name] = true + } + } + for index := range r.DocumentCount() { + if index == r.documentIndex { + continue + } + files, err := r.documentFiles(ctx, index) + if err != nil { + return nil, err + } + maps.Copy(shared, files) + } + versions, err := r.DocumentVersions() + if err != nil { + return nil, err + } + for _, version := range versions { + if r.versionInfo != nil && version.ID == r.versionInfo.ID { + continue + } + shared[version.Location] = true + for _, file := range version.Files { + shared[file.Location] = true + } + } + if r.versionInfo != nil { + entry, err := versionFileLocation("OFD.xml", r.OFD.DocBody[r.documentIndex].DocRoot) + if err != nil { + return nil, err + } + graph, err := r.versionEditGraph(ctx, entry, nil) + if err != nil { + return nil, err + } + maps.Copy(shared, graph.used) + } + return shared, nil +} + +// versionChanges 隔离当前入口修改涉及的共享文件,同步版本清单,不修改输入包或修改集 +// 入参: ctx 取消上下文, changes 待写入条目 +// 返回: map[string][]byte 隔离后的修改集, error 读取、引用或结构错误 +func (r *Reader) versionChanges(ctx context.Context, changes map[string][]byte) (map[string][]byte, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + entry, err := r.DocumentRoot() + if err != nil { + return nil, err + } + if !r.OFD.DocBody[r.documentIndex].versioned || len(changes) == 0 { + return changes, ctx.Err() + } + parts := maps.Clone(changes) + for name, data := range parts { + if original, err := r.readFileView(name); err == nil && bytes.Equal(original, data) { + delete(parts, name) + } + } + if len(parts) == 0 { + return parts, ctx.Err() + } + shared, err := r.versionSharedFiles(ctx) + if err != nil { + return nil, err + } + graph, err := r.versionEditGraph(ctx, entry, parts) + if err != nil { + return nil, err + } + owned := maps.Clone(graph.used) + if r.versionInfo != nil { + for _, file := range r.versionInfo.Files { + owned[file.Location] = true + } + } + paths := make(map[string]string) + reserved := maps.Clone(parts) + var pending []string + allocate := func(name string) { + extension := path.Ext(name) + target := packageAvailableName(r, reserved, strings.TrimSuffix(name, extension)+".edit"+extension) + paths[name], reserved[target] = target, nil + pending = append(pending, name) + } + for _, name := range slices.Sorted(maps.Keys(parts)) { + if name != "OFD.xml" && shared[name] && owned[name] { + allocate(name) + } + } + parents := make(map[string][]string) + for _, name := range slices.Sorted(maps.Keys(graph.files)) { + for _, link := range graph.files[name].links { + parents[link.target] = append(parents[link.target], name) + } + } + for i := 0; i < len(pending); i++ { + for _, name := range parents[pending[i]] { + if shared[name] && paths[name] == "" { + allocate(name) + } + } + } + for name, file := range graph.files { + data, err := file.rewrite(paths) + if err != nil { + return nil, err + } + if !bytes.Equal(data, file.data) { + parts[name] = data + } + } + for source, target := range paths { + data, ok := parts[source] + if !ok { + data, err = r.readFile(source) + if err != nil { + return nil, err + } + } + parts[target] = data + delete(parts, source) + } + if r.versionInfo != nil { + if err := r.versionManifest(parts, paths, graph.used, shared); err != nil { + return nil, err + } + } else if target := paths[entry]; target != "" { + data, ok := parts["OFD.xml"] + if !ok { + data, err = r.readFile("OFD.xml") + if err != nil { + return nil, err + } + } + root, err := parseEditorXML(data) + if err != nil { + return nil, err + } + node := root.childAt("DocBody", r.documentIndex).child("DocRoot") + parts["OFD.xml"] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, editorFontXMLText("/"+target))}) + } + return parts, ctx.Err() +} + +// rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准 +// 入参: paths 原路径到新路径的映射 +// 返回: []byte 更新后的XML, error 属性编码错误 +func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) { + var patches []editorXMLPatch + headers := make(map[*editorXML][]versionEditLink) + for _, link := range file.links { + if paths[link.target] == "" { + continue + } + if link.attribute == "" { + patches = append(patches, editorXMLContent(file.data, link.node, editorFontXMLText("/"+paths[link.target]))) + } else { + headers[link.node] = append(headers[link.node], link) + } + } + for node, links := range headers { + data := file.data[node.start:node.end] + for _, link := range links { + root, err := parseEditorXML(data) + if err != nil { + return nil, err + } + data, err = editorXMLAttribute(data, root, link.attribute, "/"+paths[link.target]) + if err != nil { + return nil, err + } + } + root, err := parseEditorXML(data) + if err != nil { + return nil, err + } + if node.open == node.end { + patches = append(patches, editorXMLPatch{node.start, node.end, data}) + } else { + patches = append(patches, editorXMLPatch{node.start, node.open, data[:root.open]}) + } + } + return editorPatchXML(file.data, patches), nil +} + +// versionManifest 更新所选版本的入口与文件清单,保留已有文件标识及扩展字段 +// 入参: parts 输出条目, paths 隔离路径, used 当前内容依赖, shared 其他入口依赖 +// 返回: error 描述读取或XML编码错误 +func (r *Reader) versionManifest(parts map[string][]byte, paths map[string]string, used, shared map[string]bool) error { + version := r.versionInfo + data, err := r.readFile(version.Location) + if err != nil { + return err + } + root, err := parseEditorXML(data) + if err != nil { + return err + } + list := root.child("FileList") + var patches []editorXMLPatch + listed := make(map[string]bool) + ids := map[string]bool{version.ID: true} + for _, node := range list.children { + if !node.matchesOFD("File") { + continue + } + ids[strings.TrimSpace(node.attr("ID"))] = true + name, err := versionFileLocation(version.Location, editorImportText(data, node)) + if err != nil { + return err + } + if target := paths[name]; target != "" { + name = target + patches = append(patches, editorXMLContent(data, node, editorFontXMLText("/"+target))) + } + listed[name] = true + } + var added []byte + serial := 0 + for _, name := range slices.Sorted(maps.Keys(used)) { + if target := paths[name]; target != "" { + name = target + } + if listed[name] { + continue + } + id := "" + for id == "" || ids[id] { + serial++ + id = "file" + strconv.Itoa(serial) + } + ids[id], listed[name] = true, true + item, err := list.containerXML("File", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}}, editorFontXMLText("/"+name)) + if err != nil { + return err + } + added = append(added, bytes.TrimPrefix(item, []byte(xml.Header))...) + } + if len(added) != 0 { + patches = append(patches, editorXMLPatch{list.close, list.close, added}) + } + if target := paths[version.DocRoot]; target != "" { + patches = append(patches, editorXMLContent(data, root.child("DocRoot"), editorFontXMLText("/"+target))) + } + updated := editorPatchXML(data, patches) + if bytes.Equal(updated, data) { + return nil + } + name := version.Location + if shared[name] { + extension := path.Ext(name) + name = packageAvailableName(r, parts, strings.TrimSuffix(name, extension)+".edit"+extension) + entry, ok := parts["OFD.xml"] + if !ok { + entry, err = r.readFile("OFD.xml") + if err != nil { + return err + } + } + root, err := parseEditorXML(entry) + if err != nil { + return err + } + for _, node := range root.childAt("DocBody", r.documentIndex).child("Versions").children { + if node.matchesOFD("Version") && node.attr("ID") == version.ID { + fragment, err := editorXMLAttribute(entry, node, "BaseLoc", "/"+name) + if err != nil { + return err + } + parts["OFD.xml"] = editorPatchXML(entry, []editorXMLPatch{{node.start, node.end, fragment}}) + break + } + } + } + parts[name] = updated + return nil +} diff --git a/ofdgo_version_extract.go b/ofdgo_version_extract.go new file mode 100644 index 0000000..6f9d03f --- /dev/null +++ b/ofdgo_version_extract.go @@ -0,0 +1,140 @@ +// 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 ( + "archive/zip" + "fmt" +) + +// ExtractVersion 将当前文档的指定版本提取为独立阅读器,可继续编辑或另存,不改变原包 +// 仅保留版本文件清单中的文件并重建单文档入口,不保留版本列表;原签名可能因入口变化而失效 +// 文件内容按需读取,使用期间不得关闭持有输入文件的原阅读器;返回值不关闭输入文件 +// 入参: id 版本标识,空值选择唯一的Current版本,没有默认版本或存在歧义时返回错误 +// 返回: *Reader 版本阅读器, error 选择、读取或文件缺失错误 +func (r *Reader) ExtractVersion(id string) (*Reader, error) { + entries, err := r.documentVersionEntries() + if err != nil { + return nil, err + } + var selected *DocumentVersionInfo + for i := range entries { + entry := &entries[i] + if id != "" && entry.ID == id || id == "" && entry.Current { + if selected != nil { + return nil, fmt.Errorf("multiple current document versions") + } + selected = entry + } + } + if selected == nil { + if id == "" { + return nil, fmt.Errorf("no current document version") + } + return nil, fmt.Errorf("document version not found: %s", id) + } + version, err := r.readDocumentVersion(selected.ID, selected.Location) + if err != nil { + return nil, fmt.Errorf("version %s: %w", selected.ID, err) + } + files := make(map[string]bool, len(version.Files)) + for _, file := range version.Files { + if !r.fileNames[file.Location] { + return nil, fmt.Errorf("version %s file not found: %s", selected.ID, file.Location) + } + files[file.Location] = true + } + root, err := r.extractedVersionRoot(version, files) + if err != nil { + return nil, err + } + next := &Reader{Path: r.Path, files: map[string][]byte{"OFD.xml": root}, encryption: r.encryption} + for name := range files { + if name != "OFD.xml" { + if data, ok := r.files[name]; ok { + next.files[name] = data + } + } + } + if r.Zip != nil { + next.Zip = &zip.Reader{Comment: r.Zip.Comment} + for _, file := range r.Zip.File { + name := cleanPackagePath(file.Name) + if name != "OFD.xml" && files[name] && !file.FileInfo().IsDir() { + next.Zip.File = append(next.Zip.File, file) + } + } + } + if err := next.initRoot(); err != nil { + return nil, err + } + if _, err := next.docStructure(); err != nil { + return nil, err + } + return next, nil +} + +// extractedVersionRoot 重建版本副本入口,保留元数据及命名空间,移除其他文档与版本列表 +// 入参: version 版本描述, files 版本包含的文件 +// 返回: []byte 主入口XML, error 读取或结构错误 +func (r *Reader) extractedVersionRoot(version DocumentVersionInfo, files map[string]bool) ([]byte, error) { + data, err := r.readFile("OFD.xml") + if err != nil { + return nil, err + } + root, err := parseEditorXML(data) + if err != nil { + return nil, err + } + body := root.childAt("DocBody", r.documentIndex) + if body == nil || body.child("DocRoot") == nil { + return nil, fmt.Errorf("document entry not found: %d", r.documentIndex) + } + var patches []editorXMLPatch + for _, child := range root.children { + if child.matchesOFD("DocBody") && child != body { + patches = append(patches, editorXMLPatch{start: child.start, end: child.end}) + } + } + entry := body.child("DocRoot") + patches = append(patches, editorXMLContent(data, entry, editorFontXMLText(version.DocRoot))) + if versions := body.child("Versions"); versions != nil { + patches = append(patches, editorXMLPatch{start: versions.start, end: versions.end}) + } + references := []*editorXML{body.child("Signatures")} + if info := body.child("DocInfo"); info != nil { + references = append(references, info.child("Cover")) + } + for _, node := range references { + if node == nil { + continue + } + name := resolveResourcePath("OFD.xml", "", editorImportText(data, node)) + if files[name] { + patches = append(patches, editorXMLContent(data, node, editorFontXMLText("/"+name))) + } else { + patches = append(patches, editorXMLPatch{start: node.start, end: node.end}) + } + } + data = editorPatchXML(data, patches) + if version.Version != "" { + root, err = parseEditorXML(data) + if err != nil { + return nil, err + } + data, err = editorXMLAttribute(data, root, "Version", version.Version) + } + return data, err +} diff --git a/ofdgo_version_reader.go b/ofdgo_version_reader.go new file mode 100644 index 0000000..c7ca97f --- /dev/null +++ b/ofdgo_version_reader.go @@ -0,0 +1,127 @@ +// 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" + +// DocumentRoot 获取当前阅读入口的包内路径,不改写OFD中的主入口 +// 未显式选择时使用唯一的Current版本;没有默认版本时使用主入口,多个默认版本返回错误 +// 返回: string 阅读入口, error 版本选择或描述错误 +func (r *Reader) DocumentRoot() (string, error) { + if r.documentRoot != "" { + return r.documentRoot, nil + } + if r.documentIndex < 0 || r.documentIndex >= r.DocumentCount() { + return "", fmt.Errorf("no docbody found") + } + body := r.OFD.DocBody[r.documentIndex] + if r.selectedVersion != nil && *r.selectedVersion == "" || !body.versioned && r.selectedVersion == nil { + root, err := versionFileLocation("OFD.xml", body.DocRoot) + if err == nil { + r.documentRoot = root + } + return root, err + } + entries, err := r.documentVersionEntries() + if err != nil { + return "", err + } + var selected *DocumentVersionInfo + for i := range entries { + entry := &entries[i] + if r.selectedVersion != nil && entry.ID == *r.selectedVersion || r.selectedVersion == nil && entry.Current { + if selected != nil { + return "", fmt.Errorf("multiple current document versions") + } + selected = entry + } + } + if selected == nil { + if r.selectedVersion != nil { + return "", fmt.Errorf("document version not found: %s", *r.selectedVersion) + } + root, err := versionFileLocation("OFD.xml", body.DocRoot) + if err == nil { + r.documentRoot = root + } + return root, err + } + info, err := r.readDocumentVersion(selected.ID, selected.Location) + if err != nil { + return "", fmt.Errorf("version %s: %w", selected.ID, err) + } + info.Index, info.Current = selected.Index, selected.Current + r.documentRoot, r.versionInfo = info.DocRoot, &info + return r.documentRoot, nil +} + +// DocumentVersionID 获取当前阅读的版本标识,主入口返回空字符串 +// 返回: string 版本标识, error 版本选择或描述错误 +func (r *Reader) DocumentVersionID() (string, error) { + if _, err := r.DocumentRoot(); err != nil { + return "", err + } + if r.versionInfo == nil { + return "", nil + } + return r.versionInfo.ID, nil +} + +// DocumentVersion 创建指定版本的独立阅读器,保留整个包,不改变当前阅读器或共享可变缓存 +// 返回值不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器 +// 入参: id 版本标识,空值显式选择主入口;Document可重新按Current选择默认版本 +// 返回: *Reader 版本阅读器, error 选择或读取错误 +func (r *Reader) DocumentVersion(id string) (*Reader, error) { + next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: r.documentIndex, selectedVersion: &id} + if err := next.initRoot(); err != nil { + return nil, err + } + if _, err := next.Doc(); err != nil { + return nil, err + } + return next, nil +} + +// documentEntry 获取指定文档的阅读入口,当前文档保留显式选择,其他文档使用默认版本 +// 入参: index 文档索引 +// 返回: string 包内路径, error 选择错误 +func (r *Reader) documentEntry(index int) (string, error) { + if index == r.documentIndex { + return r.DocumentRoot() + } + view := *r + view.documentIndex, view.documentRoot = index, "" + view.selectedVersion, view.versionInfo = nil, nil + return view.DocumentRoot() +} + +// versionSignatures 按OFD入口解析包级签名路径,选中版本仅使用文件清单内的签名入口 +// 入参: location OFD入口中的签名路径 +// 返回: string 签名路径,不属于该版本时为空 +func (r *Reader) versionSignatures(location string) string { + if location == "" { + return "" + } + name := resolveResourcePath("OFD.xml", "", location) + if r.versionInfo == nil { + return "/" + name + } + for _, file := range r.versionInfo.Files { + if file.Location == name { + return "/" + name + } + } + return "" +} diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 38ae341..d9fb70a 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -190,7 +190,7 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) { return 0, err } for _, name := range slices.Sorted(maps.Keys(parts)) { - if removed[strings.ToLower(cleanPackagePath(name))] { + if removed[cleanPackagePath(name)] { continue } if err := e.writeOutputEntry(archive, zip.FileHeader{Name: name, Method: zip.Deflate}, parts[name]); err != nil { diff --git a/ofdgo_xml.go b/ofdgo_xml.go index 805f1da..a66580a 100644 --- a/ofdgo_xml.go +++ b/ofdgo_xml.go @@ -14,6 +14,8 @@ package ofdgo +import "unicode/utf8" + // 旧版与2016版OFD命名空间 const ( ofdNamespaceLegacy = "http://www.ofdspec.org" @@ -46,3 +48,28 @@ func classifyOFDNamespace(namespace string) ofdXMLNamespace { return ofdXMLUnknown } } + +// ofdXMLIDValid 按XML 1.0的NCName规则校验xs:ID,不用于数值型ST_ID +// 入参: id 标识 +// 返回: bool 是否有效 +func ofdXMLIDValid(id string) bool { + 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 +}