DrawIo mxGraph: Using XmlToSvg loses shapes information
Asked Answered
I

2

7

I am trying to convert XML to SVG using Java, but it looks like the shapes information is getting lost in the process.

Given a simple draw.io graph:

drawio

After running XmlToSvg.java I get:

converted

I saved it as an uncompressed XML. I'm using the mxgraph-all.jar from the mxGraph Repo

Do you know if there are hidden settings to enable to preserve shapes and colors?

Intine answered 25/5, 2017 at 11:39 Comment(0)
T
3

Short version

It looks like despite the claims on the GitHub page, no implementation except for JavaScript one is really fully featured and production ready. Particularly Java implementation (as well .Net and PHP server-side ones) doesn't support "Cube" shape out of the box.

More details

Colors

You didn't provide your example XML but when I generate similar graph I get something like

<?xml version="1.0" encoding="UTF-8"?>
<mxGraphModel dx="1426" dy="816" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850"
              pageHeight="1100" background="#ffffff" math="0" shadow="0">
    <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>
        <mxCell id="2" value="" style="ellipse;whiteSpace=wrap;html=1;" parent="1" vertex="1">
            <mxGeometry x="445" y="60" width="230" height="150" as="geometry"/>
        </mxCell>
        <mxCell id="3" value="" style="ellipse;shape=doubleEllipse;whiteSpace=wrap;html=1;aspect=fixed;" parent="1" vertex="1">
            <mxGeometry x="500" y="320" width="120" height="120" as="geometry"/>
        </mxCell>
        <mxCell id="4" value="" style="endArrow=classic;html=1;" parent="1" source="3" target="2" edge="1">
            <mxGeometry width="50" height="50" relative="1" as="geometry">
                <mxPoint x="430" y="510" as="sourcePoint"/>
                <mxPoint x="480" y="460" as="targetPoint"/>
            </mxGeometry>
        </mxCell>
        <mxCell id="5" value="" style="shape=cube;whiteSpace=wrap;html=1;" parent="1" vertex="1">
            <mxGeometry x="80" y="320" width="170" height="110" as="geometry"/>
        </mxCell>
    </root>
</mxGraphModel>

Important thing here is that this XML does not contain any information about colors. Thus whole idea about "preserving colors" is wrong. In Java implementation you can configure "default colors" using an instance of mxStylesheet class and use it to init mxGraph object. For example to change colors to black and white you may do something like this:

mxStylesheet stylesheet = new mxStylesheet();
// configure "figures" aka "vertex"
{
    Map<String, Object> style = stylesheet.getDefaultVertexStyle();
    style.put(mxConstants.STYLE_FILLCOLOR, "#FFFFFF");
    style.put(mxConstants.STYLE_STROKECOLOR, "#000000");
    style.put(mxConstants.STYLE_FONTCOLOR, "#000000");
}
// configure "lines" aka "edges"
{
    Map<String, Object> style = stylesheet.getDefaultEdgeStyle();
    style.put(mxConstants.STYLE_STROKECOLOR, "#000000");
    style.put(mxConstants.STYLE_FONTCOLOR, "#000000");
}

mxGraph graph = new mxGraph(stylesheet);

You may look at mxStylesheet.createDefaultVertexStyle and mxStylesheet.createDefaultEdgeStyle for some details.

Shapes

The "ellipse" shape is not handled correctly because there is no code to parse "ellipse;whiteSpace=wrap;html=1;" and understand that the shape should be "ellipse" (comapre this to the "double ellipse" style "ellipse;shape=doubleEllipse;whiteSpace=wrap;html=1;aspect=fixed;" that contains explicit shape value). In JS implementation the first part of the style seems to select a handler function that will handle the rest of the string and do actual work. There seems to be no such feature in Java implmenetation at all. You can work this around by using "named styles" feature and define default shape for corresponding "handler" in the same mxStylesheet object like this:

// I just copied the whole list of mxConstants.SHAPE_ here
// you probably should filter it by removing non-primitive shapes
// such as mxConstants.SHAPE_DOUBLE_ELLIPSE
String[] shapes = new String[] {
        mxConstants.SHAPE_RECTANGLE,
        mxConstants.SHAPE_ELLIPSE,
        mxConstants.SHAPE_DOUBLE_RECTANGLE,
        mxConstants.SHAPE_DOUBLE_ELLIPSE,
        mxConstants.SHAPE_RHOMBUS,
        mxConstants.SHAPE_LINE,
        mxConstants.SHAPE_IMAGE,
        mxConstants.SHAPE_ARROW,
        mxConstants.SHAPE_CURVE,
        mxConstants.SHAPE_LABEL,
        mxConstants.SHAPE_CYLINDER,
        mxConstants.SHAPE_SWIMLANE,
        mxConstants.SHAPE_CONNECTOR,
        mxConstants.SHAPE_ACTOR,
        mxConstants.SHAPE_CLOUD,
        mxConstants.SHAPE_TRIANGLE,
        mxConstants.SHAPE_HEXAGON,
};
Map<String, Map<String, Object>> styles = stylesheet.getStyles();
for (String sh : shapes)
{
    Map<String, Object> style = new HashMap<>();
    style.put(mxConstants.STYLE_SHAPE, sh);
    styles.put(sh, style);
}

