// 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" "slices" "strconv" ) // LayerInfo 描述普通页面图层,Order为同类型零基顺序,ObjectCount包含页块中的图元。 type LayerInfo struct { ID string Type string DrawParam string Order int ObjectCount int } // Layers 按文件顺序返回普通页面图层,省略的类型规范化为Body,不包含模板图层。 // 入参: page 零基页面索引 // 返回: []LayerInfo 独立图层清单, error 页面读取错误 func (e *Editor) Layers(page int) ([]LayerInfo, error) { p, err := e.page(page) if err != nil { return nil, err } return editorLayerInfos(p), nil } // Layers 直接读取普通页面图层,不创建编辑器或分配新标识,返回值不持有页面可变数据。 // 入参: page 零基页面索引 // 返回: []LayerInfo 按文件顺序排列的图层, error 页面读取错误 func (r *Reader) Layers(page int) ([]LayerInfo, error) { p, err := r.PageContentByIndex(page) if err != nil { return nil, err } return editorLayerInfos(p), nil } // editorLayerInfos 将页面图层投影为不共享可变容器的管理清单。 // 入参: p 已读取的页面 // 返回: []LayerInfo 图层清单 func editorLayerInfos(p *PageContent) []LayerInfo { result := make([]LayerInfo, 0, len(p.Content.Layer)) orders := make(map[string]int) for _, layer := range p.Content.Layer { kind := editorLayerType(layer.Type) result = append(result, LayerInfo{layer.ID, kind, layer.DrawParam, orders[kind], len(layer.Objects)}) orders[kind]++ } return result } // AddLayer 追加空图层并返回新标识,空类型采用Body,空DrawParam表示不设置继承参数。 // 入参: page 页面索引, layerType 图层类型, drawParam 绘制参数标识 // 返回: string 新图层标识, error 类型、资源或结构错误 func (e *Editor) AddLayer(page int, layerType, drawParam string) (string, error) { layerType = editorLayerType(layerType) if !validEditorLayerType(layerType) { return "", fmt.Errorf("invalid layer type %q", layerType) } var id string err := e.editLayerPage(page, func(r *Reader, data []byte, root *editorXML) ([]byte, error) { if err := e.validateLayerDrawParam(page, drawParam); err != nil { return nil, err } if err := e.prepareSourceIDs(); err != nil { return nil, err } id = e.nextID() attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Type"}, Value: layerType}} attrs.add("DrawParam", drawParam) content := root.child("Content") fragment, err := root.containerXML("Layer", attrs, nil) if err != nil { return nil, err } if content != nil { return editorAppendStructure(data, content, fragment), nil } fragment, err = root.containerXML("Content", nil, fragment) if err != nil { return nil, err } at := root.close if actions := root.child("Actions"); actions != nil { at = actions.start } if root.open == root.end { return editorAppendStructure(data, root, fragment), nil } return editorPatchXML(data, []editorXMLPatch{{at, at, fragment}}), nil }) if err != nil { return "", err } return id, nil } // SetLayerType 设置图层类型,不改变图元坐标及绘制参数;同类型顺序由文件位置决定。 // 入参: page 页面索引, id 图层标识, layerType 图层类型 // 返回: error 标识、类型或结构错误 func (e *Editor) SetLayerType(page int, id, layerType string) error { layerType = editorLayerType(layerType) if !validEditorLayerType(layerType) { return fmt.Errorf("invalid layer type %q", layerType) } return e.editLayerPage(page, func(_ *Reader, data []byte, root *editorXML) ([]byte, error) { node, err := editorFindStructureID(root.child("Content"), "Layer", "ID", id) if err != nil { return nil, err } if editorLayerType(node.attr("Type")) == layerType { return data, nil } return editorStructureAttribute(data, node, "Type", layerType) }) } // SetLayerDrawParam 设置图层绘制参数,空标识清除引用,图元局部覆盖保持不变。 // 入参: page 页面索引, id 图层标识, drawParam 绘制参数标识 // 返回: error 标识、资源或结构错误 func (e *Editor) SetLayerDrawParam(page int, id, drawParam string) error { return e.editLayerPage(page, func(_ *Reader, data []byte, root *editorXML) ([]byte, error) { node, err := editorFindStructureID(root.child("Content"), "Layer", "ID", id) if err != nil { return nil, err } if err := e.validateLayerDrawParam(page, drawParam); err != nil { return nil, err } if node.attr("DrawParam") == drawParam { return data, nil } return editorStructureAttribute(data, node, "DrawParam", drawParam) }) } // MoveLayer 调整图层在同类型图层中的零基顺序,保留其他类型及扩展节点的位置。 // 入参: page 页面索引, id 图层标识, order 同类型目标顺序 // 返回: error 标识、顺序或结构错误 func (e *Editor) MoveLayer(page int, id string, order int) error { return e.editLayerPage(page, func(_ *Reader, data []byte, root *editorXML) ([]byte, error) { node, err := editorFindStructureID(root.child("Content"), "Layer", "ID", id) if err != nil { return nil, err } var nodes []*editorXML for _, child := range root.child("Content").children { if child.matchesOFD("Layer") && editorLayerType(child.attr("Type")) == editorLayerType(node.attr("Type")) { nodes = append(nodes, child) } } return editorMoveStructure(data, nodes, node, order) }) } // DeleteLayer 删除空图层,含图元、页块或扩展子元素时拒绝;可先在Transaction中迁移图元。 // 入参: page 页面索引, id 图层标识 // 返回: error 标识、非空图层或结构错误 func (e *Editor) DeleteLayer(page int, id string) error { return e.editLayerPage(page, func(_ *Reader, data []byte, root *editorXML) ([]byte, error) { content := root.child("Content") node, err := editorFindStructureID(content, "Layer", "ID", id) if err != nil { return nil, err } if len(node.children) != 0 { return nil, fmt.Errorf("layer %q is not empty", id) } if len(content.children) == 1 && len(content.attrs) == 0 { return editorPatchXML(data, []editorXMLPatch{{content.start, content.end, nil}}), nil } if len(editorStructureChildren(content, "Layer")) == 1 { return nil, fmt.Errorf("last layer contains a preserved Content container") } return editorPatchXML(data, []editorXMLPatch{{node.start, node.end, nil}}), nil }) } // MoveObjectsToLayer 将普通页面选区按原绘制顺序追加到目标层,保持标识、坐标、扩展及有效样式。 // 不接受模板或注解标识;页块成员须先通过已有复合编辑接口处理,失败不产生部分移动。 // 入参: page 页面索引, ids 不重复的普通图元标识, layerID 目标图层标识 // 返回: error 选区、继承参数或结构错误 func (e *Editor) MoveObjectsToLayer(page int, ids []string, layerID string) error { return e.editLayerPage(page, func(_ *Reader, data []byte, root *editorXML) ([]byte, error) { target, err := editorFindStructureID(root.child("Content"), "Layer", "ID", layerID) if err != nil { return nil, err } wanted := make(map[string]bool) for _, id := range ids { key := editorResourceID(id) if key == "" || wanted[key] { return nil, fmt.Errorf("invalid or duplicate object ID %q", id) } wanted[key] = true } var patches []editorXMLPatch var added []byte layers := editorStructureChildren(root.child("Content"), "Layer") slices.SortStableFunc(layers, func(a, b *editorXML) int { return layerOrder(a.attr("Type")) - layerOrder(b.attr("Type")) }) for _, layer := range layers { nodes := make(map[string]*editorXML) if err := editorXMLObjects(layer, nodes); err != nil { return nil, err } ordered := make([]*editorXML, 0, len(nodes)) for _, node := range nodes { ordered = append(ordered, node) } slices.SortFunc(ordered, func(a, b *editorXML) int { return a.start - b.start }) for _, node := range ordered { key := editorResourceID(node.attr("ID")) if !wanted[key] { continue } delete(wanted, key) if layer == target { continue } if node.parent != layer { return nil, fmt.Errorf("object %q belongs to a PageBlock", key) } fragment, err := editorXMLStandalone(data[node.start:node.end], node) if err != nil { return nil, err } if layer.attr("DrawParam") != target.attr("DrawParam") { member, err := newEditorCompositeNode(fragment) if err != nil { return nil, err } member.drawParams = []string{layer.attr("DrawParam")} member.defaults, err = e.editorDrawParam(layer.attr("DrawParam"), make(map[string]bool)) if err != nil { return nil, err } if member.object.Type == "TextObject" || member.object.Type == "ImageObject" { styled, err := e.compositeMemberStyle(member) if err != nil { return nil, err } if err := member.update(styled); err != nil { return nil, err } } if err := e.isolateCompositeStyle(member); err != nil { return nil, err } fragment, _, err = e.writeCompositeNode(member) if err != nil { return nil, err } } added = append(added, fragment...) patches = append(patches, editorXMLPatch{node.start, node.end, nil}) } } if len(wanted) != 0 { return nil, fmt.Errorf("selection contains missing ordinary page objects") } if len(added) == 0 { return data, nil } if target.open == target.end { patches = append(patches, editorXMLContent(data, target, added)) } else { patches = append(patches, editorXMLPatch{target.close, target.close, added}) } return editorPatchXML(data, patches), nil }) } // editorLayerType 将标准省略类型规范化,不改变已明确指定的类型。 // 入参: value 原类型 // 返回: string 有效类型 func editorLayerType(value string) string { if value == "" { return "Body" } return value } // validEditorLayerType 按正文表15校验可管理的图层类型。 // 入参: value 类型值 // 返回: bool 是否为支持的标准类型 func validEditorLayerType(value string) bool { return value == "Body" || value == "Background" || value == "Foreground" } // validateLayerDrawParam 在目标页面资源环境中核验完整绘制参数继承链。 // 入参: page 页面索引, id 绘制参数标识 // 返回: error 页面或资源错误 func (e *Editor) validateLayerDrawParam(page int, id string) error { if _, err := e.page(page); err != nil { return err } _, err := e.editorDrawParam(id, make(map[string]bool)) return err } // editLayerPage 在完整包快照上编辑页面原文,事务失败回滚,提交时隔离共享页面及历史版本。 // 入参: page 页面索引, edit 页面原文修改回调 // 返回: error 读取、修改或提交错误 func (e *Editor) editLayerPage(page int, edit func(*Reader, []byte, *editorXML) ([]byte, error)) error { return e.Transaction(func(_ *Editor) error { if _, err := e.page(page); err != nil { return err } r, err := e.Reader() if err != nil { return err } defer r.Close() doc, err := r.Doc() if err != nil { return err } name := r.ResPath(doc.Pages.Page[page].BaseLoc) data, err := r.readFile(name) if err != nil { return err } root, err := parseEditorXML(data) if err != nil { return err } next, err := edit(r, data, root) if err != nil || bytes.Equal(data, next) { return err } parts := map[string][]byte{name: next} shared := slices.ContainsFunc(doc.CommonData.TemplatePage, func(ref TemplatePage) bool { return r.ResPath(ref.BaseLoc) == name }) for i, ref := range doc.Pages.Page { shared = shared || i != page && r.ResPath(ref.BaseLoc) == name } if shared { private := packageAvailableName(r, parts, name) delete(parts, name) parts[private] = next document, err := r.readFile(r.documentRoot) if err != nil { return err } entry, err := parseEditorXML(document) if err != nil { return err } refNode := entry.child("Pages").childAt("Page", page) parts[r.documentRoot], err = editorStructureAttribute(document, refNode, "BaseLoc", "/"+private) if err != nil { return err } } return e.commitLayerTemplateParts(r, parts) }) } // editorStructureChildren 仅枚举当前标准命名空间中的直接子元素。 // 入参: root 容器节点, kind 子元素名称 // 返回: []*editorXML 文件顺序节点清单 func editorStructureChildren(root *editorXML, kind string) []*editorXML { var nodes []*editorXML if root != nil { for _, node := range root.children { if node.matchesOFD(kind) { nodes = append(nodes, node) } } } return nodes } // editorFindStructureID 按数值标识查找唯一直接子元素,拒绝缺失及歧义标识。 // 入参: root 容器节点, kind 元素名称, attribute 标识属性名, id 标识值 // 返回: *editorXML 唯一节点, error 标识错误 func editorFindStructureID(root *editorXML, kind, attribute, id string) (*editorXML, error) { var found *editorXML if editorResourceID(id) == "" { return nil, fmt.Errorf("invalid %s ID %q", kind, id) } for _, node := range editorStructureChildren(root, kind) { if sameResourceID(node.attr(attribute), id) { if found != nil { return nil, fmt.Errorf("ambiguous %s ID %q", kind, id) } found = node } } if found == nil { return nil, fmt.Errorf("%s %q not found", kind, id) } return found, nil } // editorStructureAttribute 局部替换或删除无命名空间属性,保留扩展及其余原文。 // 入参: data 原文, node 目标节点, name 属性名, value 新值,空值删除属性 // 返回: []byte 更新后的完整原文, error XML错误 func editorStructureAttribute(data []byte, node *editorXML, name, value string) ([]byte, error) { var desired *string if value != "" { desired = &value } patch, err := editorXMLAttributeChange(data, node, name, desired) if err != nil { return nil, err } return editorPatchXML(data, []editorXMLPatch{patch}), nil } // editorAppendStructure 向普通或自闭合容器追加已绑定命名空间的元素。 // 入参: data 原文, node 容器节点, fragment 待追加片段 // 返回: []byte 更新后的完整原文 func editorAppendStructure(data []byte, node *editorXML, fragment []byte) []byte { if node.open == node.end { return editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, fragment)}) } return editorPatchXML(data, []editorXMLPatch{{node.close, node.close, fragment}}) } // editorMoveStructure 在指定同类节点位置间移动完整原文,不触及夹杂的扩展元素。 // 入参: data 原文, nodes 同类节点, node 被移动节点, order 目标零基顺序 // 返回: []byte 更新后的完整原文, error 顺序错误 func editorMoveStructure(data []byte, nodes []*editorXML, node *editorXML, order int) ([]byte, error) { if order < 0 || order >= len(nodes) { return nil, fmt.Errorf("order %d out of range", order) } from := slices.Index(nodes, node) if from == order { return data, nil } ordered := slices.Clone(nodes) moveEditorItem(ordered, from, order) patches := make([]editorXMLPatch, len(nodes)) for i, old := range nodes { next := ordered[i] patches[i] = editorXMLPatch{old.start, old.end, data[next.start:next.end]} } return editorPatchXML(data, patches), nil } // commitLayerTemplateParts 持久化结构与已注册资源,隔离共享文件并恢复未变图元的会话编辑状态。 // 入参: reader 当前独立包快照, parts 文件修改集 // 返回: error 资源、隔离或提交错误 func (e *Editor) commitLayerTemplateParts(reader *Reader, parts map[string][]byte) error { ctx := context.Background() data := parts[reader.documentRoot] var err error if data == nil { data, err = reader.readFile(reader.documentRoot) if err != nil { return err } } root, err := parseEditorXML(data) if err != nil { return err } common := root.child("CommonData") if common == nil { return fmt.Errorf("document has no CommonData") } var definitions []byte registered := make(map[string]bool) for _, kind := range []string{"PublicRes", "DocumentRes"} { for _, node := range editorStructureChildren(common, kind) { registered[reader.ResPath(editorImportText(data, node))] = true } } var fonts, images []editorResource var spaces []ColorSpace for _, resource := range e.resources { if resource.name != "" { parts[resource.name] = resource.data } if resource.definition() != "" && !registered[resource.name] { definitions = append(definitions, common.textXML("DocumentRes", "/"+resource.name)...) registered[resource.name] = true } if resource.font != nil { fonts = append(fonts, resource) } if resource.image != nil { images = append(images, resource) } if resource.space != nil { spaces = append(spaces, *resource.space) } } if len(fonts)+len(images)+len(spaces) > 0 { encoded, err := e.encodeXML(func(x *ofdXML) { x.resources(fonts, images, spaces) }) if err != nil { return err } doc, err := reader.Doc() if err != nil { return err } name, added, err := mergePackageResources(ctx, reader, parts, doc, e.packageDirectory(), encoded) if err != nil { return err } if added { definitions = append(definitions, common.textXML("DocumentRes", "/"+name)...) } } var patches []editorXMLPatch if maximum := common.child("MaxUnitID"); maximum != nil { value, _ := strconv.Atoi(editorImportText(data, maximum)) if e.maxID > value { patches = append(patches, editorXMLPatch{maximum.start, maximum.end, maximum.textXML("MaxUnitID", strconv.Itoa(e.maxID))}) } } if len(definitions) > 0 { at := common.close for _, node := range common.children { if node.matchesOFD("TemplatePage") || node.matchesOFD("DefaultCS") { at = node.start break } } patches = append(patches, editorXMLPatch{at, at, definitions}) } parts[reader.documentRoot] = editorPatchXML(data, patches) scan := &editorFontDocument{reader: reader, index: reader.DocumentIndex()} if err := scan.apply(ctx, parts); err != nil { return err } previous := e.transactionSnapshot() if err := e.commitResourceSnapshot(ctx, reader); err != nil { return err } for i := range e.pages { oldIndex := slices.IndexFunc(previous.pages, func(p PageContent) bool { return p.ID == e.pages[i].ID }) if oldIndex < 0 || len(previous.pages[oldIndex].Content.Layer) == 0 { continue } states := make(map[string]GraphicObject) for _, layer := range previous.pages[oldIndex].Content.Layer { for _, object := range layer.Objects { states[editorObjectID(object)] = object } } page, err := e.page(i) if err != nil { return err } for j := range page.Content.Layer { for k := range page.Content.Layer[j].Objects { object := &page.Content.Layer[j].Objects[k] if old, ok := states[editorObjectID(*object)]; ok { object.state = old.state object.TextObject.layout = old.TextObject.layout object.CompositeGraphicUnit.states = old.CompositeGraphicUnit.states } } } } return nil }