// Copyright 2025-2026 肖其顿 (XIAO QI DUN) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ofdgo import ( "context" "encoding/base64" "fmt" "maps" "math" "slices" "strconv" "strings" "github.com/xiaoqidun/pdfgo" ) // PDF逻辑结构标引使用独立命名空间,图元引用沿用ObjectRef语义 const ( pdfStructureTagType = "OFDGo.PDFStructure" pdfStructureNamespace = "https://ofdgo.aite.me/ns/pdf/structure/1" ) // pdfStructureBindings 保存来源区段与最终页面图元的关联,不记录临时渲染对象 type pdfStructureBindings struct { parents map[*pdfgo.Reader]map[[2]int64]pdfgo.Reference prepared map[*pdfgo.Reader]bool readers []*pdfgo.Reader marked map[pdfStructureContent]bool sequences map[*pdfgo.ContentSequence][]StructureReference order []*pdfgo.ContentSequence contents []*pdfgo.ContentSequence objects map[pdfStructureObject][]StructureReference } // pdfStructureObject 隔离不同文件及页面的整对象引用 type pdfStructureObject struct { reader *pdfgo.Reader page pdfgo.Reference object pdfgo.Reference } // pdfStructureObjectBinding 保存尚未提交的注解外观引用 type pdfStructureObjectBinding struct { source pdfStructureObject ids []string content []pdfStructureBinding } // pdfStructureContent 隔离不同文件、页面及内容流中的标记编号 type pdfStructureContent struct { reader *pdfgo.Reader page pdfgo.Reference stream *pdfgo.Stream mcid int64 } // pdfStructureReferences 保存内容引用与已提交图元的索引 type pdfStructureReferences map[pdfStructureContent][]StructureReference // pdfStructureReferenceSet 按首次出现顺序归并页内图元,单页时不建立页索引 type pdfStructureReferenceSet struct { references []StructureReference pages map[string]int seen map[[2]string]bool } // pdfStructureBinding 保存一个来源区段对应的连续对象位置 type pdfStructureBinding struct { sequence *pdfgo.ContentSequence start int end int } // pdfStructureValues 保存属性值类型及间接对象共享关系 type pdfStructureValues struct { ctx context.Context reader *pdfgo.Reader seen map[pdfgo.Reference]bool elements map[pdfgo.Reference]bool objects []StructureNode } // prepareStructure 校验结构内容关联并登记引用区段,不提前写入标引 // 入参: ctx 取消上下文 // 返回: error 结构或内容容器错误 func (p *pdfImporter) prepareStructure(ctx context.Context) error { if err := ctx.Err(); err != nil { return err } if p.structureBindings.prepared[p.reader] { return nil } tree, err := p.reader.StructureTree() if err != nil { return err } if tree == nil { if p.structureBindings.prepared == nil { p.structureBindings.prepared = make(map[*pdfgo.Reader]bool) } p.structureBindings.prepared[p.reader] = true return ctx.Err() } parents, err := p.structureParents(ctx) if err != nil { return err } marked := make(map[pdfStructureContent]bool) if err := p.reader.WalkStructure(ctx, func(_ int, item pdfgo.StructureItem) error { if item.Type != "MCR" && item.Type != "OBJR" { return nil } content, err := p.reader.StructureContent(item) if err != nil { return err } mcid := int64(-1) if item.Type == "MCR" { mcid = item.MCID marked[pdfStructureContent{reader: p.reader, page: item.Page, stream: content.Stream, mcid: item.MCID}] = true } if parent, ok := parents[[2]int64{content.ParentKey, mcid}]; !ok || parent != item.Parent { return fmt.Errorf("PDF structure content parent mismatch") } return nil }); err != nil { return err } if err := ctx.Err(); err != nil { return err } b := p.structureBindings if b.parents == nil { b.parents = make(map[*pdfgo.Reader]map[[2]int64]pdfgo.Reference) } if b.prepared == nil { b.prepared = make(map[*pdfgo.Reader]bool) } if b.marked == nil { b.marked = make(map[pdfStructureContent]bool) } b.parents[p.reader], b.prepared[p.reader] = parents, true maps.Copy(b.marked, marked) b.readers = append(b.readers, p.reader) return nil } // prepareSourceStructure 使用来源阅读器校验引用文档,共享最终图元索引 // 入参: ctx 取消上下文, reader 来源阅读器 // 返回: error 结构或取消错误 func (p *pdfImporter) prepareSourceStructure(ctx context.Context, reader *pdfgo.Reader) error { if p.structureBindings.prepared[reader] { return ctx.Err() } source := *p source.reader = reader return source.prepareStructure(ctx) } // structureParents 复用预检父树索引,未预检时读取当前文档 // 入参: ctx 取消上下文 // 返回: map[[2]int64]pdfgo.Reference 父树键及区段编号对应的元素, error 解析或取消错误 func (p *pdfImporter) structureParents(ctx context.Context) (map[[2]int64]pdfgo.Reference, error) { if err := ctx.Err(); err != nil { return nil, err } if parents := p.structureBindings.parents[p.reader]; parents != nil { return parents, nil } parents := make(map[[2]int64]pdfgo.Reference) if err := p.reader.WalkStructureParents(ctx, func(parent pdfgo.StructureParent) error { parents[[2]int64{parent.Key, parent.MCID}] = parent.Element return nil }); err != nil { return nil, err } return parents, nil } // documentStructures 分别保存主文档与引用文档的结构,不合并来源对象编号 // 入参: ctx 取消上下文 // 返回: error 结构或提交错误 func (p *pdfImporter) documentStructures(ctx context.Context) error { for _, sequence := range p.structureBindings.order { if err := p.prepareSourceStructure(ctx, sequence.Reader); err != nil { return err } } for _, reader := range p.structureBindings.readers { source := *p source.reader = reader if err := source.documentStructure(ctx); err != nil { return err } } return ctx.Err() } // documentStructure 写出逻辑结构层级、文本属性及已提交的内容引用 // 入参: ctx 取消上下文 // 返回: error 解析、未支持的结构属性或提交错误 func (p *pdfImporter) documentStructure(ctx context.Context) error { if err := ctx.Err(); err != nil { return err } tree, err := p.reader.StructureTree() if err != nil || tree == nil { return err } refs, err := p.structureBindings.contentReferences(ctx) if err != nil { return err } objects, err := p.structureBindings.objectReferences(ctx) if err != nil { return err } parents, err := p.structureParents(ctx) if err != nil { return err } root := StructureNode{Name: "Structure", Namespace: pdfStructureNamespace} values := pdfStructureValues{ctx: ctx, reader: p.reader, seen: make(map[pdfgo.Reference]bool), elements: make(map[pdfgo.Reference]bool)} if tree.Reference != (pdfgo.Reference{}) { values.elements[tree.Reference] = true root.Attributes = pdfStructureSource(tree.Reference) } if err := p.reader.WalkStructure(ctx, func(_ int, item pdfgo.StructureItem) error { if item.Type == "StructElem" && item.Reference != (pdfgo.Reference{}) { values.elements[item.Reference] = true } return nil }); err != nil { return err } properties, err := p.structureBindings.contentProperties(&values) if err != nil { return err } for _, key := range []pdfgo.Name{"RoleMap", "ClassMap"} { if value := tree.Dictionary[key]; value != nil { node, err := values.encode(value, 0) if err != nil { return err } root.Children = append(root.Children, StructureNode{Name: string(key), Namespace: pdfStructureNamespace, Children: []StructureNode{node}}) } } extensions, err := values.extensions(tree.Dictionary, []pdfgo.Name{"Type", "K", "ParentTree", "ParentTreeNextKey", "RoleMap", "ClassMap", "IDTree"}) if err != nil { return err } root.Children = append(root.Children, extensions...) stack := []*StructureNode{&root} ids := make(map[string]pdfgo.Reference) err = p.reader.WalkStructure(ctx, func(depth int, item pdfgo.StructureItem) error { if depth >= len(stack) { return fmt.Errorf("invalid PDF structure depth") } stack = stack[:depth+1] parent := stack[depth] node := StructureNode{Name: string(item.Type), Namespace: pdfStructureNamespace} var content pdfgo.StructureContent if item.Type == "MCR" || item.Type == "OBJR" { content, err = p.reader.StructureContent(item) if err != nil { return err } mcid := int64(-1) if item.Type == "MCR" { mcid = item.MCID } if parent, ok := parents[[2]int64{content.ParentKey, mcid}]; !ok || parent != item.Parent { return fmt.Errorf("PDF structure content parent mismatch") } } switch item.Type { case "StructElem": extensions, err := values.extensions(item.Dictionary, []pdfgo.Name{"Type", "S", "P", "K", "Pg", "R", "ID", "T", "Lang", "Alt", "ActualText", "E", "A", "C"}) if err != nil { return err } node.Children = append(node.Children, extensions...) if item.Reference != (pdfgo.Reference{}) { node.Attributes = pdfStructureSource(item.Reference) } if _, err := p.reader.StructureAttributes(ctx, item); err != nil { return err } for _, key := range []pdfgo.Name{"A", "C"} { if value := item.Dictionary[key]; value != nil { child, err := values.encode(value, 0) if err != nil { return err } node.Children = append(node.Children, StructureNode{Name: string(key), Namespace: pdfStructureNamespace, Children: []StructureNode{child}}) } } node.Attributes = append(node.Attributes, pdfStructureAttribute("Tag", string(item.Tag))...) node.Attributes = append(node.Attributes, StructureAttribute{Name: "Revision", Value: strconv.FormatInt(item.Revision, 10)}) for _, field := range []pdfgo.Name{"ID", "T", "Lang", "Alt", "ActualText", "E"} { value, err := p.reader.Resolve(item.Dictionary[field]) if err != nil { return err } if value == nil { continue } text, ok := value.(pdfgo.String) if !ok { return fmt.Errorf("invalid PDF structure text: %s", field) } decoded := string(text) if field == "ID" { if _, exists := ids[decoded]; exists { return fmt.Errorf("duplicate PDF structure identifier") } ids[decoded] = item.Reference } else { decoded, err = pdfgo.DecodeTextString(text) if err != nil { return err } } node.Attributes = append(node.Attributes, pdfStructureAttribute(string(field), decoded)...) } case "MCR": extensions, err := values.extensions(item.Dictionary, []pdfgo.Name{"Type", "Pg", "Stm", "StmOwn", "MCID"}) if err != nil { return err } node.Children = append(node.Children, extensions...) targets, err := refs.targets(ctx, p.reader, item) if err != nil { return err } node.Attributes = []StructureAttribute{{Name: "MCID", Value: strconv.FormatInt(item.MCID, 10)}} key := pdfStructureContent{reader: p.reader, page: item.Page, stream: content.Stream, mcid: item.MCID} if property, ok := properties[key]; ok { node.Children = append(node.Children, property) } for _, target := range targets { node.Children = append(node.Children, StructureNode{Name: "ObjectRef", Namespace: pdfStructureNamespace, Attributes: []StructureAttribute{{Name: "PageRef", Value: target.PageID}}, Text: strings.Join(target.ObjectIDs, ",")}) } case "OBJR": extensions, err := values.extensions(item.Dictionary, []pdfgo.Name{"Type", "Pg", "Obj"}) if err != nil { return err } node.Children = append(node.Children, extensions...) targets, exists := objects[pdfStructureObject{reader: p.reader, page: item.Page, object: item.Target}] if !exists { kind, err := p.reader.Resolve(content.Dictionary["Type"]) if err != nil { return err } subtype, err := p.reader.Resolve(content.Dictionary["Subtype"]) if err != nil { return err } name, _ := subtype.(pdfgo.Name) if content.Stream != nil { if kind != nil && kind != pdfgo.Name("XObject") || name != "Form" && name != "Image" { return fmt.Errorf("unsupported PDF structure object target") } } else if kind != nil && kind != pdfgo.Name("Annot") || !(pdfgo.Annotation{Subtype: name}).IsStandard() { return fmt.Errorf("unsupported PDF structure object target") } } for _, target := range targets { if len(target.ObjectIDs) != 0 { node.Children = append(node.Children, StructureNode{Name: "ObjectRef", Namespace: pdfStructureNamespace, Attributes: []StructureAttribute{{Name: "PageRef", Value: target.PageID}}, Text: strings.Join(target.ObjectIDs, ",")}) } } default: return fmt.Errorf("unsupported PDF structure item: %s", item.Type) } parent.Children = append(parent.Children, node) stack = append(stack, &parent.Children[len(parent.Children)-1]) return ctx.Err() }) if err != nil { return err } if err := p.reader.WalkStructureIDs(ctx, func(id string, item pdfgo.StructureItem) error { ref, exists := ids[id] if !exists || ref != item.Reference { return fmt.Errorf("PDF structure identifier target is outside the hierarchy") } delete(ids, id) return nil }); err != nil { return err } if len(ids) != 0 { return fmt.Errorf("missing PDF structure identifier index") } if len(values.objects) != 0 { root.Children = append(root.Children, StructureNode{Name: "SourceObjects", Namespace: pdfStructureNamespace, Children: values.objects}) } data, err := encodeStructureNodeContext(ctx, root) if err != nil { return err } _, err = p.editor.addStructureXML(ctx, CustomTag{TypeID: pdfStructureTagType, NameSpace: pdfStructureNamespace}, data) return err } // pdfStructureSource 保存源结构标识,不与OFD图元标识混用 // 入参: ref 来源引用 // 返回: []StructureAttribute 对象编号及代数 func pdfStructureSource(ref pdfgo.Reference) []StructureAttribute { return []StructureAttribute{{Name: "SourceNumber", Value: strconv.FormatInt(ref.Number, 10)}, {Name: "SourceGeneration", Value: strconv.FormatInt(ref.Generation, 10)}} } // extensions 保留未投影字段,字段名不作为XML元素名称 // 入参: dictionary 来源字典, known 已投影字段 // 返回: []StructureNode 扩展字段, error 读取或取消错误 func (v *pdfStructureValues) extensions(dictionary pdfgo.Dictionary, known []pdfgo.Name) ([]StructureNode, error) { var result []StructureNode for _, key := range slices.Sorted(maps.Keys(dictionary)) { if err := v.ctx.Err(); err != nil { return nil, err } if slices.Contains(known, key) { continue } value, err := v.encode(dictionary[key], 0) if err != nil { return nil, err } result = append(result, StructureNode{Name: "Extension", Namespace: pdfStructureNamespace, Attributes: pdfStructureAttribute("Key", string(key)), Children: []StructureNode{value}}) } return result, v.ctx.Err() } // encode 按原始类型保存属性对象,间接对象仅展开一次,字符串保留原始字节 // 入参: object 属性对象, depth 当前值层级 // 返回: StructureNode 类型化属性值, error 读取、层级或取消错误 func (v *pdfStructureValues) encode(object pdfgo.Object, depth int) (StructureNode, error) { node := StructureNode{Namespace: pdfStructureNamespace} if err := v.ctx.Err(); err != nil { return node, err } if depth > 128 { return node, fmt.Errorf("PDF structure attribute limit exceeded") } switch value := object.(type) { case nil: node.Name = "Null" case pdfgo.Boolean: node.Name, node.Text = "Boolean", strconv.FormatBool(bool(value)) case pdfgo.Integer: node.Name, node.Text = "Integer", strconv.FormatInt(int64(value), 10) case pdfgo.Real: if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) { return node, fmt.Errorf("invalid PDF structure real") } node.Name, node.Text = "Real", strconv.FormatFloat(float64(value), 'g', -1, 64) case pdfgo.Name: node.Name = "Name" node.Attributes = pdfStructureAttribute("Value", string(value)) case pdfgo.String: node.Name, node.Text = "String", base64.StdEncoding.EncodeToString(value) node.Attributes = []StructureAttribute{{Name: "Encoding", Value: "base64"}} case pdfgo.Reference: node.Name = "Reference" node.Attributes = []StructureAttribute{{Name: "Number", Value: strconv.FormatInt(value.Number, 10)}, {Name: "Generation", Value: strconv.FormatInt(value.Generation, 10)}} if v.elements[value] { node.Name = "ElementReference" return node, v.ctx.Err() } if !v.seen[value] { v.seen[value] = true resolved, err := v.reader.Resolve(value) if err != nil { return node, err } child, err := v.encode(resolved, depth+1) if err != nil { return node, err } v.objects = append(v.objects, StructureNode{Name: "Indirect", Namespace: pdfStructureNamespace, Attributes: slices.Clone(node.Attributes), Children: []StructureNode{child}}) } case pdfgo.Array: node.Name = "Array" for _, object := range value { child, err := v.encode(object, depth+1) if err != nil { return node, err } node.Children = append(node.Children, child) } case pdfgo.Dictionary: node.Name = "Dictionary" for _, key := range slices.Sorted(maps.Keys(value)) { child, err := v.encode(value[key], depth+1) if err != nil { return node, err } node.Children = append(node.Children, StructureNode{Name: "Entry", Namespace: pdfStructureNamespace, Attributes: pdfStructureAttribute("Key", string(key)), Children: []StructureNode{child}}) } case *pdfgo.Stream: node.Name = "Stream" dictionary, err := v.encode(value.Dictionary, depth+1) if err != nil { return node, err } data, err := value.DecodeContext(v.ctx) if err != nil { return node, err } node.Children = []StructureNode{dictionary, {Name: "DecodedData", Namespace: pdfStructureNamespace, Attributes: []StructureAttribute{{Name: "Encoding", Value: "base64"}}, Text: base64.StdEncoding.EncodeToString(data)}} default: return node, fmt.Errorf("invalid PDF structure attribute value") } return node, v.ctx.Err() } // pdfStructureAttribute 保留XML不可表达的源字段,不替换原始字符 // 入参: name 属性名, value 属性值 // 返回: []StructureAttribute 属性及可选的编码声明 func pdfStructureAttribute(name, value string) []StructureAttribute { if structureXMLText(value) { return []StructureAttribute{{Name: name, Value: value}} } return []StructureAttribute{{Name: name, Value: base64.StdEncoding.EncodeToString([]byte(value))}, {Name: name + "Encoding", Value: "base64"}} } // targets 解析结构树中的内容流引用,不将未绘制内容绑定到其他同号区段 // 入参: ctx 取消上下文, reader 来源阅读器, item 标记内容引用 // 返回: []StructureReference 独立的目标引用, error 来源或取消错误 func (refs pdfStructureReferences) targets(ctx context.Context, reader *pdfgo.Reader, item pdfgo.StructureItem) ([]StructureReference, error) { if err := ctx.Err(); err != nil { return nil, err } if item.Type != "MCR" || item.MCID < 0 || item.Page == (pdfgo.Reference{}) { return nil, fmt.Errorf("invalid PDF marked content reference") } key := pdfStructureContent{reader: reader, page: item.Page, mcid: item.MCID} if item.Stream != (pdfgo.Reference{}) { value, err := reader.Resolve(item.Stream) if err != nil { return nil, err } var ok bool key.stream, ok = value.(*pdfgo.Stream) if !ok { return nil, fmt.Errorf("invalid PDF marked content stream") } } var result []StructureReference for _, ref := range refs[key] { if err := ctx.Err(); err != nil { return nil, err } copy := StructureReference{PageID: ref.PageID} for _, id := range ref.ObjectIDs { if err := ctx.Err(); err != nil { return nil, err } copy.ObjectIDs = append(copy.ObjectIDs, id) } result = append(result, copy) } return result, ctx.Err() } // objectReferences 汇总注解及XObject调用对应的已提交图元 // 入参: ctx 取消上下文 // 返回: map[pdfStructureObject][]StructureReference 对象引用索引, error 来源或取消错误 func (b *pdfStructureBindings) objectReferences(ctx context.Context) (map[pdfStructureObject][]StructureReference, error) { result := make(map[pdfStructureObject][]StructureReference) sets := make(map[pdfStructureObject]*pdfStructureReferenceSet) appendReferences := func(key pdfStructureObject, sources []StructureReference) error { set := sets[key] if set == nil { set = &pdfStructureReferenceSet{} sets[key] = set } if err := set.append(ctx, sources); err != nil { return err } result[key] = set.references return nil } for key, refs := range b.objects { if err := appendReferences(key, refs); err != nil { return nil, err } } for _, sequence := range b.order { if err := ctx.Err(); err != nil { return nil, err } if sequence.Object == (pdfgo.Reference{}) { continue } if sequence.Reader == nil || sequence.Page == nil { return nil, fmt.Errorf("missing PDF object source") } key := pdfStructureObject{reader: sequence.Reader, page: sequence.Page.Reference, object: sequence.Object} if err := appendReferences(key, b.sequences[sequence]); err != nil { return nil, err } } return result, ctx.Err() } // observe 保存已读取的标记区段,不登记尚未提交的图元 // 入参: sequence 来源区段 func (b *pdfStructureBindings) observe(sequence *pdfgo.ContentSequence) { if sequence != nil && sequence.Properties["MCID"] != nil { b.contents = append(b.contents, sequence) } } // pdfStructureContentKey 解析来源区段中的MCID,空值表示非结构标记 // 入参: sequence 来源区段 // 返回: pdfStructureContent 来源键, bool 是否具有MCID, error 属性或来源错误 func pdfStructureContentKey(sequence *pdfgo.ContentSequence) (pdfStructureContent, bool, error) { var key pdfStructureContent value := sequence.Properties["MCID"] if value == nil { return key, false, nil } if sequence.Reader == nil || sequence.Page == nil { return key, false, fmt.Errorf("missing PDF marked content source") } value, err := sequence.Reader.Resolve(value) if err != nil || value == nil { return key, false, err } mcid, ok := value.(pdfgo.Integer) if !ok || mcid < 0 { return key, false, fmt.Errorf("invalid PDF marked content ID") } return pdfStructureContent{reader: sequence.Reader, page: sequence.Page.Reference, stream: sequence.Stream, mcid: int64(mcid)}, true, nil } // contentProperties 保存区段标签及原始属性,重复调用须具有相同内容 // 入参: values 类型化属性编码器 // 返回: map[pdfStructureContent]StructureNode 属性索引, error 属性或取消错误 func (b *pdfStructureBindings) contentProperties(values *pdfStructureValues) (map[pdfStructureContent]StructureNode, error) { result := make(map[pdfStructureContent]StructureNode) seen := make(map[*pdfgo.ContentSequence]bool) for _, sequences := range [][]*pdfgo.ContentSequence{b.contents, b.order} { for _, sequence := range sequences { if err := values.ctx.Err(); err != nil { return nil, err } if seen[sequence] || sequence.Reader != values.reader { continue } seen[sequence] = true key, ok, err := pdfStructureContentKey(sequence) if err != nil { return nil, err } if !ok { continue } node := StructureNode{Name: "MarkedContent", Namespace: pdfStructureNamespace, Attributes: pdfStructureAttribute("Tag", string(sequence.Tag))} properties := sequence.Properties if _, ok := properties["MCID"].(pdfgo.Integer); ok { properties = maps.Clone(properties) delete(properties, "MCID") } if len(properties) != 0 { property, err := values.encode(properties, 0) if err != nil { return nil, err } node.Children = append(node.Children, property) } if previous, ok := result[key]; ok { equal, err := pdfStructurePropertiesEqual(values.ctx, previous, node) if err != nil { return nil, err } if !equal { return nil, fmt.Errorf("conflicting PDF marked content properties") } continue } result[key] = node } } return result, values.ctx.Err() } // pdfStructurePropertiesEqual 比较类型化属性树,不生成临时XML // 入参: ctx 取消上下文, left 原属性, right 待比较属性 // 返回: bool 属性是否一致, error 取消错误 func pdfStructurePropertiesEqual(ctx context.Context, left, right StructureNode) (bool, error) { if err := ctx.Err(); err != nil { return false, err } if left.Name != right.Name || left.Namespace != right.Namespace || left.Text != right.Text || !slices.Equal(left.Attributes, right.Attributes) || len(left.Children) != len(right.Children) { return false, nil } for i := range left.Children { equal, err := pdfStructurePropertiesEqual(ctx, left.Children[i], right.Children[i]) if err != nil || !equal { return equal, err } } return true, nil } // contentReferences 按来源解释MCID并汇总重复调用的目标图元 // 入参: ctx 取消上下文 // 返回: pdfStructureReferences 内容引用索引, error 属性或取消错误 func (b *pdfStructureBindings) contentReferences(ctx context.Context) (pdfStructureReferences, error) { if err := ctx.Err(); err != nil { return nil, err } result := make(pdfStructureReferences) sets := make(map[pdfStructureContent]*pdfStructureReferenceSet) for _, sequence := range b.order { if err := ctx.Err(); err != nil { return nil, err } key, ok, err := pdfStructureContentKey(sequence) if err != nil { return nil, err } if !ok { continue } set := sets[key] if set == nil { set = &pdfStructureReferenceSet{} sets[key] = set } if err := set.append(ctx, b.sequences[sequence]); err != nil { return nil, err } result[key] = set.references } return result, ctx.Err() } // append 归并引用并去重,不持有输入切片 // 入参: ctx 取消上下文, sources 来源引用 // 返回: error 取消错误 func (s *pdfStructureReferenceSet) append(ctx context.Context, sources []StructureReference) error { if err := ctx.Err(); err != nil { return err } for _, source := range sources { if err := ctx.Err(); err != nil { return err } index := -1 if last := len(s.references) - 1; last >= 0 && s.references[last].PageID == source.PageID { index = last } else if page, ok := s.pages[source.PageID]; ok { index = page } for _, id := range source.ObjectIDs { if err := ctx.Err(); err != nil { return err } key := [2]string{source.PageID, id} if s.seen[key] { continue } if s.seen == nil { s.seen = make(map[[2]string]bool) } s.seen[key] = true if index < 0 { index = len(s.references) if index == 1 { s.pages = map[string]int{s.references[0].PageID: 0} } if s.pages != nil { s.pages[source.PageID] = index } s.references = append(s.references, StructureReference{PageID: source.PageID}) } s.references[index].ObjectIDs = append(s.references[index].ObjectIDs, id) } } return ctx.Err() } // structureBindings 在提交前收集直接来源及含MCID的外层区段 // 入参: ctx 取消上下文 // 返回: []pdfStructureBinding 绑定位置, error 取消错误 func (buffer *pdfObjectBuffer) structureBindings(ctx context.Context) ([]pdfStructureBinding, error) { if err := ctx.Err(); err != nil { return nil, err } if buffer == nil { return nil, nil } var result []pdfStructureBinding for index, run := range buffer.sequences { if err := ctx.Err(); err != nil { return nil, err } end := buffer.len() if index+1 < len(buffer.sequences) { end = buffer.sequences[index+1].start } var sources pdfOriginSet collect := func(sequence *pdfgo.ContentSequence) error { for current := sequence; current != nil; current = current.Parent { if err := ctx.Err(); err != nil { return err } if current == sequence || current.Properties["MCID"] != nil || current.Object != (pdfgo.Reference{}) { sources.add(current) } } return nil } if err := collect(run.sequence); err != nil { return nil, err } for _, sequence := range run.related { if err := collect(sequence); err != nil { return nil, err } } if !sources.started { continue } result = append(result, pdfStructureBinding{sequence: sources.origin.sequence, start: run.start, end: end}) for _, sequence := range sources.origin.related { result = append(result, pdfStructureBinding{sequence: sequence, start: run.start, end: end}) } } return result, ctx.Err() } // append 在页面提交成功后登记已分配的图元标识 // 入参: pageID 页面标识, objects 已分配标识的对象, bindings 已准备的绑定位置 func (b *pdfStructureBindings) append(pageID string, objects []GraphicObject, bindings []pdfStructureBinding) { for _, binding := range bindings { last := b.reference(pageID, binding.sequence) for _, object := range objects[binding.start:binding.end] { last.ObjectIDs = append(last.ObjectIDs, editorObjectID(object)) } } } // reference 返回已提交来源区段在页面中的引用 // 入参: pageID 页面标识, sequence 来源区段 // 返回: *StructureReference 页面引用 func (b *pdfStructureBindings) reference(pageID string, sequence *pdfgo.ContentSequence) *StructureReference { if b.sequences == nil { b.sequences = make(map[*pdfgo.ContentSequence][]StructureReference) } refs := b.sequences[sequence] if len(refs) == 0 { b.order = append(b.order, sequence) } if len(refs) == 0 || refs[len(refs)-1].PageID != pageID { refs = append(refs, StructureReference{PageID: pageID}) } b.sequences[sequence] = refs return &refs[len(refs)-1] }