diff --git a/README.md b/README.md index 41020f3..118a8eb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Excalidraw Converter -**A command line tool for porting Excalidraw diagrams to Gliffy.** +**A command line tool for porting Excalidraw diagrams to Gliffy.** [Excalidraw](https://excalidraw.com/) is great for sketching diagrams as part of a design process, but chances are that you have to redo those sketches for documentation. This tool is made to bridge those tasks. @@ -9,8 +9,8 @@ Excalidraw Converter ports Excalidraw diagrams to a Gliffy compatible format, wh ## Getting started -### Installation -#### MacOS with [Homebrew](https://brew.sh/) +### Installation +#### MacOS with [Homebrew](https://brew.sh/) ```shell brew install sindrel/tap/excalidraw-converter ``` @@ -20,14 +20,14 @@ Download a compatible binary from the [Releases](https://github.com/sindrel/exca If you're a Linux or MacOS user, move it to your local bin folder to make it available in your environment (optional). -### How to convert diagrams +### How to convert diagrams First save your Excalidraw diagram to a file. -Then, to do a conversion, simply execute the binary by specifying the `gliffy` command, the path to your Excalidraw save file, and the path to where you want your converted file to be saved. +Then, to do a conversion, simply execute the binary by specifying the `gliffy` command, the path to your Excalidraw save file, and the path to where you want your converted file to be saved.
MacOS example - + ``` $ exconv gliffy -i ~/Downloads/my-diagram.excalidraw -o /tmp/my-ported-diagram.gliffy Parsing input file: ~/Downloads/my-diagram.excalidraw @@ -48,7 +48,7 @@ Then, to do a conversion, simply execute the binary by specifying the `gliffy` c
Linux example - + ``` $ ./exconv gliffy -i ~/Downloads/my-diagram.excalidraw -o /tmp/my-ported-diagram.gliffy Parsing input file: ~/Downloads/my-diagram.excalidraw @@ -69,7 +69,7 @@ Then, to do a conversion, simply execute the binary by specifying the `gliffy` c
Windows example - + ``` C:\> exconv.exe gliffy -i C:\Downloads\my-diagram.excalidraw -o C:\tmp\my-ported-diagram.gliffy Parsing input file: C:\Downloads\my-diagram.excalidraw @@ -92,7 +92,7 @@ Then, to do a conversion, simply execute the binary by specifying the `gliffy` c After converting your diagram(s), import them into Gliffy using the standard Import dialog. -## Commands +## Commands ```sh Available Commands: completion Generate the autocompletion script for the specified shell @@ -104,11 +104,11 @@ Flags: -h, --help help for exconv ``` -## Features +## Features All fixed shapes and most styling and text options are supported. -### Shapes +### Shapes * Rectangle * Rounded rectangle * Diamond @@ -116,18 +116,17 @@ All fixed shapes and most styling and text options are supported. * Arrow * Line -### Text - +### Text * Font family (Normal and Code) * Font size * Font color * Horizontal alignment +* Text contained in shapes -### Styling - -* Canvas background color +### Styling +* Canvas background color * Fill color -* Fill style (hachure and cross-hatch translate to gradients) +* Fill style (hachure and cross-hatch translate to gradients) * Stroke color * Stroke width * Opacity @@ -135,27 +134,26 @@ All fixed shapes and most styling and text options are supported. Free hand drawings and library graphics are currently not supported. ## Contributing - See something you'd like to improve? Feel free to add a pull request. If it's a major change, it's probably best to describe it in an [issue](https://github.com/sindrel/excalidraw-converter/issues/new) first. ## Development
Instructions -### Prerequisites: -* Go (see version in `go.mod`) +### Prerequisites: +* Go (see version in `go.mod`) -### Download dependencies +### Download dependencies ```shell go mod download ``` -### Run tests +### Run tests ```shell go test -v ./cmd ``` -### Compile and run +### Compile and run ```shell go run ./cmd/main.go ``` diff --git a/internal/conversion/gliffy.go b/internal/conversion/gliffy.go index ea27488..facf5d5 100644 --- a/internal/conversion/gliffy.go +++ b/internal/conversion/gliffy.go @@ -4,6 +4,7 @@ import ( internal "diagram-converter/internal" datastr "diagram-converter/internal/datastructures" "encoding/json" + "errors" "fmt" "os" "strconv" @@ -31,10 +32,87 @@ func ConvertExcalidrawToGliffy(importPath string, exportPath string) error { var output datastr.GliffyScene var objects []datastr.GliffyObject + objectIDs := map[string]int{} + objects, objectIDs, err = AddElements(false, input, objects, objectIDs) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to add element(s): %s\n", err) + os.Exit(1) + } + + objects, _, err = AddElements(true, input, objects, objectIDs) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to add element(s) with parent(s): %s\n", err) + os.Exit(1) + } + + priorityGraphics := []string{ + //"Line", + //"Text", + } + + objects = OrderGliffyObjectsByPriority(objects, priorityGraphics) + + var layer datastr.GliffyLayer + layer.Active = true + layer.GUID = "dR5PnMr9lIuu" + layer.Name = "Layer 0" + layer.NodeIndex = 11 + layer.Visible = true + + output.ContentType = "application/gliffy+json" + output.EmbeddedResources.Resources = []string{} + output.Version = "1.3" + output.Metadata.LastSerialized = timestamp + output.Metadata.Libraries = []string{ + "com.gliffy.libraries.basic.basic_v1.default", + "com.gliffy.libraries.flowchart.flowchart_v1.default", + } + output.Metadata.LoadPosition = "default" + output.Metadata.Revision = 0 + output.Metadata.Title = "Import" + output.Stage.Background = input.AppState.ViewBackgroundColor + output.Stage.DrawingGuidesOn = true + output.Stage.GridOn = true + output.Stage.Height = 1024 + output.Stage.Layers = append(output.Stage.Layers, layer) + output.Stage.MaxWidth = 5000 + output.Stage.MaxHeight = 5000 + output.Stage.Objects = objects + output.Stage.PrintModel.PageSize = "Letter" + output.Stage.PrintModel.Portrait = true + output.Stage.SnapToGrid = true + output.Stage.ViewportType = "default" + output.Stage.Width = 1024 + + outputJson, err := json.Marshal(output) + if err != nil { + fmt.Fprintf(os.Stderr, "Error occured during JSON marshaling: %s", err) + os.Exit(1) + } + + err = internal.WriteToFile(exportPath, string(outputJson)) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to write diagram to file: %s", err) + os.Exit(1) + } + + fmt.Printf("Converted diagram saved to file: %s\n", exportPath) + + return nil +} + +func AddElements(addChildren bool, input datastr.ExcalidrawScene, objects []datastr.GliffyObject, objectIDs map[string]int) ([]datastr.GliffyObject, map[string]int, error) { graphics := internal.MapGraphics() for i, element := range input.Elements { + if len(element.ContainerId) > 0 && !addChildren { + continue + } + if len(element.ContainerId) == 0 && addChildren { + continue + } + var object datastr.GliffyObject var shape datastr.GliffyShape var text datastr.GliffyText @@ -144,66 +222,42 @@ func ConvertExcalidrawToGliffy(importPath string, exportPath string) error { continue } - fmt.Printf(" Adding object: %s\n", object.UID) - object.ID = i - objects = append(objects, object) - } + objectIDs[element.ID] = object.ID - priorityGraphics := []string{ - //"Line", - //"Text", - } + fmt.Printf(" Adding object: %s (%s,%d,%d)\n", object.UID, element.ID, object.ID, object.Order) - objects = OrderGliffyObjectsByPriority(objects, priorityGraphics) + if len(element.ContainerId) > 0 { + var parent int = 999999 + for obj_k, obj := range objects { + if obj.ID == objectIDs[element.ContainerId] { + parent = obj_k + } + } - var layer datastr.GliffyLayer - layer.Active = true - layer.GUID = "dR5PnMr9lIuu" - layer.Name = "Layer 0" - layer.NodeIndex = 11 - layer.Visible = true + if parent == 999999 { + return nil, nil, errors.New("unable to find object parent") + } - output.ContentType = "application/gliffy+json" - output.EmbeddedResources.Resources = []string{} - output.Version = "1.3" - output.Metadata.LastSerialized = timestamp - output.Metadata.Libraries = []string{ - "com.gliffy.libraries.basic.basic_v1.default", - "com.gliffy.libraries.flowchart.flowchart_v1.default", - } - output.Metadata.LoadPosition = "default" - output.Metadata.Revision = 0 - output.Metadata.Title = "Import" - output.Stage.Background = input.AppState.ViewBackgroundColor - output.Stage.DrawingGuidesOn = true - output.Stage.GridOn = true - output.Stage.Height = 1024 - output.Stage.Layers = append(output.Stage.Layers, layer) - output.Stage.MaxWidth = 5000 - output.Stage.MaxHeight = 5000 - output.Stage.Objects = objects - output.Stage.PrintModel.PageSize = "Letter" - output.Stage.PrintModel.Portrait = true - output.Stage.SnapToGrid = true - output.Stage.ViewportType = "default" - output.Stage.Width = 1024 + object.X = 2 + object.Y = 0 + object.Rotation = 0 + object.UID = "" + object.Width = objects[parent].Width - 4 + object.Height = objects[parent].Height - 4 - outputJson, err := json.Marshal(output) - if err != nil { - fmt.Fprintf(os.Stderr, "Error occured during JSON marshaling: %s", err) - os.Exit(1) - } + fmt.Printf(" - Adding as child of %d\n", parent) - err = internal.WriteToFile(exportPath, string(outputJson)) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to write diagram to file: %s", err) - os.Exit(1) - } + children := append(objects[parent].Children, object) + objects[parent].Children = children - fmt.Printf("Converted diagram saved to file: %s\n", exportPath) + continue + } - return nil + objects = append(objects, object) + } + + return objects, objectIDs, nil } func StrokeStyleConvExcGliffy(style string) string { diff --git a/internal/datastructures/excalidraw.go b/internal/datastructures/excalidraw.go index 0313349..81fda2d 100644 --- a/internal/datastructures/excalidraw.go +++ b/internal/datastructures/excalidraw.go @@ -10,6 +10,7 @@ type ExcalidrawScene struct { BackgroundColor string `json:"backgroundColor"` Baseline float64 `json:"baseline"` BoundElementIds []string `json:"boundElementIds"` + ContainerId string `json:"containerId"` EndArrowhead string `json:"endArrowhead"` FillStyle string `json:"fillStyle"` FontFamily int64 `json:"fontFamily"` diff --git a/internal/datastructures/gliffy.go b/internal/datastructures/gliffy.go index 0901479..2cdbe3d 100644 --- a/internal/datastructures/gliffy.go +++ b/internal/datastructures/gliffy.go @@ -45,7 +45,7 @@ type GliffyScene struct { } type GliffyObject struct { - Children interface{} `json:"children"` + Children []GliffyObject `json:"children"` Graphic struct { Shape *GliffyShape `json:",omitempty"` Text *GliffyText `json:",omitempty"` diff --git a/test/data/test_input.excalidraw b/test/data/test_input.excalidraw index 9fbc1e9..6fe4960 100644 --- a/test/data/test_input.excalidraw +++ b/test/data/test_input.excalidraw @@ -252,8 +252,8 @@ }, { "type": "text", - "version": 154, - "versionNonce": 12542552, + "version": 155, + "versionNonce": 1654964799, "isDeleted": false, "id": "Ol1iTVjuYpopxSLV3mcR-", "fillStyle": "hachure", @@ -262,18 +262,18 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 486.3800061035156, + "x": 488.78, "y": 280.5, "strokeColor": "#000000", "backgroundColor": "transparent", - "width": 263.3999938964844, + "width": 261, "height": 90, "seed": 594323485, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1692972588073, + "updated": 1695189419996, "link": null, "locked": false, "fontSize": 24.800000000000004, @@ -455,6 +455,210 @@ "updated": 1676741853510, "link": null, "locked": false + }, + { + "id": "wgYXnpqQehSK1d1eWiyJI", + "type": "rectangle", + "x": 1294.9400030517577, + "y": 176.5, + "width": 289, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 630672497, + "version": 87, + "versionNonce": 1542376863, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "K5SaCX7g_D6gj2OLpGgJd" + } + ], + "updated": 1695189499223, + "link": null, + "locked": false + }, + { + "id": "K5SaCX7g_D6gj2OLpGgJd", + "type": "text", + "x": 1299.9400030517577, + "y": 190, + "width": 26.700000762939453, + "height": 23, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1137280497, + "version": 9, + "versionNonce": 1185780127, + "isDeleted": false, + "boundElements": null, + "updated": 1695189515811, + "link": null, + "locked": false, + "text": "left", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "middle", + "baseline": 17, + "containerId": "wgYXnpqQehSK1d1eWiyJI", + "originalText": "left", + "lineHeight": 1.15 + }, + { + "type": "rectangle", + "version": 134, + "versionNonce": 1528823455, + "isDeleted": false, + "id": "7569iQtcYeEq5L3xXDYXA", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1294.4400030517577, + "y": 240.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 289, + "height": 50, + "seed": 1521915473, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "35whneboDH07leChOhtLt" + } + ], + "updated": 1695189501481, + "link": null, + "locked": false + }, + { + "id": "35whneboDH07leChOhtLt", + "type": "text", + "x": 1411.123335494995, + "y": 254, + "width": 55.63333511352539, + "height": 23, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 526370033, + "version": 9, + "versionNonce": 2115247423, + "isDeleted": false, + "boundElements": null, + "updated": 1695189512808, + "link": null, + "locked": false, + "text": "center", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 17, + "containerId": "7569iQtcYeEq5L3xXDYXA", + "originalText": "center", + "lineHeight": 1.15 + }, + { + "type": "rectangle", + "version": 178, + "versionNonce": 705827807, + "isDeleted": false, + "id": "qDJlEhHOIHWhonnVZO8ZL", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1294.4400030517577, + "y": 306.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 289, + "height": 50, + "seed": 621913137, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "yrnKVP6EBW9UnwIii4mTU" + } + ], + "updated": 1695189503973, + "link": null, + "locked": false + }, + { + "id": "yrnKVP6EBW9UnwIii4mTU", + "type": "text", + "x": 1539.5066687011717, + "y": 320, + "width": 38.93333435058594, + "height": 23, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1259961265, + "version": 10, + "versionNonce": 1023342047, + "isDeleted": false, + "boundElements": null, + "updated": 1695189517803, + "link": null, + "locked": false, + "text": "right", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "right", + "verticalAlign": "middle", + "baseline": 17, + "containerId": "qDJlEhHOIHWhonnVZO8ZL", + "originalText": "right", + "lineHeight": 1.15 } ], "appState": { diff --git a/test/data/test_output.gliffy b/test/data/test_output.gliffy index c7d1e97..f576c31 100644 --- a/test/data/test_output.gliffy +++ b/test/data/test_output.gliffy @@ -8,7 +8,7 @@ "autosaveDisabled": false, "editorVersion": null, "exportBorder": false, - "lastSerialized": 1692972802251, + "lastSerialized": 1695189587139, "libraries": [ "com.gliffy.libraries.basic.basic_v1.default", "com.gliffy.libraries.flowchart.flowchart_v1.default" @@ -331,8 +331,8 @@ "order": 7, "rotation": 0, "uid": "com.gliffy.shape.basic.basic_v1.default.text", - "width": 316.0799926757812, - "x": 486.3800061035156, + "width": 313.2, + "x": 488.78, "y": 280.5 }, { @@ -526,6 +526,216 @@ "width": 151, "x": 1191.5, "y": 652 + }, + { + "children": [ + { + "children": null, + "graphic": { + "Text": { + "calculatedHeight": 0, + "calculatedWidth": 0, + "hposition": "none", + "html": "

left

", + "outerPaddingBottom": 0, + "outerPaddingLeft": 0, + "outerPaddingRight": 0, + "outerPaddingTop": 0, + "overflow": "none", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "tid": null, + "valign": "middle", + "vposition": "none" + }, + "type": "Text" + }, + "height": 46, + "hidden": false, + "id": 15, + "layerId": "dR5PnMr9lIuu", + "linkMap": null, + "lockAspectRatio": false, + "lockShape": false, + "order": 15, + "rotation": 0, + "uid": "", + "width": 285, + "x": 2, + "y": 0 + } + ], + "graphic": { + "Shape": { + "dashStyle": "", + "dropShadow": false, + "fillColor": "none", + "gradient": true, + "opacity": 1, + "shadowX": 0, + "shadowY": 0, + "state": 0, + "strokeColor": "#1e1e1e", + "strokeWidth": 1, + "tid": "com.gliffy.stencil.rectangle.basic_v1" + }, + "type": "Shape" + }, + "height": 50, + "hidden": false, + "id": 14, + "layerId": "dR5PnMr9lIuu", + "linkMap": null, + "lockAspectRatio": false, + "lockShape": false, + "order": 14, + "rotation": 0, + "uid": "com.gliffy.shape.basic.basic_v1.default.rectangle", + "width": 289, + "x": 1294.9400030517577, + "y": 176.5 + }, + { + "children": [ + { + "children": null, + "graphic": { + "Text": { + "calculatedHeight": 0, + "calculatedWidth": 0, + "hposition": "none", + "html": "

center

", + "outerPaddingBottom": 0, + "outerPaddingLeft": 0, + "outerPaddingRight": 0, + "outerPaddingTop": 0, + "overflow": "none", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "tid": null, + "valign": "middle", + "vposition": "none" + }, + "type": "Text" + }, + "height": 46, + "hidden": false, + "id": 17, + "layerId": "dR5PnMr9lIuu", + "linkMap": null, + "lockAspectRatio": false, + "lockShape": false, + "order": 17, + "rotation": 0, + "uid": "", + "width": 285, + "x": 2, + "y": 0 + } + ], + "graphic": { + "Shape": { + "dashStyle": "", + "dropShadow": false, + "fillColor": "none", + "gradient": true, + "opacity": 1, + "shadowX": 0, + "shadowY": 0, + "state": 0, + "strokeColor": "#1e1e1e", + "strokeWidth": 1, + "tid": "com.gliffy.stencil.rectangle.basic_v1" + }, + "type": "Shape" + }, + "height": 50, + "hidden": false, + "id": 16, + "layerId": "dR5PnMr9lIuu", + "linkMap": null, + "lockAspectRatio": false, + "lockShape": false, + "order": 16, + "rotation": 0, + "uid": "com.gliffy.shape.basic.basic_v1.default.rectangle", + "width": 289, + "x": 1294.4400030517577, + "y": 240.5 + }, + { + "children": [ + { + "children": null, + "graphic": { + "Text": { + "calculatedHeight": 0, + "calculatedWidth": 0, + "hposition": "none", + "html": "

right

", + "outerPaddingBottom": 0, + "outerPaddingLeft": 0, + "outerPaddingRight": 0, + "outerPaddingTop": 0, + "overflow": "none", + "paddingBottom": 0, + "paddingLeft": 0, + "paddingRight": 0, + "paddingTop": 0, + "tid": null, + "valign": "middle", + "vposition": "none" + }, + "type": "Text" + }, + "height": 46, + "hidden": false, + "id": 19, + "layerId": "dR5PnMr9lIuu", + "linkMap": null, + "lockAspectRatio": false, + "lockShape": false, + "order": 19, + "rotation": 0, + "uid": "", + "width": 285, + "x": 2, + "y": 0 + } + ], + "graphic": { + "Shape": { + "dashStyle": "", + "dropShadow": false, + "fillColor": "none", + "gradient": true, + "opacity": 1, + "shadowX": 0, + "shadowY": 0, + "state": 0, + "strokeColor": "#1e1e1e", + "strokeWidth": 1, + "tid": "com.gliffy.stencil.rectangle.basic_v1" + }, + "type": "Shape" + }, + "height": 50, + "hidden": false, + "id": 18, + "layerId": "dR5PnMr9lIuu", + "linkMap": null, + "lockAspectRatio": false, + "lockShape": false, + "order": 18, + "rotation": 0, + "uid": "com.gliffy.shape.basic.basic_v1.default.rectangle", + "width": 289, + "x": 1294.4400030517577, + "y": 306.5 } ], "printModel": {