// 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" "image/color" "reflect" "strconv" ) // AddCompositeObject 按页面坐标向内部范围末尾添加对象,复用已注册资源 // 入参: page 页面索引, path 父路径, object 基本对象或复合快照 // 返回: int 新成员序号, error 错误信息 func (e *Editor) AddCompositeObject(page int, path ObjectPath, object GraphicObject) (int, error) { indexes, err := e.CopyObjectsToComposite(page, path, []GraphicObject{object}, 0, 0) if err != nil { return 0, err } return indexes[0], nil } // CopyObjectsToComposite 将页面坐标快照复制到内部范围,隔离目标继承样式并分配新标识 // 保真快照保留原文,全部对象成功后提交一次撤销记录 // 入参: page 页面索引, path 父路径, objects 当前文档快照或基本对象, dx 页面横向位移, dy 页面纵向位移 // 返回: []int 新成员序号, error 错误信息 func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []GraphicObject, dx, dy float64) ([]int, error) { if !finite(dx) || !finite(dy) { return nil, fmt.Errorf("copy requires finite offsets") } var result []int err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, members []*editorCompositeNode) error { if len(objects) == 0 { return nil } owner := root.loadedScope(path.Children) box, _ := ParseBox(owner.object.CompositeGraphicUnit.Boundary) parent := owner.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(owner.object.CompositeGraphicUnit.CTM)) inverse, ok := parent.Invert() if !ok { return fmt.Errorf("composite insertion requires an invertible transform") } next := len(members) if container := owner.node.child("Content"); container != nil { next = 0 for _, member := range members { if member.owner != owner || member.span.start < container.close { next++ } } } var content []byte validation := &editorValidation{Editor: e} for _, source := range objects { id := e.nextID() object, err := validation.prepareCopiedObject(id, source) if err != nil { return err } maximum := e.maxID object, origin, err := e.copyObjectOrigin(source, object, &maximum) if err != nil { return err } e.maxID = maximum var data []byte if origin != nil { data, err = editorXMLObject(origin.data, origin.node, origin.object, object) if err == nil { data, err = editorXMLStandalone(data, origin.node) } } else { data, err = editorObjectXML(object) } if err != nil { return err } node, err := newEditorCompositeNode(data) if err != nil { return err } if node.object.Type == "PathObject" { node.defaults = renderer.drawParamDefaults(e.copiedLayerStyle(source), nil) style, styleErr := e.resolveEditorStyle(node.object, e.copiedLayerStyle(source)) _, fillErr := e.Color(style.PathObject.FillColor) _, strokeErr := e.Color((*FillColor)(style.PathObject.StrokeColor)) if styleErr != nil && editReason(styleErr) == EditUnsupportedColor || fillErr != nil || strokeErr != nil { node, err = e.wrapCompositePath(renderer, node) if err != nil { return err } } } node.parent, node.boundaryInCTM = IdentityMatrix, true node.setStates(object.CompositeGraphicUnit.states) node.drawParams = []string{e.copiedLayerStyle(source)} if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" { if err := node.convertCoordinates(IdentityMatrix, true); err != nil { return err } } if err := e.isolateCompositeStyle(node); err != nil { return err } if err := e.transformCompositeMember(renderer, node, inverse.Multiply(TranslationMatrix(dx, dy))); err != nil { return err } if !owner.boundaryInCTM { if err := node.convertCoordinates(parent, false); err != nil { return err } } state := object.state if object.TextObject.layout != nil && !reflect.DeepEqual(state.layout, object.TextObject.layout) { y, _ := strconv.ParseFloat(object.TextObject.TextCode[0].Y, 64) state.layout, state.origin = object.TextObject.layout, [2]float64{0, y} } node.setState(state) for id, state := range node.states { owner.states[id] = state } content = append(content, node.data...) result = append(result, next+len(result)) } if container := owner.node.child("Content"); container != nil { if container.open == container.end { owner.patches = append(owner.patches, editorXMLContent(owner.data, container, content)) } else { owner.patches = append(owner.patches, editorXMLPatch{container.close, container.close, content}) } } else if owner.node.name.Local == "Appearance" { if owner.node.open == owner.node.end { owner.patches = append(owner.patches, editorXMLContent(owner.data, owner.node, content)) } else { owner.patches = append(owner.patches, editorXMLPatch{owner.node.close, owner.node.close, content}) } } else { content, err := editorXMLContainer("Content", nil, content) if err != nil { return err } content = bytes.TrimPrefix(content, []byte(xml.Header)) if owner.node.open == owner.node.end { owner.patches = append(owner.patches, editorXMLContent(owner.data, owner.node, content)) } else { owner.patches = append(owner.patches, editorXMLPatch{owner.node.close, owner.node.close, content}) } } return nil }) if err != nil { return nil, err } return result, nil } // wrapCompositePath 通过标准矢量实例变换复杂路径,保持渐变坐标与原始颜色节点 // 入参: renderer 资源度量器, node 复杂路径 // 返回: *editorCompositeNode 包装节点, error 错误信息 func (e *Editor) wrapCompositePath(renderer *Renderer, node *editorCompositeNode) (*editorCompositeNode, error) { return e.vectorInstance(renderer, []*editorCompositeNode{node}) } // isolateCompositeStyle 固定对象的绘制参数,避免目标容器改变颜色、虚线和端点样式 // 入参: node 页面坐标中的对象节点 // 返回: error 错误信息 func (e *Editor) isolateCompositeStyle(node *editorCompositeNode) error { if err := e.isolateCompositeDefaultPaint(node); err != nil { return err } if node.object.Type == "CompositeObject" || node.object.Type == "CompositeGraphicUnit" || node.object.Type == "PathObject" { id, err := e.neutralDrawParam() if err != nil { return err } for _, source := range node.drawParams { id, err = e.copyDrawParam(source, id, make(map[string]bool)) if err != nil { return err } } parameter := node.object.CompositeGraphicUnit.DrawParam if node.object.Type == "PathObject" { parameter = node.object.PathObject.DrawParam } id, err = e.copyDrawParam(parameter, id, make(map[string]bool)) if err != nil { return err } object := node.object if object.Type == "PathObject" { object.PathObject.DrawParam = id } else { object.CompositeGraphicUnit.DrawParam = id } return node.update(object) } object, err := e.resolveEditorStyleDefaults(node.object, &DrawParam{}) if err != nil { return err } if object.Type == "TextObject" { id, err := e.neutralDrawParam() if err != nil { return err } object.TextObject.DrawParam = id } return node.update(object) } // isolateCompositeDefaultPaint 固定基准参数会覆盖的默认透明色,按继承环境复用资源副本 // 入参: node 待复制的独立节点 // 返回: error 参数、资源或XML错误 func (e *Editor) isolateCompositeDefaultPaint(node *editorCompositeNode) error { type resourceKey struct { id string fill, stroke bool } cache := make(map[resourceKey]string) visiting := make(map[string]bool) var transparent *FillColor var isolate func(*editorCompositeNode, DrawParam) error isolate = func(current *editorCompositeNode, base DrawParam) error { parameter, err := e.editorDrawParam(current.node.attr("DrawParam"), make(map[string]bool)) if err != nil { return err } base = *mergeDrawParam(base, parameter) if base.FillColor != nil && base.StrokeColor != nil { return nil } object := current.object path := object.Type == "PathObject" && object.PathObject.FillColor == nil && base.FillColor == nil text := object.Type == "TextObject" && object.TextObject.StrokeColor == nil && base.StrokeColor == nil if path || text { if transparent == nil { transparent, err = e.RGBColor(color.NRGBA{}) if err != nil { return err } } if path { object.PathObject.FillColor = transparent } else { object.TextObject.StrokeColor = (*StrokeColor)(transparent) } return current.update(object) } if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" { return nil } id := object.CompositeGraphicUnit.ResourceID replacement := id if id != "" { key := resourceKey{editorResourceID(id), base.FillColor != nil, base.StrokeColor != nil} if key.id == "" { key.id = id } if visiting[key.id] { return fmt.Errorf("cyclic composite resource %q", id) } var ok bool replacement, ok = cache[key] if !ok { resource, err := e.compositeDefinition(id) if err != nil { return err } visiting[key.id] = true err = isolate(resource, base) delete(visiting, key.id) if err != nil { return err } replacement = id if resource.changed { for _, entry := range e.resources { if sameResourceID(entry.composite, id) { resource.setStates(entry.states) break } } replacement, err = e.addCompositeResource(resource.data, resource.states) if err != nil { return err } node.remaps = append(node.remaps, e.resources[len(e.resources)-1].objectIDs) } cache[key] = replacement } } var patches []editorXMLPatch var visit func(*editorXML) error visit = func(parent *editorXML) error { for _, child := range parent.children { if classifyOFDNamespace(child.name.Space) == ofdXMLUnknown { continue } switch child.name.Local { case "Content", "PageBlock": if err := visit(child); err != nil { return err } case "TextObject", "PathObject", "CompositeObject", "CompositeGraphicUnit": if child.name.Local == "TextObject" && (base.StrokeColor != nil || child.child("StrokeColor") != nil) || child.name.Local == "PathObject" && (base.FillColor != nil || child.child("FillColor") != nil) { continue } data, err := editorXMLStandalone(current.data[child.start:child.end], child) if err != nil { return err } member, err := newEditorCompositeNode(data) if err != nil { return err } if err := isolate(member, base); err != nil { return err } if member.changed { patches = append(patches, editorXMLPatch{child.start, child.end, member.data}) } } } return nil } if err := visit(current.node); err != nil { return err } if len(patches) != 0 { next, err := newEditorCompositeNode(editorPatchXML(current.data, patches)) if err != nil { return err } current.data, current.node, current.object, current.changed = next.data, next.node, next.object, true } if replacement != id { object = current.object object.CompositeGraphicUnit.ResourceID = replacement return current.update(object) } return nil } var base DrawParam for _, id := range node.drawParams { parameter, err := e.editorDrawParam(id, make(map[string]bool)) if err != nil { return err } base = *mergeDrawParam(base, parameter) } return isolate(node, base) } // neutralDrawParam 注册隔离继承用的基准参数,默认透明色由对象显式保留 // 返回: string 绘制参数标识, error 错误信息 func (e *Editor) neutralDrawParam() (string, error) { fill, err := e.RGBColor(color.NRGBA{A: 255}) if err != nil { return "", err } zero := 0.0 return e.addEditorDrawParam(DrawParam{LineWidth: defaultPathLineWidth, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, DashOffset: &zero, dashPatternSet: true, FillColor: fill, StrokeColor: (*StrokeColor)(fill)}) } // addEditorDrawParam 注册已校验的绘制参数,相同定义复用资源 // 入参: draw 绘制参数 // 返回: string 资源标识, error 错误信息 func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) { draw.ID, draw.ResourceID, draw.BaseLoc, draw.Link = "", "", "", "" for _, resource := range e.resources { if resource.draw != nil { previous := *resource.draw previous.ID = "" if reflect.DeepEqual(previous, draw) { return resource.draw.ID, nil } } } if err := e.prepareSourceIDs(); err != nil { return "", err } draw.ID = e.nextID() data, err := e.encodeXML(func(x *ofdXML) { x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}}) x.start("DrawParams", nil) attrs := ofdAttrs{} for _, pair := range [][2]string{{"ID", draw.ID}, {"Relative", draw.Relative}, {"Cap", draw.Cap}, {"Join", draw.Join}, {"Font", draw.Font}} { attrs.add(pair[0], pair[1]) } attrs.number("LineWidth", draw.LineWidth) if draw.LineWidthSet && draw.LineWidth == 0 { attrs.add("LineWidth", "0") } attrs.number("MiterLimit", draw.MiterLimit) if draw.MiterLimitSet && draw.MiterLimit == 0 { attrs.add("MiterLimit", "0") } if draw.Relative == "" || draw.dashPatternSet || draw.DashPattern != "" { attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "DashPattern"}, Value: draw.DashPattern}) } if draw.DashOffset != nil { attrs.add("DashOffset", ofdNumber(*draw.DashOffset)) } attrs.number("Size", draw.Size) attrs.number("Weight", float64(draw.Weight)) if draw.Italic { attrs.add("Italic", "true") } x.start("DrawParam", attrs) x.color("FillColor", draw.FillColor) x.color("StrokeColor", (*FillColor)(draw.StrokeColor)) x.end("DrawParam") x.end("DrawParams") x.end("Res") }) if err != nil { return "", err } refs, err := editorResourceReferences(data) if err != nil { return "", err } resource := editorResource{name: e.packageName("Res/DrawParams/DrawParam_" + draw.ID + ".xml"), data: data, draw: &draw, references: refs} e.resources = append(e.resources, resource) return draw.ID, nil }