Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating, viewing, and saving SVG client-side in browser

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?

like image 962
Tim Pederick Avatar asked May 10 '14 14:05

Tim Pederick


People also ask

How to display SVG file in HTML?

SVG images can be written directly into the HTML document using the <svg> </svg> tag. To do this, open the SVG image in VS code or your preferred IDE, copy the code, and paste it inside the <body> element in your HTML document. If you did everything correctly, your webpage should look exactly like the demo below.

How to insert an SVG in HTML?

The quick way: img elementTo embed an SVG via an <img> element, you just need to reference it in the src attribute as you'd expect. You will need a height or a width attribute (or both if your SVG has no inherent aspect ratio). If you have not already done so, please read Images in HTML.


1 Answers

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.

like image 116
Tim Pederick Avatar answered Sep 28 '22 17:09

Tim Pederick