Still you may notice that the list of the mxConstants.SHAPE_ doesn't contain "cube". In JS implementation "cube" is a compound shape that is handled by a specialized handler in examples/grapheditor/www/js/Shape.js which is not a part of the core library! It means that if you want to support such advanced shapes in your Java code, you'll have to roll out the code to handle it yourself.

P.S. With all those changes (hacks) the image I get using Java code from the XML in the first snippet is:

Image for the simple graph

Transudate answered 29/5, 2017 at 0:7 Comment(13)
Thanks a lot for this investigation and tricks. Do you know a JavaScript way of displaying XML as SVG avoiding full client side loading (it tries to load remote resources during instantiation)?Intine
This answer is correct. mxGraph is the JavaScript client library, Java, .NET etc is not part of the core mxGraph. When you say "It looks like despite the claims on the GitHub page, no implementation except for JavaScript one is really fully featured and production ready." could you point to where we make these mis-leading claims and we'll correct them. Thanks.Gabriellegabrielli
@David, I think that sentences "It is the underlying technology that powers the drawing functionality that you see in draw.io." and "Also provided is server-side functionality in Java and .NET for persistence (open and save) functionality, as well as server-side image generation." on the GitHub page and the NPM page are quite misleading and this question is a result. I think for many readers second sentence implies that you can use .Net or Java implementation to generate image from the XML from draw.io.Transudate
With the first one, we'll have to agree to disagree :). draw.io is clearly is a super-set of mxGraph and the idea that you could create a diagram in draw.io and mxGraph not understand something I feel is reasonable. The second one, OK. What I mean to say is "server-side image generation for browsers that can't generate the image client-side". It's not stand-alone server-side image generation, the client has to send the graphics primitives to the server. I'll try to clarify that.Gabriellegabrielli
@David, I think that the issue is in both sentences together. Each one separately is OK. But together in the same "intro" section they are misleading. I think what you need to clarify is not the fact that primitives has to be sent by client, but the fact that core mxGraph library is a subset of draw.io and thus the server-side implementations support only a subset of all draw.io features.Transudate
To clarify to future readers, mxGraph JavaScript won't render the attached XML properly either. All the extra shapes are indeed defined inside draw.io. I yet to finish investigation into how hard is to load all the extra shapes from draw.io available xml files.Intine
@David, it looks like you removed from text any references to server-side implementations whatsoever. Of course it is up to you, but I don't think this is perfect solution for this issue.Transudate
hello @SergGr, any idea how to assign a stencil (from xml file) to a shape? I load them from available xml files but can't figure out how to assign it to a shape id (e.g. read definition of "generic database" from aws3.xml and associate it with mxgraph.aws3.generic_database)Intine
@MykolaGolubyev, I'm not sure what you mean by "stencil" here. If you mean something similar to the way "cube" shape is implemented in the JS example, then I think the only way is to roll out a piece of custom code that would parse XML, analyze style and do custom drawing. My hack based on the "named styles" feature (not sure what is official name for it) allows you to only assign a simple shape that is already supported by the core library.Transudate
jgraph.github.io/mxgraph/docs/manual_javavis.html#3.1.1.1 github.com/jgraph/draw.io/blob/master/war/stencils/aws3.xmlIntine
@Transudate thank you again for the investigation. I assigned the bounty but leaving the question un-answered to potentially get a viable working approach.Intine
@MykolaGolubyev, I'm sorry but after looking into the code I think that what you want can be done but will take to much of effort even for a hefty 500 bounty (I'd estimate it as at least 1-2 weeks project).Transudate
@Transudate I think I figured out how to draw stencils using 2D but not SVG. I appreciate you looking into it. Will keep digging. If you noticed some hints into how to render stencil as SVG - please let me know.Intine
T
1

There is an XML-file, containing parameters of the most generic shapes. You should load it into stylesheet to make images look exactly as they were drawn in editor. Default stylesheet is default.xml.

So first of all make your code to get 2 things: stylesheet and diagram content.

String diagramText = getAsString(diagramPath);
String stylesheetText = getAsString(stylesheetPath);

Next, the simplest way to create SVG image is to utilize classes from mxgraph-core.jar. It looks like this

