// 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" "encoding/xml" "fmt" "maps" "path" "slices" "strings" "time" ) // AddAnnotation 新增标准注解并分配独立标识,外观中的基本对象使用注解局部毫米坐标 // Appearance.Objects为空时使用分类对象集合,输入对象及资源不被修改 // 入参: index 页面索引, annotation 注解内容,ID由编辑器生成,空LastModDate使用当前日期 // 返回: string 注解标识, error 错误信息 func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) { var id string err := e.Transaction(func(next *Editor) error { var data []byte var err error id, data, err = next.prepareAnnotation(index, annotation) if err != nil { return err } return next.appendAnnotations(index, data) }) if err != nil { return "", err } return id, nil } // prepareAnnotation 校验注解并分配外观标识,提交由调用方事务或独立导入文档负责 // 入参: index 页面索引, annotation 注解内容 // 返回: string 注解标识, []byte 标准XML, error 校验错误 func (e *Editor) prepareAnnotation(index int, annotation Annotation) (string, []byte, error) { if index < 0 || index >= len(e.pages) { return "", nil, fmt.Errorf("page index %d out of range", index) } if !slices.Contains([]string{"Link", "Path", "Highlight", "Stamp", "Watermark"}, annotation.Type) { return "", nil, fmt.Errorf("invalid annotation type %q", annotation.Type) } if _, err := creationBox(annotation.Appearance.Boundary); err != nil { return "", nil, err } if err := e.prepareSourceIDs(); err != nil { return "", nil, err } id := e.nextID() objects := annotation.Appearance.Objects if len(objects) == 0 { objects = nil for _, object := range annotation.Appearance.TextObject { objects = append(objects, GraphicObject{Type: "TextObject", TextObject: object}) } for _, object := range annotation.Appearance.PathObject { objects = append(objects, GraphicObject{Type: "PathObject", PathObject: object}) } for _, object := range annotation.Appearance.ImageObject { objects = append(objects, GraphicObject{Type: "ImageObject", ImageObject: object}) } if len(annotation.Appearance.CompositeGraphicUnit) != 0 { return "", nil, fmt.Errorf("use composite copy to insert existing complex content") } } var content []byte for _, object := range objects { prepared, err := e.prepareObject(e.nextID(), cloneEditorData(object)) if err != nil { return "", nil, err } data, err := editorObjectXML(prepared) if err != nil { return "", nil, err } content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...) } appearance, err := editorXMLContainer("Appearance", ofdAttrs{{Name: xml.Name{Local: "Boundary"}, Value: annotation.Appearance.Boundary}}, content) if err != nil { return "", nil, err } date := annotation.LastModDate if date == "" { date = time.Now().Format("2006-01-02") } else if _, err := time.Parse("2006-01-02", date); err != nil { return "", nil, fmt.Errorf("invalid annotation date %q: %w", date, err) } attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Type"}, Value: annotation.Type}, {Name: xml.Name{Local: "Creator"}, Value: annotation.Creator}, {Name: xml.Name{Local: "LastModDate"}, Value: date}} attrs.add("Subtype", annotation.Subtype) attrs.flag("Visible", annotation.Visible) if annotation.NoZoom { attrs.add("NoZoom", "true") } if annotation.NoRotate { attrs.add("NoRotate", "true") } content = nil if annotation.Remark != "" { content = editorXMLText("Remark", annotation.Remark) } if annotation.Parameters != nil && len(annotation.Parameters.Parameter) != 0 { var parameters []byte for _, parameter := range annotation.Parameters.Parameter { if parameter.Name == "" { return "", nil, fmt.Errorf("empty annotation parameter name") } var value bytes.Buffer if err := xml.EscapeText(&value, []byte(parameter.Value)); err != nil { return "", nil, err } data, err := editorXMLContainer("Parameter", ofdAttrs{{Name: xml.Name{Local: "Name"}, Value: parameter.Name}}, value.Bytes()) if err != nil { return "", nil, err } parameters = append(parameters, bytes.TrimPrefix(data, []byte(xml.Header))...) } data, err := editorXMLContainer("Parameters", nil, parameters) if err != nil { return "", nil, err } content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...) } content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...) data, err := editorXMLContainer("Annot", attrs, content) return id, data, err } // Annotation 获取注解的独立内容快照,修改原注解应使用局部编辑方法以保留扩展字段 // 入参: index 页面索引, id 注解标识 // 返回: Annotation 注解内容, error 错误信息 func (e *Editor) Annotation(index int, id string) (Annotation, error) { data, err := e.annotationXML(index, id) var annotation Annotation if err == nil { err = xml.Unmarshal(data, &annotation) } return annotation, err } // annotationXML 读取唯一注解的独立原文,不以类型、大小或可见性筛选 // 入参: index 页面索引, id 注解标识 // 返回: []byte 注解XML, error 错误信息 func (e *Editor) annotationXML(index int, id string) ([]byte, error) { if index < 0 || index >= len(e.pages) { return nil, fmt.Errorf("page index %d out of range", index) } if e.source == nil || e.source.document.Annotations == "" { return nil, fmt.Errorf("page has no annotations") } reader := e.source.reader name := reader.ResPath(e.source.document.Annotations) data, err := reader.readFile(name) if err != nil { return nil, err } root, err := parseEditorXML(data) if err != nil { return nil, err } var result []byte for _, ref := range root.children { if !packageOFDNode(ref, "Page") || ref.attr("PageID") != e.pages[index].ID || ref.child("FileLoc") == nil { continue } file, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, ref.child("FileLoc")))) if err != nil { return nil, err } content, err := reader.readFile(file) if err != nil { return nil, err } page, err := parseEditorXML(content) if err != nil { return nil, err } for _, node := range page.children { if !packageOFDNode(node, "Annot") || node.attr("ID") != id { continue } if result != nil { return nil, fmt.Errorf("ambiguous annotation ID %q", id) } result, err = editorXMLStandalone(content[node.start:node.end], node) if err != nil { return nil, err } } } if result == nil { return nil, fmt.Errorf("annotation %q not found", id) } return result, nil } // appendAnnotations 在独立页面注解文件末尾追加原文,保留原索引及未知内容 // 入参: index 页面索引, annotations 标准注解XML片段 // 返回: error 错误信息 func (e *Editor) appendAnnotations(index int, annotations []byte) error { base := e.source var err error if base == nil { base, err = e.importBase() if err != nil { return err } } reader := base.reader name := reader.ResPath(base.document.Annotations) var data []byte if base.document.Annotations == "" { name = e.packageName("Annotations.xml") data, err = editorXMLContainer("Annotations", nil, nil) } else { if file, ok := reader.packageFile(name); ok { name = cleanPackagePath(file.Name) } data, err = reader.readFile(name) } if err != nil { return err } root, err := parseEditorXML(data) if err != nil { return err } var ref *editorXML var existing string content, err := editorXMLContainer("PageAnnot", nil, nil) if err != nil { return err } for _, item := range root.children { if packageOFDNode(item, "Page") && item.attr("PageID") == e.pages[index].ID { ref = item loc := item.child("FileLoc") if loc == nil { return fmt.Errorf("annotation page has no FileLoc") } file, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, loc))) if err != nil { return err } content, err = reader.readFile(file) if err != nil { return err } existing = file break } } page, err := parseEditorXML(content) if err != nil { return err } added := bytes.TrimPrefix(annotations, []byte(xml.Header)) if page.open == page.end { content = editorPatchXML(content, []editorXMLPatch{editorXMLContent(content, page, added)}) } else { content = editorPatchXML(content, []editorXMLPatch{{page.close, page.close, added}}) } file := existing if file != "" { for _, other := range root.children { if other == ref || !packageOFDNode(other, "Page") || other.child("FileLoc") == nil { continue } shared, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, other.child("FileLoc")))) if err == nil && shared == existing { file = "" break } } } if file == "" { file = e.packageName(path.Join("Annotations", "Page_"+e.pages[index].ID+".xml")) } if ref != nil { if file != existing { loc := ref.child("FileLoc") var value bytes.Buffer _ = xml.EscapeText(&value, []byte("/"+file)) data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, loc, value.Bytes())}) } } else { entry, err := editorXMLContainer("Page", ofdAttrs{{Name: xml.Name{Local: "PageID"}, Value: e.pages[index].ID}}, editorXMLText("FileLoc", "/"+file)) if err != nil { return err } entry = bytes.TrimPrefix(entry, []byte(xml.Header)) if root.open == root.end { data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, root, entry)}) } else { data = editorPatchXML(data, []editorXMLPatch{{root.close, root.close, entry}}) } } parts := map[string][]byte{name: data, file: content} if base.document.Annotations == "" { docName := cleanPackagePath(reader.OFD.DocBody[reader.documentIndex].DocRoot) docData, err := reader.readFile(docName) if err != nil { return err } doc, err := parseEditorXML(docData) if err != nil { return err } position := doc.close for _, child := range doc.children { if slices.Contains([]string{"CustomTags", "Attachments", "Extensions"}, child.name.Local) { position = child.start break } } patch := editorXMLPatch{position, position, editorXMLText("Annotations", "/"+name)} if node := doc.child("Annotations"); node != nil { var value bytes.Buffer _ = xml.EscapeText(&value, []byte("/"+name)) patch = editorXMLContent(docData, node, value.Bytes()) } parts[docName] = editorPatchXML(docData, []editorXMLPatch{patch}) } if err := e.commitAnnotationParts(base, parts); err != nil { return err } pages := maps.Clone(e.source.annotationPages) if pages == nil { pages = make(map[string]bool) } pages[e.pages[index].ID] = true e.source.annotationPages = pages return nil } // annotationFiles 获取仍被引用的注解文件,保留原文件中的未知页引用 // 返回: []string 文件路径 func (e *Editor) annotationFiles() []string { if len(e.source.reader.annotationFiles) == 0 { return nil } removed := make(map[string]bool) for id := range e.source.pages { removed[id] = true } for id := range e.source.annotationPages { removed[id] = true } for _, page := range e.pages { delete(removed, page.ID) } var files []string for name, pages := range e.source.reader.annotationFiles { for _, page := range pages { if !removed[page] { files = append(files, name) break } } } return files } // commitAnnotationParts 提交独立注解包快照,新建文档复用导入包基底 // 入参: base 原包, parts 修改的文件 // 返回: error 错误信息 func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]byte) error { files := maps.Clone(base.reader.files) if files == nil { files = make(map[string][]byte) } for name, data := range parts { if file, ok := base.reader.packageFile(name); ok { name = cleanPackagePath(file.Name) } files[name] = data } reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex} if err := reader.initRoot(); err != nil { return err } doc, err := reader.Doc() if err != nil { return err } before, after := e.source, *base after.reader, after.document = reader, doc if before == nil { for i := range e.resources { resource := &e.resources[i] if resource.font != nil { resource.font = cloneEditorData(resource.font) if resource.name != "" { resource.font.FontFile = "/" + resource.name } } if resource.image != nil { resource.image = cloneEditorData(resource.image) resource.image.MediaFile = "/" + resource.name } } } e.source = &after if change := e.recordChange(); change != nil { change.undo = func(e *Editor) { e.source = before } change.redo = func(e *Editor) { e.source = &after } } return nil } // annotationScope 将注解外观作为内部编辑根容器,保留标准页块结构和坐标约定 // 入参: page 页面索引, path 注解内部路径 // 返回: *Reader 预览, *Renderer 度量器, *editorCompositeNode 根, []*editorCompositeNode 成员, error 错误信息 func (e *Editor) annotationScope(page int, path ObjectPath) (*Reader, *Renderer, *editorCompositeNode, []*editorCompositeNode, error) { data, err := e.annotationXML(page, path.Annotation) if err != nil { return nil, nil, nil, nil, err } annotation, err := parseEditorXML(data) if err != nil { return nil, nil, nil, nil, err } appearance := annotation.child("Appearance") if appearance == nil { return nil, nil, nil, nil, fmt.Errorf("annotation has no appearance") } data, err = editorXMLStandalone(data[appearance.start:appearance.end], appearance) if err != nil { return nil, nil, nil, nil, err } root, err := newEditorCompositeNode(data) if err != nil { return nil, nil, nil, nil, err } root.setStates(e.source.annotationStates[e.pages[page].ID+"/"+path.Annotation]) reader, err := e.Reader() if err != nil { return nil, nil, nil, nil, err } if _, err = reader.PageContentByIndex(page); err != nil { reader.Close() return nil, nil, nil, nil, err } renderer := e.newRenderer(reader) root.parent, root.visible = IdentityMatrix, true scope := root for depth := 0; ; depth++ { members, loadErr := e.compositeMembers(scope, reader, renderer, make(map[string]bool)) if loadErr != nil { err = loadErr break } if depth == len(path.Children) { for i, member := range members { member.index = i } return reader, renderer, root, members, nil } i := path.Children[depth] if i < 0 || i >= len(members) { err = fmt.Errorf("annotation member index %d out of range", i) break } scope = members[i] } reader.Close() return nil, nil, nil, nil, err } // writeAnnotationScope 只替换外观片段并保存会话排版状态,不改写注解元数据和动作 // 入参: page 页面索引, id 注解标识, data 外观XML, states 内部编辑状态 // 返回: error 错误信息 func (e *Editor) writeAnnotationScope(page int, id string, data []byte, states map[string]editorCompositeState) error { revision := e.Revision() err := e.editAnnotations(page, []string{id}, func(original []byte, node *editorXML) ([]byte, error) { fragment, err := editorXMLStandalone(original[node.start:node.end], node) if err != nil { return nil, err } root, err := parseEditorXML(fragment) if err != nil { return nil, err } appearance := root.child("Appearance") return editorAnnotationDate(editorPatchXML(fragment, []editorXMLPatch{{appearance.start, appearance.end, bytes.TrimPrefix(data, []byte(xml.Header))}})) }) if err == nil && revision != e.Revision() { statesByID := maps.Clone(e.source.annotationStates) if statesByID == nil { statesByID = make(map[string]map[string]editorCompositeState) } statesByID[e.pages[page].ID+"/"+id] = maps.Clone(states) e.source.annotationStates = statesByID } return err }