Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

fabricjs: text alignment issue while using export and import as svg

Text added on the canvas once exported as SVG and imported to canvas, is not aligning the same way as it's original

http://jsbin.com/ruluko/edit?html,js,output

using fabric js v1.6.0-rc.1

like image 542
CodingNinja Avatar asked Oct 08 '15 21:10

CodingNinja


2 Answers

This looks like a bug in fabric's svg parser.
You can see it if, instead of grouping the svg elements, you add them one by one :

var canvas1 = new fabric.Canvas('c1');
var circle = new fabric.Circle({
  radius: 50,
  fill: '#eef'
});
canvas1.add(circle);
var text = new fabric.Text('fabric', { fill: 'red', left: 100, top: 100 });
canvas1.add(text);
var topText = new fabric.Text('Top', { fill: 'red', left: 100, top: 0 });
canvas1.add(topText);
var botText = new fabric.Text('Bot LL', { fill: 'red', left: 80, top: 163 });
canvas1.add(botText);

var canvas2 = new fabric.Canvas('c2');

var doAction = function() {
  var mysvg = canvas1.toSVG();
  canvas2.clear();
  fabric.loadSVGFromString(mysvg, function(objects, options) {
    for (var i = 0; i < objects.length; i++) {
      canvas2.add(objects[i]);
    }
  });
};
.canvas-container{border:1px solid; display: inline-block; position: relative;}
<script src="//code.jquery.com/jquery.min.js"></script>
<script src="//cdn.bootcss.com/fabric.js/1.6.0-rc.1/fabric.min.js"></script>

  <canvas id="c1" width="200" height="200"></canvas>
  <canvas id="c2" width="200" height="200"></canvas>
  <button onclick='doAction()'>Export & Import as SVG</button>

As you can see in the snippet, each element's box is at the wrong coordinates. Note that it also occurs with the loadSVGFromURL() method.


One quick workaround if you were going to group those elements anyway, is to draw your svg as an image :

var canvas1 = new fabric.Canvas('c1');

var circle = new fabric.Circle({
  radius: 50,
  fill: '#eef'
});
canvas1.add(circle);
var text = new fabric.Text('fabric', { fill: 'red', left: 100, top: 100 });
canvas1.add(text);
var topText = new fabric.Text('Top', { fill: 'red', left: 100, top: 0 });
canvas1.add(topText);
var botText = new fabric.Text('Bot LL', { fill: 'red', left: 80, top: 163 });
canvas1.add(botText);

var canvas2 = new fabric.Canvas('c2');


var doAction = function() {
  var mysvg = canvas1.toSVG();
  canvas2.clear();
  var svgURL = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(mysvg.split('svg11.dtd">')[1]);
  fabric.Image.fromURL(svgURL, function(oImg) {
    canvas2.add(oImg)
  });
};
.canvas-container{border:1px solid; display: inline-block; position: relative;}
<script src="//code.jquery.com/jquery.min.js"></script>
<script src="//cdn.bootcss.com/fabric.js/1.6.0-rc.1/fabric.min.js"></script>

<canvas id="c1" width="200" height="200"></canvas>
<canvas id="c2" width="200" height="200"></canvas>
<button onclick='doAction()'>Export & Import as SVG</button>

The main caveat of this workaround is that you will lose ability to modify each path of your group once loaded.


Since the problem occurs because of <tspan> elements, a better fix would be to first clean your svg string, by replacing all those <tspan> to <text> elements :

var canvas1 = new fabric.Canvas('c1');

var circle = new fabric.Circle({
  radius: 50,
  fill: '#eef'
});
canvas1.add(circle);
var text = 'this is\na multiline\ntext';
var alignedRightText = new fabric.Text(text, {
  textAlign: 'right'
});
canvas1.add(alignedRightText)
var topText = new fabric.Text('Top', { fill: 'red', left: 100, top: 0 });
canvas1.add(topText);
var botText = new fabric.Text('Bot LL', { fill: 'red', left: 80, top: 163 });
canvas1.add(botText);

var canvas2 = new fabric.Canvas('c2');


var doAction = function() {
  var mysvg = canvas1.toSVG();
  canvas2.clear();
  fabric.loadSVGFromString(fixSVGText(mysvg), function(objects, options) {
    options.selectable = false;
    var obj = fabric.util.groupSVGElements(objects, options);
    canvas2.add(obj).renderAll();
  });
};

