Question

I am writing a little HTML5+JS tool to generate an SVG image. I have encountered a number of issues in doing so, and while I have workarounds for most of them, in at least one case I feel like there must be a better way. And then there are a couple of things that still just aren't working.

At present, this is for my own use, so cross-browser compatibility isn't a concern; as long as it works in Firefox (first preference) or Chromium, it's all good. I would like to stick it online once it's working right, though, so compatibility caveats would be appreciated.

Goals

  1. All processing should be done client-side; in fact, at this stage everything is a local file://, no web server involved.
  2. Add text and elements to an SVG image (inline in the HTML) using scripted form elements.
  3. Click on the SVG (which is shrunk down to a "preview" size) to open it, as modified, in a new window/tab.
  4. Use some easy-to-access method (i.e. not "DOM inspector, copy to text file, save") to save the SVG to disk.

Test case

https://gist.github.com/perey/1d352a790f749aa05a8b (see it in action)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>SVG Generator</title>
    <style type="text/css">
      figure {
        width: 45%;
        float: right;
      }
      #output-pic {
        border: thin solid green;
        cursor: pointer;
      }
      form {
        width: 45%;
        float: left;
      }
    </style>
    <script>
      window.onload = function() {
        document.getElementById("input-box").oninput = update_text;
        document.getElementById("output-pic").onclick = show_svg;
        update_text();
      }
      function update_text() {
        var input_elem = document.getElementById("input-box");
        var output_elem = document.getElementById("changeable-text");
        output_elem.textContent = input_elem.value;
      }
      function show_svg() {
        var svg_win = window.open("", "svg_win");
        var embedded_svg = document.getElementById("output-pic");
        var transplanted_svg = svg_win.document.importNode(embedded_svg, true);
        var blank_root = svg_win.document.documentElement;
        svg_win.document.removeChild(blank_root);
        svg_win.document.appendChild(transplanted_svg);
      }
    </script>
  </head>
  <body>
    <figure role="img" aria-labelledby="preview-caption">
      <figcaption id="preview-caption">Preview <small>(click for full
        size)</small></figcaption>
      <svg id="output-pic"
           xmlns="http://www.w3.org/2000/svg"
           xmlns:xlink="http://www.w3.org/1999/xlink"
           version="1.1" width="640px" height="480px"
           viewBox="0 0 640 480" preserveAspectRatio="xMinYMin">
        <title>A test SVG file</title>
        <defs>
          <style type="text/css">
            text {
              font-family: serif;
              stroke: none;
              fill: red;
            }
            .underline {
              stroke: blue;
              stroke-width: 1;
              fill: none;
              marker-mid: url(#arrow);
            }
          </style>
          <marker id="arrow"
                  viewBox="-3 -3 6 6" orient="auto"
                  markerUnits="strokeWidth"
                  refX="0" refY="0"
                  markerWidth="6" markerHeight="6">
            <path d="M0,0 -3,-3 3,0 -3,3 Z"/>
          </marker>
        </defs>
        <text id="changeable-text" text-anchor="middle" font-size="40"
              x="320" y="240"></text>
        <path class="underline" d="M10,250 h310 310"/>
      </svg>
    </figure>
    <form>
      <label>Text: <input id="input-box"/></label>
    </form>
  </body>
</html>

Issues

Opening the SVG

Opening about:blank, deleting its document element, and adding the SVG element, feels really hacky. However, nothing else has worked I have only found a slightly better way of constructing a document in a new window (see below).

In particular, I've tried loading a barebones SVG file and adding all the child nodes of the preview SVG, like so:

function show_svg() {
    var svg_win = window.open("blank.svg", "svg_win");
    var embedded_svg = document.getElementById("output-pic");
    var transplanted_svg = svg_win.document.importNode(embedded_svg, true);
    var blank_root = svg_win.document.documentElement;

    while (transplanted_svg.hasChildNodes()) {
        blank_root.appendChild(transplanted_svg.firstChild);
    }
    svg_win.alert("Done!");
}

However, after this function does its thing, the loaded file then "wipes" all changes made to it and reverts to its pristine state. (The alert is there to highlight this fact: in Firefox, the alert box itself disappears without user action when the page is "wiped". In Chromium, the alert box hangs about, but the wipe happens after it's dismissed.)

It's not a matter of tying the node reparenting to the new window's onload handler. Yes it is. I made a mistake when I first tried that. Here's what I did:

function show_svg() {
    var svg_win = window.open("blank.svg", "svg_win");
    var embedded_svg = document.getElementById("output-pic");
    var transplanted_svg = svg_win.document.importNode(embedded_svg, true);
    var blank_root = svg_win.document.documentElement;

    svg_win.onload = function () {
        while (transplanted_svg.hasChildNodes()) {
            blank_root.appendChild(transplanted_svg.firstChild);
        }
        svg_win.alert("Done!");
    }
}

What I should've done is put the definition of blank_root inside the onload handler. That works.

Still feels like there should be a way to construct a new document from scratch, though. "Modifying a blank SVG" is better than "modifying the about:blank HTML", but is that really the best way?

Missing markers

(This only seems to be an issue with Firefox, not with Chromium.)

The marker-mid styling works fine in the preview image, but not in the opened SVG. I have no idea why. Edit: Modifying an SVG file instead of about:blank doesn't have this issue. I'm off to file a bug, but I already suspect they're going to say "don't try and dynamically convert a HTML file into an SVG file".

Saving the generated SVG

I have no idea how to do this. A few tantalising hints seem to say that it's something to do with Blobs, but I've found nothing that addresses saving a generated SVG file client-side, and I don't understand what they're doing well enough to make it work for me.

Any help, suggestions, advice, or corrections?

Was it helpful?

Solution

I have solved my own problems, using modern HTML5 APIs.

The new show_svg() function looks like this:

function show_svg(evt) {
    var svg = document.getElementById("output-pic");
    var serializer = new XMLSerializer();
    var svg_blob = new Blob([serializer.serializeToString(svg)],
                            {'type': "image/svg+xml"});
    var url = URL.createObjectURL(svg_blob);

    var svg_win = window.open(url, "svg_win");
}

The browser's own Save functionality will work on this new window, and it doesn't involve any modifications to other files that "feel" weird or hacky. (It does seem a bit odd to serialise the SVG only to view it in the browser again, but this nonetheless seems to be The Right Thing under HTML5.)

The only unresolved problem is the disappearing markers—in fact, the problem gets worse, as now <use> elements don't work either! However, they're still there and functional in the code, so once the SVG is saved to a file, everything in the file works fine. And I've filed a bug with Mozilla, too.

OTHER TIPS

...and to display the SVG in the same window, use:

window.location = url;

instead of

 window.open(url,...

Note also that the Titel tag is important, as this is used for the default file name when the svg file is saved.

I'm not sure if I understand your problem correctly but since I'm working on a similar problem I suggest the following(hope it works for you). With the code sample below you shouldn't have problems with the use tag

function show_svg(evt)
{
  var svg = document.getElementById("output-pic");
  var serializer = new XMLSerializer();
  var ser = serializer.serializeToString(svg);
  var w = window.open();
  w.document.open();
  w.document.write(ser);
  w.document.close();
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top