mxStylesheet stylesheet = new mxStylesheet(); // mxgraph-core.jar
InputSource is = new InputSource(new StringReader(stylesheetText));
Document document = documentBuilder.parse(is);
mxCodec codec = new mxCodec(document);
codec.decode(document.getDocumentElement(), stylesheet);
mxIGraphModel model = new mxGraphModel();
mxGraph graph = new mxGraph(model, context.stylesheet);
is = new InputSource(new StringReader(diagramText));
document = documentBuilder.parse(new InputSource(is));
codec = new mxCodec(document);
codec.decode(document.getDocumentElement(), model);
final Document svgDocument = documentBuilder.newDocument();
mxCellRenderer.drawCells(
        graph,
        null,
        1d,
        null,
        new mxCellRenderer.CanvasFactory() {
            @Override
            public mxICanvas createCanvas(int width, int height) {
                Element root = output.createElement("svg");
                String w = Integer.toString(width);
                String h = Integer.toString(height);
                root.setAttribute("width", w);
                root.setAttribute("height", h);
                root.setAttribute("viewBox", "0 0 " + w + " " + h);
                root.setAttribute("version", "1.1");
                root.setAttribute("xmlns", "http://www.w3.org/2000/svg");
                root.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
                output.appendChild(root);
                mxSvgCanvas canvas = new mxSvgCanvas(svgDocument);
                canvas.setEmbedded(true);
                return canvas;
            }
        });
return svgDocument; // this is the result

However, as SergGr pointed, Java implementation of mxgraph library doesn't contain some useful shapes. Their drawing rules are described by JavaScript functions in Shape.js.

I tried to execute that JavaScript in ScriptEngine shipped in Java standard library. Unfortunately this idea didn't work, because the JavaScript code somewhere deep inside interacts with browser.

But if we run the code in a browser, it works well. I did it successfully with HtmlUnit.

Write a JavaScript function to call from Java:

function convertToSVG(diagramText, stylesheetText) {
    var stylesheet = new mxStylesheet();
    var doc = mxUtils.parseXml(stylesheetText);
    var stylesheetRoot = doc.documentElement;
    var stylesheetCodec = new mxCodec(doc);
    var dom = document.implementation;
    stylesheetCodec.decode(stylesheetRoot, stylesheet);
    doc = dom.createDocument(null, "div", null);
    var model = new mxGraphModel();
    var graph = new mxGraph(doc.documentElement, model, "exact", stylesheet);
    doc = new DOMParser().parseFromString(diagram, "text/xml");
    var codec = new mxCodec(doc);
    codec.decode(doc.documentElement, model);
    doc = dom.createDocument("http://www.w3.org/2000/svg", "svg", null);
    var svgRoot = doc.documentElement;
    var bounds = graph.getGraphBounds();
    svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    svgRoot.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
    svgRoot.setAttribute("width", bounds.width);
    svgRoot.setAttribute("height", bounds.height);
    svgRoot.setAttribute("viewBox", "0 0 " + bounds.width + " " + bounds.height);
    svgRoot.setAttribute("version", "1.1");
    var svgCanvas = new mxSvgCanvas2D(svgRoot);
    svgCanvas.translate(-bounds.x, -bounds.y);
    var exporter = new mxImageExport();
    var state = graph.getView().getState(model.root);
    exporter.drawState(state, svgCanvas);
    var result = new XMLSerializer().serializeToString(doc);
    return result;
}

Load this text into String and run the following code

String jsFunction = getAsString("convertToSVG.js");
Path file = Files.createTempFile("44179673-", ".html"); // do not forget to delete it
String hmltText = "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
        + "<head><title>Empty file</title></head><body/></html>";
Files.write(file, Arrays.asList(htmlText));
WebClient webClient = new WebClient(); // net.sourceforge.htmlunit:htmlunit
HtmlPage page = webClient.getPage(file.toUri().toString());
String initScript = ""
  + "var mxLoadResources = false;"
  + "var mxLoadStylesheets = false;"
  + "var urlParams = new Object();";
page.executeJavaScript(initScript);
page.executeJavaScript(getAsString("mxClient.min.js"));
page.executeJavaScript(getAsString("Graph.js")); // Shape.js depends on it
page.executeJavaScript(getAsString("Shapes.js"));
ScriptResult scriptResult = page.executeJavaScript(jsFunction);
Object convertFunc = scriptResult.getJavaScriptResult();
Object args[] = new Object[]{ diagramText, stylesheetText };
scriptResult = page.executeJavaScriptFunction(convertFunc, null, args, null);
String svg = scriptResult.getJavaScriptResult().toString();

The code above seems to work well for me.

Talky answered 6/1, 2019 at 16:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.