function fixSVGText(str) {
  // parse our string as a DOM object and get the SVGElement
  var svg = new DOMParser().parseFromString(str, "image/svg+xml").documentElement;

  // get all <tspan> elements
  var tspans = svg.querySelectorAll('tspan');
  for (var i = 0; i < tspans.length; i++) {
    var ts = tspans[i],
      parent = ts.parentNode,
      gParent = parent.parentNode;
    var j = 0;

    // create a new SVGTextElement to replace our tspan
    var replace = document.createElementNS('http://www.w3.org/2000/svg', 'text');

    var tsAttr = ts.attributes;
    // set the 'x', 'y' and 'fill' attributes to our new element
    for (j = 0; j < tsAttr.length; j++) {
      replace.setAttributeNS(null, tsAttr[j].name, tsAttr[j].value);
    }

    // append the contentText
    var childNodes = ts.childNodes;
    for (j = 0; j < childNodes.length; j++) {
      replace.appendChild(ts.childNodes[j]);
    }

    var tAttr = parent.attributes;
    // set the original text attributes to our new one
    for (j = 0; j < tAttr.length; j++) {
      replace.setAttributeNS(null, tAttr[j].name, tAttr[j].value);
    }
    // append our new text to the grand-parent
    gParent.appendChild(replace);

    // if this is the last tspan
    if (ts === parent.lastElementChild)
    // remove the old, now empty, SVGTextElement
      gParent.removeChild(parent)
  }
  // return a string version of our cleaned svg
  return new XMLSerializer().serializeToString(svg);
}
.canvas-container{border:1px solid; display: inline-block; position: relative;}
<script src="//code.jquery.com/jquery.min.js"></script>
<script src="//cdn.bootcss.com/fabric.js/1.6.0-rc.1/fabric.min.js"></script>

<canvas id="c1" width="200" height="200"></canvas>
<canvas id="c2" width="200" height="200"></canvas>
<button onclick='doAction()'>Export & Import as SVG</button>

But the best solution is still to open a new issue to kangax, and hope that this bug will be fixed in future versions.

like image 58
Kaiido Avatar answered Sep 18 '22 05:09

Kaiido


I really appreciate finding your solution Moussa. Thank you.

I noticed for multiline text boxes, that solution is only grabbing the last line. Below is a solution I worked out starting from your code. The alignment is still funky for that, but at least this looks like a step in the right direction:

function convertTspans(svg) {
    var tspans = svg.match(/<\s*tspan[^>]*>(.*?)<\s*\/\s*tspan>/g);

    var x = null;
    var y = null;
    var groupString = null;
    var matrixString = null;
    var xMatrix = null;
    var yMatrix = null;
    var transformedSvg = svg;

    if (tspans === null) {
        return svg;
    }

    for (var i = 0; i < tspans.length; i++) {
        var tspanString = tspans[i];
        var text = tspanString.replace(/<\s*tspan[^>]*>/g, '');
        var text = text.replace(/<\s*\/tspan[^>]*>/g, '');

        var coordMatch = tspanString.match(/x="(-?\d*\.?\d+)" y="(-?\d*\.?\d+)"/);

        if (coordMatch && coordMatch.length > 2) {
            x = parseFloat(coordMatch[1]);
            y = parseFloat(coordMatch[2]);
        }

        var groupTagMatch = svg.match(/<g transform.*>/);

        if (groupTagMatch) {
            groupString = groupTagMatch[0];

            // groupString is like <g transform="matrix(a b c d e f)" style=""  > where e = xMatrix et f = yMatrix
            var matrixMatch = groupString.match(/matrix\((-?\d*\.?\d+) (-?\d*\.?\d+) (-?\d*\.?\d+) (-?\d*\.?\d+) (-?\d*\.?\d+) (-?\d*\.?\d+)\)/);

            if (matrixMatch && matrixMatch.length > 6) {
                matrixString = matrixMatch[0];
                xMatrix = parseFloat(matrixMatch[5]);
                yMatrix = parseFloat(matrixMatch[6]);

                if (x !== null 
                    && y !== null 
                    && xMatrix !== null 
                    && yMatrix !== null 
                    && text !== null 
                    && groupString !== null 
                    && matrixString !== null
                ) {

                    var newMatrixString = 'matrix(' + matrixMatch[1] + ' ' + matrixMatch[2] + ' ' + matrixMatch[3] + ' ' + matrixMatch[4] + ' ' +
                        (x + xMatrix) + ' ' + (y + yMatrix) + ')';

                    transformedSvg = svg
                        .replace(matrixString, newMatrixString)
                        .replace(tspanString, text);

                    console.log('--> svg', svg);
                    console.log('--> transformedSvg', transformedSvg);
                }
            }
        }
    }

    return transformedSvg;
}
like image 29
Rands Avatar answered Sep 21 '22 05:09

Rands