package gliffy2drawio import ( "encoding/json" "fmt" "html" "math" "os" "regexp" "sort" "strconv" "strings" ) type GliffyDiagramConverter struct { diagramString string gliffyDiagram Diagram drawioDiagram *MxGraph vertices map[string]*GliffyObject layers map[string]*GliffyLayer rotationPattern *regexp.Regexp pageIDPattern *regexp.Regexp namePattern *regexp.Regexp report strings.Builder translator *StencilTranslator } func NewGliffyDiagramConverter(gliffyDiagramString string) (*GliffyDiagramConverter, error) { customTrans := os.Getenv("GLIFFY_TRANSLATIONS_JSON") c := &GliffyDiagramConverter{ diagramString: gliffyDiagramString, drawioDiagram: NewMxGraph(), vertices: make(map[string]*GliffyObject), layers: make(map[string]*GliffyLayer), rotationPattern: regexp.MustCompile(`rotation=(\-?\w+)`), pageIDPattern: regexp.MustCompile(`pageId=([^&]+)`), namePattern: regexp.MustCompile(`name=([^&]+)`), translator: NewStencilTranslator("").WithCustomMapping(customTrans), } if err := c.start(); err != nil { return nil, err } return c, nil } func (c *GliffyDiagramConverter) start() error { if err := json.Unmarshal([]byte(c.diagramString), &c.gliffyDiagram); err != nil { return err } // Support newer Gliffy files that wrap content in pages/scene. if len(c.gliffyDiagram.Stage.Objects) == 0 && len(c.gliffyDiagram.Pages) > 0 { selected := c.gliffyDiagram.Pages[0] if c.gliffyDiagram.DefaultPage != "" { for _, p := range c.gliffyDiagram.Pages { if p.ID == c.gliffyDiagram.DefaultPage { selected = p break } } } scene := selected.Scene c.gliffyDiagram.Stage = Stage{ Background: scene.Background, Width: scene.Width, Height: scene.Height, AutoFit: scene.AutoFit, GridOn: scene.GridOn, DrawingGuidesOn: scene.DrawingGuidesOn, Objects: scene.Objects, Layers: scene.Layers, TextStyles: scene.TextStyles, } } c.collectLayersAndConvert(c.layers, c.gliffyDiagram.Stage.Layers) c.collectVerticesAndConvert(c.vertices, c.gliffyDiagram.Stage.Objects, nil) sortObjectsByOrder(c.gliffyDiagram.Stage.Objects) c.importLayers() for _, obj := range c.gliffyDiagram.Stage.Objects { if err := c.importObject(obj, obj.Parent); err != nil { c.report.WriteString("-- Warning, Object " + obj.ID + " cannot be transformed.\n") } } // Shift everything to origin to avoid bottom-right offset in draw.io. c.drawioDiagram.NormalizeOrigin() return nil } func (c *GliffyDiagramConverter) importLayers() { if len(c.gliffyDiagram.Stage.Layers) == 0 { return } sortLayersByOrder(c.gliffyDiagram.Stage.Layers) for i, layer := range c.gliffyDiagram.Stage.Layers { // Avoid duplicating the default layer (id=1 already in root). if i == 0 && layer.MxObject != nil && layer.MxObject.ID == "1" && c.drawioDiagram.HasCell("1") { continue } c.drawioDiagram.AddCell(layer.MxObject, c.drawioDiagram.root) if i == 0 { c.drawioDiagram.SetDefaultParent(layer.MxObject) } } } func (c *GliffyDiagramConverter) importObject(obj *GliffyObject, gliffyParent *GliffyObject) error { var parent *MxCell if gliffyParent != nil { parent = gliffyParent.MxObject } if parent == nil && obj.LayerID != nil { if layer, ok := c.layers[*obj.LayerID]; ok && layer != nil { parent = layer.MxObject } } c.drawioDiagram.AddCell(obj.MxObject, parent) if obj.hasChildren() { if obj.isSwimlane() { if obj.Rotation != 0 { reverse(obj.Children) } } else { sortObjectsByOrder(obj.Children) } for _, child := range obj.Children { if err := c.importObject(child, obj); err != nil { return err } } } if obj.isLine() { startTerminal := c.getTerminalCell(obj, true) endTerminal := c.getTerminalCell(obj, false) if startTerminal != nil { obj.MxObject.Source = startTerminal.ID } if endTerminal != nil { obj.MxObject.Target = endTerminal.ID } c.setWaypoints(obj, startTerminal, endTerminal) } return nil } func (c *GliffyDiagramConverter) getTerminalCell(edge *GliffyObject, start bool) *MxCell { cons := edge.GetConstraints() if cons == nil { return nil } var con *Constraint if start { con = cons.StartConstraint } else { con = cons.EndConstraint } if con == nil { return nil } var data *ConstraintData if start { data = con.StartPositionConstraint } else { data = con.EndPositionConstraint } if data == nil { return nil } terminal := c.vertices[data.NodeID] if terminal == nil { return nil } return terminal.MxObject } func (c *GliffyDiagramConverter) collectLayersAndConvert(layerMap map[string]*GliffyLayer, layers []*GliffyLayer) { if len(layers) == 0 { return } // Ensure only a single default layer (id=1, parent=0) exists. if len(c.drawioDiagram.Model.Root.Cells) > 2 { trimmed := []*MxCell{} for _, cell := range c.drawioDiagram.Model.Root.Cells { if cell == nil { continue } if cell.ID == "0" || cell.ID == "1" { trimmed = append(trimmed, cell) } } if len(trimmed) < 2 { trimmed = append(trimmed, &MxCell{ID: "1", Parent: c.drawioDiagram.root.ID}) } c.drawioDiagram.Model.Root.Cells = trimmed } // Reuse existing default layer (id 1, parent 0) for the first Gliffy layer to match draw.io expectations. var defaultLayer *MxCell if len(c.drawioDiagram.Model.Root.Cells) >= 2 { if cell := c.drawioDiagram.Model.Root.Cells[1]; cell != nil && cell.Parent == c.drawioDiagram.root.ID { defaultLayer = cell } } if defaultLayer == nil { defaultLayer = &MxCell{ID: "1", Parent: c.drawioDiagram.root.ID} c.drawioDiagram.Model.Root.Cells = append(c.drawioDiagram.Model.Root.Cells, defaultLayer) } for i, layer := range layers { var cell *MxCell if i == 0 { cell = defaultLayer } else { cell = &MxCell{} c.drawioDiagram.AddCell(cell, c.drawioDiagram.root) } cell.Value = layer.Name if layer.Locked { cell.Style = joinStyle(cell.Style, "locked=1") } if !layer.Visible { cell.Style = joinStyle(cell.Style, "visible=0") } layer.MxObject = cell layerMap[layer.GUID] = layer if i == 0 { c.drawioDiagram.SetDefaultParent(cell) } } } func (c *GliffyDiagramConverter) collectVerticesAndConvert(vertices map[string]*GliffyObject, objects []*GliffyObject, parent *GliffyObject) { for _, obj := range objects { obj.Parent = parent obj.MxObject = c.convertGliffyObject(obj, parent) if !obj.isLine() { vertices[obj.ID] = obj } if obj.isGroup() || obj.isSelection() || (obj.isLine() && obj.hasChildren()) { c.collectVerticesAndConvert(vertices, obj.Children, obj) } } } func (c *GliffyDiagramConverter) GraphXML() (string, error) { grid := c.gliffyDiagram.Stage.GridOn guides := c.gliffyDiagram.Stage.DrawingGuidesOn w := c.gliffyDiagram.Stage.Width h := c.gliffyDiagram.Stage.Height if w == 0 { w = 1200 } if h == 0 { h = 900 } xml, err := c.drawioDiagram.ToXML("default-style2", c.gliffyDiagram.Stage.Background, grid, guides, w, h) if err != nil { return "", err } name := c.diagramName() return fmt.Sprintf(`%s`, html.EscapeString(name), xml), nil } func (c *GliffyDiagramConverter) diagramName() string { if c.gliffyDiagram.Title != "" { return c.gliffyDiagram.Title } if len(c.gliffyDiagram.Pages) > 0 { selected := c.gliffyDiagram.Pages[0] if c.gliffyDiagram.DefaultPage != "" { for _, p := range c.gliffyDiagram.Pages { if p.ID == c.gliffyDiagram.DefaultPage { selected = p break } } } if selected.Title != "" { return selected.Title } } if c.gliffyDiagram.Metadata.Title != "" { return c.gliffyDiagram.Metadata.Title } return "Page-1" } func (c *GliffyDiagramConverter) convertGliffyObject(obj *GliffyObject, parent *GliffyObject) *MxCell { cell := &MxCell{} if obj.ID != "" { cell.ID = obj.ID } if obj.IsUnrecognizedGraphicType() { return cell } var style strings.Builder geo := &MxGeometry{X: obj.X, Y: obj.Y, Width: obj.Width, Height: obj.Height} obj.adjustGeo(geo) cell.Geometry = geo var textObject *GliffyObject var link string graphic := obj.GraphicOrChildGraphic() translatedStyle := c.translator.Translate(obj.UID, obj.TID) if obj.isGroup() { if graphic == nil || translatedStyle == "" { style.WriteString("group;") } cell.Vertex = true } else { textObject = obj.TextObject() } if graphic != nil { link = obj.AdjustedLink() switch graphic.GetType() { case GraphicTypeShape, GraphicTypeMindmap: shape := graphic.Shape if shape == nil { shape = &GliffyShape{} } cell.Vertex = true isChevron := strings.Contains(obj.UID, "chevron") if translatedStyle != "" { style.WriteString("shape=" + translatedStyle + ";") } else { // Fallback if stencil translation is missing: render as a basic rect so the shape is visible. style.WriteString("shape=rect;") } if !strings.Contains(style.String(), "shadow=") { style.WriteString("shadow=" + intToString(boolToInt(shape.DropShadow)) + ";") } if !strings.Contains(style.String(), "strokeWidth") { style.WriteString("strokeWidth=" + intToString(shape.StrokeWidthValue()) + ";") if shape.StrokeWidthValue() == 0 && !isChevron { style.WriteString("strokeColor=none;") } } if !strings.Contains(style.String(), "fillColor") { if shape.NoFill() && !isChevron { style.WriteString("fillColor=none;") } else { style.WriteString("fillColor=" + shape.FillColor + ";") } if shape.FillColor == "none" { style.WriteString("pointerEvents=0;") } } if !strings.Contains(style.String(), "strokeColor") && !shape.NoFill() { stroke := shape.StrokeColor if obj.IsUseFillColorForStroke() { stroke = shape.FillColor } style.WriteString("strokeColor=" + stroke + ";") } if !strings.Contains(style.String(), "gradient") && shape.Gradient && !obj.GradientIgnored() { style.WriteString("gradientColor=" + obj.GradientColor() + ";gradientDirection=north;") } if !obj.isVennCircle() && !strings.Contains(style.String(), "opacity") { style.WriteString("opacity=" + floatToString(shape.Opacity*100) + ";") } style.WriteString(DashStyleMapping(shape.DashStyle, 1)) if obj.IsSubRoutine() && obj.Width != 0 { style.WriteString("size=" + floatToString(10/obj.Width) + ";") } if fragment := obj.UMLSequenceCombinedFragmentText(); fragment != "" && len(obj.Children) > 0 { cell.Value = fragment obj.Children = obj.Children[1:] } case GraphicTypeLine: if graphic.Line == nil { break } line := graphic.Line cell.Edge = true style.WriteString("shape=filledEdge;") style.WriteString("strokeWidth=" + intToString(line.StrokeWidthValue()) + ";") style.WriteString("strokeColor=" + line.StrokeColor + ";") style.WriteString("fillColor=" + line.FillColor + ";") style.WriteString(ArrowMapping(line.StartArrow).ToString(true)) style.WriteString(ArrowMapping(line.EndArrow).ToString(false)) if line.CornerRadius != nil { style.WriteString("rounded=1;") } else { style.WriteString("rounded=0;") } style.WriteString(DashStyleMapping(line.DashStyle, line.StrokeWidthValue())) style.WriteString(LineMapping(line.Interpolation)) cell.Geometry.X = 0 cell.Geometry.Y = 0 case GraphicTypeText: textObject = obj cell.Vertex = true style.WriteString("text;html=1;nl2Br=0;") cell.Value = obj.TextHTML() if obj.Parent != nil && !obj.Parent.isGroup() { parentGeo := obj.Parent.MxObject.Geometry if obj.Parent.isLine() { mxGeo := &MxGeometry{X: 0, Y: 0, Width: 0, Height: 0} lineT := 0.0 if graphic.Text != nil { lineT = graphic.Text.LineTValue*2 - 1 } mxGeo.X = lineT var lblY, lblX float64 if graphic.Text != nil && graphic.Text.LinePerpValue != nil { control := obj.Parent.Graphic.Line.ControlPath if len(control) >= 2 { i1 := 0 i2 := len(control) - 1 noCardinal := false switch graphic.Text.CardinalityType { case "begin": i2 = 1 case "end": i1 = len(control) - 2 default: noCardinal = true } if noCardinal || control[i1][1] == control[i2][1] { lblY = *graphic.Text.LinePerpValue if control[i1][0]-control[i2][0] > 0 { lblY = -lblY } } else { lblX = *graphic.Text.LinePerpValue if control[i1][1]-control[i2][1] < 0 { lblX = -lblX } } } } mxGeo.SourcePoint = &MxPoint{X: lblX, Y: lblY} setRelative(mxGeo, true) cell.Geometry = mxGeo style.WriteString("labelBackgroundColor=" + c.gliffyDiagram.Stage.Background + ";") if graphic.Text != nil { graphic.Text.SetHAlign("") } } else { cell.Geometry = &MxGeometry{X: 0, Y: 0, Width: parentGeo.Width, Height: parentGeo.Height} setRelative(cell.Geometry, true) } } case GraphicTypeImage: img := graphic.Image cell.Vertex = true style.WriteString("shape=" + c.translator.Translate(obj.UID, obj.TID) + ";") style.WriteString("image=" + img.CleanURL() + ";") case GraphicTypeSVG: svg := graphic.Svg cell.Vertex = true style.WriteString("shape=image;imageAspect=0;") if svg != nil && svg.EmbeddedResourceID != nil { if res, ok := c.gliffyDiagram.EmbeddedResources.Get(*svg.EmbeddedResourceID); ok { svgUtil := SVGImporterUtils{} data := svgUtil.SetViewBox(res.Data) style.WriteString("image=data:image/svg+xml," + EmbeddedResource{Data: data}.Base64EncodedData() + ";") } } default: // Fallback: render as image if possible or apply translated style if found. if graphic.Image != nil && graphic.Image.URL != "" { cell.Vertex = true style.WriteString("shape=image;imageAspect=0;image=" + graphic.Image.CleanURL() + ";") } else if graphic.Svg != nil && graphic.Svg.EmbeddedResourceID != nil { cell.Vertex = true style.WriteString("shape=image;imageAspect=0;") if res, ok := c.gliffyDiagram.EmbeddedResources.Get(*graphic.Svg.EmbeddedResourceID); ok { svgUtil := SVGImporterUtils{} data := svgUtil.SetViewBox(res.Data) style.WriteString("image=data:image/svg+xml," + EmbeddedResource{Data: data}.Base64EncodedData() + ";") } } else if translatedStyle != "" { cell.Vertex = true style.WriteString("shape=" + translatedStyle + ";") } } } else if obj.isSwimlane() && len(obj.Children) > 0 { cell.Vertex = true style.WriteString(c.translator.Translate(obj.UID, "") + ";") if obj.Rotation == 0 { style.WriteString("childLayout=stackLayout;resizeParent=1;resizeParentMax=0;") } header := obj.Children[0] shape := header.Graphic.Shape style.WriteString("strokeWidth=" + intToString(shape.StrokeWidthValue()) + ";") style.WriteString("shadow=" + intToString(boolToInt(shape.DropShadow)) + ";") style.WriteString("fillColor=" + shape.FillColor + ";") style.WriteString("strokeColor=" + shape.StrokeColor + ";") style.WriteString("startSize=" + floatToString(header.Height) + ";") style.WriteString("whiteSpace=wrap;") for i := 1; i < len(obj.Children); i++ { gLane := obj.Children[i] gLane.Parent = obj gs := gLane.Graphic.Shape var laneStyle strings.Builder laneStyle.WriteString("swimlane;collapsible=0;swimlaneLine=0;") laneStyle.WriteString("strokeWidth=" + intToString(gs.StrokeWidthValue()) + ";") laneStyle.WriteString("shadow=" + intToString(boolToInt(gs.DropShadow)) + ";") laneStyle.WriteString("fillColor=" + gs.FillColor + ";") laneStyle.WriteString("strokeColor=" + gs.StrokeColor + ";") laneStyle.WriteString("whiteSpace=wrap;html=1;fontStyle=0;") childGeometry := &MxGeometry{X: gLane.X, Y: gLane.Y, Width: gLane.Width, Height: gLane.Height} if obj.Rotation != 0 { if obj.Rotation == 270 { laneStyle.WriteString("horizontal=0;") w := childGeometry.Width childGeometry.Width = childGeometry.Height childGeometry.Height = w x := childGeometry.X childGeometry.X = childGeometry.Y childGeometry.Y = obj.Width - w - x } else { laneStyle.WriteString("rotation=" + floatToString(obj.Rotation) + ";") rotateGeometry(childGeometry, obj.Rotation, obj.Width/2, obj.Height/2) } } mxLane := &MxCell{Vertex: true} laneTxt := gLane.Children[0] mxLane.Value = laneTxt.TextHTML() if laneTxt.Graphic != nil && laneTxt.Graphic.Text != nil { laneStyle.WriteString(laneTxt.Graphic.Text.GetStyle(0, 0)) } laneStyle.WriteString("gliffyId=" + gLane.ID + ";") mxLane.Style = laneStyle.String() mxLane.Geometry = childGeometry gLane.MxObject = mxLane cell.Geometry = geo } } else if obj.isMindmap() && len(obj.Children) > 0 { rectangle := obj.Children[0] if rectangle.Graphic == nil || rectangle.Graphic.Mindmap == nil { obj.MxObject = cell return cell } mindmap := rectangle.Graphic.Mindmap style.WriteString("shape=" + c.translator.Translate(obj.UID, "") + ";") style.WriteString("shadow=" + intToString(boolToInt(mindmap.DropShadow)) + ";") style.WriteString("strokeWidth=" + intToString(mindmap.StrokeWidthValue()) + ";") style.WriteString("fillColor=" + mindmap.FillColor + ";") style.WriteString("strokeColor=" + mindmap.StrokeColor + ";") style.WriteString(DashStyleMapping(mindmap.DashStyle, 1)) if mindmap.Gradient { style.WriteString("gradientColor=#FFFFFF;gradientDirection=north;") } cell.Vertex = true } if !obj.isLine() { if strings.Contains(style.String(), "rotation") { if m := c.rotationPattern.FindStringSubmatch(style.String()); len(m) > 1 { initial, _ := strconv.ParseFloat(m[1], 64) rotation := initial + obj.Rotation styleStr := c.rotationPattern.ReplaceAllString(style.String(), "rotation="+floatToString(rotation)) style.Reset() style.WriteString(styleStr) } } else if obj.Rotation != 0 { if strings.Contains(style.String(), "swimlane;collapsible=0;") && obj.Rotation == 270 { w := geo.Width h := geo.Height geo.X = geo.X + (w-h)/2 geo.Y = geo.Y + (h-w)/2 geo.Width = h geo.Height = w style.WriteString("childLayout=stackLayout;resizeParent=1;resizeParentMax=0;horizontal=0;horizontalStack=0;") } else { if obj.isGroup() { for _, child := range obj.Children { c.rotateGroupedObject(obj, child) } } style.WriteString("rotation=" + floatToString(obj.Rotation) + ";") } } } if textObject != nil { style.WriteString("html=1;nl2Br=0;") if !obj.isLine() && textObject.Graphic != nil && textObject.Graphic.Text != nil { txt := textObject.Graphic.Text if obj.isSwimlane() { txt.SetForceTopPaddingShift(true) txt.SetVAlign("middle") } cell.Value = textObject.TextHTML() obj.adjustTextPos(textObject) if obj.ContainsTextBracket() { c.fixFrameTextBorder(obj, &style) style.WriteString(strings.ReplaceAll(txt.GetStyle(0, 0), "verticalAlign=middle", "verticalAlign=top")) } else { isChevron := strings.Contains(obj.UID, "chevron") if textObject == obj || isChevron { style.WriteString(txt.GetStyle(0, 0)) } else { style.WriteString(txt.GetStyle(textObject.X, textObject.Y)) } } } } popup := c.getGliffyPopup(obj) if link != "" || popup != nil { userObj := &UserObject{} if link != "" { if lb := c.extractLightboxDataFromGliffyUrl(link); lb != nil { link = "/plugins/drawio/lightbox.action?ceoId=" + intToString(int(lb.Key)) + "&diagramName=" + lb.Value + ".drawio" } userObj.Link = link if textObject != nil { userObj.Label = textObject.TextHTML() } } if popup != nil && popup.Graphic != nil && popup.Graphic.PopupNote != nil { userObj.Tooltip = popup.Graphic.PopupNote.Text } cell.UserObject = userObj } style.WriteString("gliffyId=" + obj.ID + ";") cell.Style = style.String() obj.MxObject = cell return cell } func (c *GliffyDiagramConverter) getGliffyPopup(obj *GliffyObject) *GliffyObject { if !obj.hasChildren() { return nil } for _, child := range obj.Children { if child.Graphic != nil && child.Graphic.GetType() == GraphicTypePopup { return child } } return nil } func (c *GliffyDiagramConverter) rotateGroupedObject(group, child *GliffyObject) { pivot := &MxPoint{X: group.Width/2 - child.Width/2, Y: group.Height/2 - child.Height/2} temp := &MxPoint{X: child.X, Y: child.Y} if group.Rotation != 0 { rads := group.Rotation * math.Pi / 180 cos := math.Cos(rads) sin := math.Sin(rads) temp = &MxPoint{ X: temp.X*cos - temp.Y*sin + pivot.X, Y: temp.X*sin + temp.Y*cos + pivot.Y, } child.X = temp.X child.Y = temp.Y child.Rotation += group.Rotation } } func (c *GliffyDiagramConverter) fixFrameTextBorder(obj *GliffyObject, style *strings.Builder) { if obj.TextObject() == nil { return } wrong := "labelX=32" correct := "labelX=" + floatToString(obj.TextObject().Width*1.1) styleStr := strings.Replace(style.String(), wrong, correct, 1) style.Reset() style.WriteString(styleStr) } func (c *GliffyDiagramConverter) addConstraint(obj *GliffyObject, terminal *MxCell, source bool, orthogonal bool) bool { cons := obj.GetConstraints() if cons == nil { return orthogonal } var con *Constraint if source { con = cons.StartConstraint } else { con = cons.EndConstraint } if con == nil { return orthogonal } var data *ConstraintData if source { data = con.StartPositionConstraint } else { data = con.EndPositionConstraint } if data != nil { direction := getStyleValue(terminal, "direction", "east") temp := &MxPoint{X: data.PX, Y: data.PY} rotation := 0 switch strings.ToLower(direction) { case "south": rotation = 270 case "west": rotation = 180 case "north": rotation = 90 } if rotation != 0 { rad := float64(rotation) * math.Pi / 180 temp = &MxPoint{ X: temp.X*math.Cos(rad) - temp.Y*math.Sin(rad), Y: temp.X*math.Sin(rad) + temp.Y*math.Cos(rad), } } if !orthogonal || (temp.X == 0.5 && temp.Y == 0.5) || obj.ForceConstraints() { cell := obj.MxObject if source { cell.Style += "exitX=" + floatToString(temp.X) + ";exitY=" + floatToString(temp.Y) + ";exitPerimeter=0;" } else { cell.Style += "entryX=" + floatToString(temp.X) + ";entryY=" + floatToString(temp.Y) + ";entryPerimeter=0;" } } return true } return orthogonal } func (c *GliffyDiagramConverter) setWaypoints(obj *GliffyObject, start, end *MxCell) { if obj.Graphic == nil || obj.Graphic.Line == nil { return } cell := obj.MxObject geo := cell.Geometry if geo == nil { geo = &MxGeometry{} cell.Geometry = geo } setRelative(geo, true) points := obj.Graphic.Line.ControlPath if len(points) < 2 { return } mxPoints := make([]*MxPoint, 0, len(points)) pivot := &MxPoint{X: obj.X + obj.Width/2, Y: obj.Y + obj.Height/2} for _, p := range points { wp := &MxPoint{X: p[0] + obj.X, Y: p[1] + obj.Y} if obj.Rotation != 0 { rads := obj.Rotation * math.Pi / 180 cos := math.Cos(rads) sin := math.Sin(rads) wp = &MxPoint{ X: pivot.X + (wp.X-pivot.X)*cos - (wp.Y-pivot.Y)*sin, Y: pivot.Y + (wp.X-pivot.X)*sin + (wp.Y-pivot.Y)*cos, } } mxPoints = append(mxPoints, wp) } orthogonal := true last := mxPoints[0] for _, p := range mxPoints[1:] { orthogonal = orthogonal && (last.X == p.X || last.Y == p.Y) last = p } p0 := mxPoints[0] pe := mxPoints[len(mxPoints)-1] if start == nil { geo.SourcePoint = &MxPoint{X: p0.X, Y: p0.Y, As: "sourcePoint"} mxPoints = mxPoints[1:] } else { if c.addConstraint(obj, start, true, orthogonal) { // keep point } } if end == nil { geo.TargetPoint = &MxPoint{X: pe.X, Y: pe.Y, As: "targetPoint"} mxPoints = mxPoints[:len(mxPoints)-1] } else { if c.addConstraint(obj, end, false, orthogonal) { // keep } } if orthogonal { cell.Style += "edgeStyle=orthogonalEdgeStyle;" result := make([]*MxPoint, 0, len(mxPoints)) var prev *MxPoint for _, p := range mxPoints { if prev == nil || prev.X != p.X || prev.Y != p.Y { result = append(result, p) prev = p } } if len(result) == 0 && ((start == nil && end != nil) || (start != nil && end == nil)) { center := &MxPoint{X: p0.X + (pe.X-p0.X)/2, Y: p0.Y + (pe.Y-p0.Y)/2} result = []*MxPoint{center, center} } mxPoints = result } if len(mxPoints) > 0 { geo.Points = &MxPointArray{Points: mxPoints} } cell.Geometry = geo } type lightboxInfo struct { Key int64 Value string } func (c *GliffyDiagramConverter) extractLightboxDataFromGliffyUrl(link string) *lightboxInfo { pagem := c.pageIDPattern.FindStringSubmatch(link) namem := c.namePattern.FindStringSubmatch(link) if len(pagem) > 1 { oldPageID, err := strconv.ParseInt(pagem[1], 10, 64) if err != nil { return nil } if len(namem) > 1 { return &lightboxInfo{Key: oldPageID, Value: namem[1]} } } return nil } func getStyleValue(cell *MxCell, key, defaultValue string) string { if cell == nil { return defaultValue } style := cell.Style if style == "" { return defaultValue } pairs := strings.Split(style, ";") for _, p := range pairs { if p == "" { continue } parts := strings.SplitN(p, "=", 2) if len(parts) == 2 && strings.EqualFold(parts[0], key) { return parts[1] } } return defaultValue } func sortObjectsByOrder(objs []*GliffyObject) { sort.SliceStable(objs, func(i, j int) bool { o1 := objs[i].Order.String() o2 := objs[j].Order.String() if o1 == "" && o2 == "" { return false } if o1 == "" { return false } if o2 == "" { return true } f1, err1 := strconv.ParseFloat(o1, 64) f2, err2 := strconv.ParseFloat(o2, 64) if err1 == nil && err2 == nil { return f1 < f2 } return o1 < o2 }) } func sortLayersByOrder(layers []*GliffyLayer) { sort.SliceStable(layers, func(i, j int) bool { return layers[i].Order < layers[j].Order }) } func reverse(objs []*GliffyObject) { for i, j := 0, len(objs)-1; i < j; i, j = i+1, j-1 { objs[i], objs[j] = objs[j], objs[i] } } func boolToInt(v bool) int { if v { return 1 } return 0 } // Convenience entry point for users wanting a single call. func ConvertGliffyJSONToDrawioXML(gliffyJSON string) (string, error) { converter, err := NewGliffyDiagramConverter(gliffyJSON) if err != nil { return "", err } return converter.GraphXML() }