Fabric.js: TSPAN SUPPORT

Created on 15 Apr 2014  ·  29Comments  ·  Source: fabricjs/fabric.js

So these issues all relate to the fabric.loadSVGFromString method, when given a string such as:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-10 -19.297698418170555 201.4199981689453 228.51539500528642"><desc>Created with Snap</desc><defs></defs><g><image xlink:href="http://imgh.us/picnic_12_p1_pic001_uk__v0.svg" preserveAspectRatio="none" x="0" y="0" width="181.42" height="189.92"></image></g><g transform="matrix(1,0,0,1,14.0374,82.2655)"><g><text x="0" y="6.0536095419811105" style="font-family: Jura; font-size: 12px; font-style: normal; font-weight: normal; text-decoration: none; text-anchor: start;"><tspan space="preserve" x="0" dy="1em">&lt;&lt;price&gt;&gt;</tspan></text><rect x="0" y="0" width="90.20648307120202" height="24.26346908396222" fill="#ffffff" stroke="#000000" style="fill-opacity: 0; stroke-width: 0.5px; stroke-dasharray: 5px, 2px;"></rect></g><line x1="45.10324153560101" x2="45.10324153560101" y1="12.13173454198111" y2="-10" fill="none" stroke="#eeeeee" style="stroke-dasharray: 2px, 2px; stroke-width: 0.7px; visibility: hidden;"></line><circle cx="45.10324153560101" cy="12.13173454198111" r="2.5" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></circle><circle cx="45.10324153560101" cy="-10" r="2.5" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></circle><rect x="88.20648307120202" y="10.13173454198111" width="4" height="4" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></rect><rect x="43.10324153560101" y="22.26346908396222" width="4" height="4" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></rect></g><g transform="matrix(1,0,0,1,14.0374,7.5084)"><g><text x="77.585072682781" y="-3.4223932794852185" style="font-family: 'Mr Dafoe'; font-size: 20px; font-style: normal; font-weight: normal; text-decoration: none; text-anchor: middle;"><tspan space="preserve" x="77.585072682781" dy="1em">&lt;&lt;title&gt;&gt;</tspan></text><rect x="0" y="0" width="155.170145365562" height="20.998963441029563" fill="#ffffff" stroke="#000000" style="fill-opacity: 0; stroke-width: 0.5px; stroke-dasharray: 5px, 2px;"></rect></g><line x1="77.585072682781" x2="77.585072682781" y1="10.499481720514781" y2="-10" fill="none" stroke="#eeeeee" style="stroke-dasharray: 2px, 2px; stroke-width: 0.7px; visibility: hidden;"></line><circle cx="77.585072682781" cy="10.499481720514781" r="2.5" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></circle><circle cx="77.585072682781" cy="-10" r="2.5" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></circle><rect x="153.170145365562" y="8.499481720514781" width="4" height="4" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></rect><rect x="75.585072682781" y="18.998963441029563" width="4" height="4" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></rect></g><g transform="matrix(1,0,0,1,14.0374,31.0128)"><g><text x="0" y="0" style="font-family: 'Shadows Into Light'; font-size: 10px; font-style: normal; font-weight: normal; text-decoration: none; text-anchor: start;"><tspan space="preserve" x="0" dy="1em">&lt;&lt;description&gt;&gt;</tspan></text><rect x="0" y="0" width="154.84369480126873" height="49.72661309883699" fill="#ffffff" stroke="#000000" style="fill-opacity: 0; stroke-width: 0.5px; stroke-dasharray: 5px, 2px;"></rect></g><line x1="77.42184740063436" x2="77.42184740063436" y1="24.863306549418496" y2="-10" fill="none" stroke="#eeeeee" style="stroke-dasharray: 2px, 2px; stroke-width: 0.7px; visibility: hidden;"></line><circle cx="77.42184740063436" cy="24.863306549418496" r="2.5" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></circle><circle cx="77.42184740063436" cy="-10" r="2.5" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></circle><rect x="152.84369480126873" y="22.863306549418496" width="4" height="4" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></rect><rect x="75.42184740063436" y="47.72661309883699" width="4" height="4" fill="#000000" stroke="#eeeeee" style="stroke-width: 0.4px; visibility: hidden;"></rect></g></svg>

Which given the SVG rendered here:
screen shot 2014-04-15 at 16 50 04

Produces the following output:
screen shot 2014-04-15 at 16 50 18

Which is doing several things wrong:

  • It ignores the "visibility: hidden" style property.
  • It is incorrectly positioning several elements; notably all the text, and the circles which should be hidden anyway (and are way off the "render area" of the canvas)

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

feature svg_parser

Most helpful comment

I change the fabrics.js 👍 https://github.com/haki9/fabric.js/commit/6d049ed0954ac08b7cd927975ca500e6ddd4fd0b

And code javascript:

fabric.loadSVGFromURL(url, function (
            objects,
            options,
            elements,
            allElements
        ) {
            objects.forEach(function (obj, index) {
                if (obj["type"] == "image") {
                    if (obj["id"].toUpperCase() == "BACKGROUND") {
                        fabric.Image.fromURL(obj['xlink:href'], function (img) {
                            canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
                        });
                    } else {
                        obj.set({
                            selectable: true,
                            evented: true
                        });
                        canvas.add(obj).renderAll();
                    }
                } else if (obj["type"] == "text") {
                    var element = elements[index];
                    var childrens = [].slice.call(element.childNodes);
                    var value = "";
                    childrens.forEach(function (el, index, array) {
                        if (el.nodeName == "tspan") {
                            value += el.childNodes[0].nodeValue;
                        } else if (el.nodeName == "#text") {
                            value += el.nodeValue;
                        }

                        if (index < childrens.length - 1) {
                            value += "\n";
                        }
                    });

                    value =
                        obj["text-transform"] == "uppercase"
                            ? value.toUpperCase()
                            : value;

                    var text = new fabric.IText(obj.text, obj.toObject());
                    text.set({
                        text: value,
                        type: 'i-text'
                    });

                    var left = 0;
                    var _textAlign = obj.get("textAnchor")
                        ? obj.get("textAnchor")
                        : "left";
                    switch (_textAlign) {
                        case "center":
                            left = obj.left - text.getScaledWidth() / 2;
                            break;
                        case "right":
                            left = obj.left - text.getScaledWidth();
                            break;
                        default:
                            left = obj.left;
                            break;
                    }

                    text.set({
                        left: left,
                        textAlign: _textAlign
                    });
                    canvas.add(text).renderAll();
                } else {
                    canvas.add(obj).renderAll();
                }
            });

            canvas.setWidth(options.width);
            canvas.setHeight(options.height);
            canvas.calcOffset();

And this is SVG file for testing: https://drive.google.com/open?id=1laXzM0SyK1URzHVy9blCFeBDcaWlV78P

All 29 comments

Ok, so I fixed few things.

"visibility: hidden" should now be working. Text is now positioned slightly better, but not exactly correct yet. We need to add support for "text-anchor" which isn't straightforward.

I'll be looking into it more.

Hi,

I write this code to start to manage tspan position x/y :

  fabric.Text.positionFromTspan = function(element, options) {

        var position = {
            x: 0,
            y: 0
        };

        var childrens = [].slice.call(element.childNodes);

        var tspans = new Array();

        childrens.forEach(function(el, index, array) {

            if (el.nodeName == 'tspan') {
                tspans.push(el);
            }
        });

        if (tspans.length > 0) {

            var tspan = tspans[0];

            var attributes = [].slice.call(tspan.attributes);

            attributes.forEach(function(attr, index, array) {

                if (attr.nodeName == 'x') {
                    position.x = parseFloat(attr.nodeValue);
                } else if (attr.nodeName == 'y') {
                    position.y = parseFloat(attr.nodeValue);
                } 

            });

            return position;

        } else {

            return undefined;
        }
  };

  /**
   * Returns fabric.Text instance from an SVG element (<b>not yet implemented</b>)
   * @static
   * @memberOf fabric.Text
   * @param {SVGElement} element Element to parse
   * @param {Object} [options] Options object
   * @return {fabric.Text} Instance of fabric.Text
   */
  fabric.Text.fromElement = function(element, options) {
    if (!element) {
      return null;
    }

    var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES);
    options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes);

    // TSPAN :
    var position = this.positionFromTspan(element, options);

    if (position != undefined) {
        options.top = position.y;
        options.left = position.x;
    } else {
        options.top = options.top || 0;
        options.left = options.left || 0;
    }

    if ('dx' in parsedAttributes) {
      options.left += parsedAttributes.dx;
    }
    if ('dy' in parsedAttributes) {
      options.top += parsedAttributes.dy;
    }

    if (!('fontSize' in options)) {
      options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE;
    }

    if (!options.originX) {
      options.originX = 'left';
    }
    var textContent = element.textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' '),
        text = new fabric.Text(textContent, options),
        /*
          Adjust positioning:
            x/y attributes in SVG correspond to the bottom-left corner of text bounding box
            top/left properties in Fabric correspond to center point of text bounding box
        */
        offX = 0;

    if (text.originX === 'left') {
      offX = text.getWidth() / 2;
    }
    if (text.originX === 'right') {
      offX = -text.getWidth() / 2;
    }
    text.set({
      left: text.getLeft() + offX,
      top: text.getTop() - text.getHeight() / 2 + text.fontSize * (0.18 + text._fontSizeFraction) /* 0.3 is the old lineHeight */
    });

    return text;
  };

I closed #820 but we should just not forget to account for rotate attribute on tspans

Hi guys,
I'm really straggling with this issue, I'm creating an editor and it's very important to have the ability to save and load back for edit exactly as it was edited, that's proven to be difficult when importing back from svg it re position text elements, is there any clean fix/workaround for this? it's becoming crucial whether or not keep using this approach if no solution will be found.
Thanks!

can t you just use json for exporting and reimporting? svg is not meant for that

Wondering about this issue - any possible workarounds related to how the .SVG is initially constructed/saved? Great library thanks

Hello,

@asturur you are suggesting to use json for exporting and importing, but what if we need to create an export pdf file ? I am using both exporting formats: 1. SVG - for PDF export 2. JSON for later import in canvas. But now I have encountered an issue with updating text in SVG. For text objects I am using texbox as I need text-wrapping. The thing is that after export in SVG each letter is kept in separate tspan with its own styling. But later when I want to update that text (for example some tranbsaltion key ahs been added to canvas and before export to pdf I want to change that key with real translated value) I don't know what styling to apply to tspans. So my question mainly is how fabirc is calculating styling for tspans position, etc. Or is there other way to update text in exported svg without loosing styling and avoiding tspans styling calculation ?

Thanks in advance

there is not. Svg is pretty much an image with POOR text support.

you could make a trick, modify the svg export, add an attribute to text object with a json stringfy of the style object, and make some custom code when reimport it.

not something i want to support.

i want to do proper TSPAN support but i did not start yet

Maybe i understood bad. what styling are you talking about? color, font, size?

@asturur first of all thanks for your very quick response.

My main problem is that pdf export is done via cron job, when editor is closed. So I can't export svg directly from canvas. I am taking svg from DB and tryng to translate it before export.

So now about stylings mentioned in above comment: I am talking about the style of each tspan element. When we export canvas to svg each letter of canvas text object is kept in separate "" tag that keeps some styling which I guess is responsible for letter position, alignment, font, etc.
For example lets consider that in my SVG there is '#' translation key, that should be replaced with 'Hello' string. Please have a look on below tspan element:
<tspan x="-100" y="7.87" style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: ; opacity: 1;">#</tspan>

So for translating SVG I need to replace '#' to 'Hello' which means that above tspan(s) list should be changed to new tspan list containing letters for 'Hello' (Means that 1 tspan should be replaced with 5 tspans). Well for 1st tspans can be kept the same x, y, style (but maybe even this can not be kept, as if I have translated 2nd letter bigger/smaller than trasnaltion key letter than might be that x coordinate should be changed as well), but what about other new tspans. The problem is that I don't know what x, y, style to apply for each letter of "Hello". I guess as for styling I can take translation keys last letter style for all other newer elements, but what about x, y position. This is my main problem. So the question can be rephrased so how x, y attribute values are calculated for each tspan based on letter that it contains. Or in general how a text can be changed with other text in SVG without loosing any information about its styling.

Actually I was doing translation of SVG without taking into account tspan at all. Just each text ndoeValue was changed $text->nodeValue = $translatedText (Please refer to below translateTemplateSVG ). BUt then I realised that with this approach text alignments are lost (left, right, center), so I came to an idea that tspans should be kept to have everything working as they supposed to.

static public function translateTemplateSVG($svg) {
        $doc = new DOMDocument();
        $doc->loadXML($svg);
        $texts = $doc->getElementsByTagName('text');

        if(!$texts){
            return $svg;
        }

        foreach ($texts as $text) {
            $currentText = preg_replace('/\s+/', '', $text->nodeValue);
            $traslatedText = self::translateText($currentText);
            if($traslatedText){
                $text->nodeValue = $traslatedText;
            }
        }

        return $doc->saveXML();
}

Looking forward to hear from you soon.

Hello @asturur; I was curious if there is any future plan for implementing tspansupport for multiline text importing. I see it was removed from the 2.0.0 milestone. :(

clippath is priority and masks.
tspan support has started with the vertical offset that is ready in a branch and unmerged.

Thanks @asturur! Mind sharing the branch that has the changes for handling the vertical offset. Didn't see it in the stale or active branches. Apologies if it is mixed in with other branches and I just didn't look hard enough. Thanks for your work on this project!

is in the branch list, is there. search for superscrit or subscript

On 21 Dec 2017 21:13, "Seth Messer" notifications@github.com wrote:

Thanks @asturur https://github.com/asturur! Mind sharing the branch
that has the changes for handling the vertical offset. Didn't see it in the
stale or active branches. Apologies if it is mixed in with other branches
and I just didn't look hard enough. Thanks for your work on this project!


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/kangax/fabric.js/issues/1280#issuecomment-353447070,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABI4QKS99LBhcG8XhL4XmE5dr06hLJfWks5tCrwEgaJpZM4Bysv3
.

baseline-shift is the name. but it does not contain much about tspan yet

On 21 Dec 2017 21:20, "Andrea Bogazzi" andreabogazzi79@gmail.com wrote:

is in the branch list, is there. search for superscrit or subscript

On 21 Dec 2017 21:13, "Seth Messer" notifications@github.com wrote:

Thanks @asturur https://github.com/asturur! Mind sharing the branch
that has the changes for handling the vertical offset. Didn't see it in the
stale or active branches. Apologies if it is mixed in with other branches
and I just didn't look hard enough. Thanks for your work on this project!


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/kangax/fabric.js/issues/1280#issuecomment-353447070,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABI4QKS99LBhcG8XhL4XmE5dr06hLJfWks5tCrwEgaJpZM4Bysv3
.

Thanks so much @asturur; If y'all have any ideas/details around what you were wanting to see happen with that, I'd be happy to work on it in that baseline-shift branch or in a new branch. It's a pretty important capability for what I'm working on, so I'd for sure be available to work on implementing that feature, at least to some basic degree. Thanks and let me know! Have a great day and Christmas/holiday season if I don't hear back from you.

well if you want to research on it, we still need parsing of text element,
preserving the tspan properties for each text chunk and also the dx and dy
attribute of tspans can be in written in many different ways and they need
to be parsed as well.

tspan that look like different text lines will probably not be different
text lines of the same text. will likely be different fabricjs texts.

On 21 Dec 2017 21:28, "Seth Messer" notifications@github.com wrote:

Thanks so much @asturur https://github.com/asturur; If y'all have any
ideas/details around what you were wanting to see happen with that, I'd be
happy to work on it in that baseline-shift branch or in a new branch.
It's a pretty important capability for what I'm working on, so I'd for sure
be available to work on implementing that feature, at least to some basic
degree. Thanks and let me know! Have a great day and Christmas/holiday
season if I don't hear back from you.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/kangax/fabric.js/issues/1280#issuecomment-353450150,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABI4QOyVBOpHz6stmMD2TTZPoaAQz1qZks5tCr98gaJpZM4Bysv3
.

Isn't it possible to just remove the tspan from the text elements, as a workaround ? Like, in the reviver method ?

Tspans are removed indeed, but positions and different styles are lost.

Good afternoon friends I had the same problem to position the tspan with the loadSVGFromString method, I noticed that creating by itext and passing the text configuration parameters the position is more accurate. So far I have managed to position it as in the following example codepen but want to achieve a more exact position. I have tried different methods but as far as Tspan still has no support in the versions of fabricjs

I change the fabrics.js 👍 https://github.com/haki9/fabric.js/commit/6d049ed0954ac08b7cd927975ca500e6ddd4fd0b

And code javascript:

fabric.loadSVGFromURL(url, function (
            objects,
            options,
            elements,
            allElements
        ) {
            objects.forEach(function (obj, index) {
                if (obj["type"] == "image") {
                    if (obj["id"].toUpperCase() == "BACKGROUND") {
                        fabric.Image.fromURL(obj['xlink:href'], function (img) {
                            canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
                        });
                    } else {
                        obj.set({
                            selectable: true,
                            evented: true
                        });
                        canvas.add(obj).renderAll();
                    }
                } else if (obj["type"] == "text") {
                    var element = elements[index];
                    var childrens = [].slice.call(element.childNodes);
                    var value = "";
                    childrens.forEach(function (el, index, array) {
                        if (el.nodeName == "tspan") {
                            value += el.childNodes[0].nodeValue;
                        } else if (el.nodeName == "#text") {
                            value += el.nodeValue;
                        }

                        if (index < childrens.length - 1) {
                            value += "\n";
                        }
                    });

                    value =
                        obj["text-transform"] == "uppercase"
                            ? value.toUpperCase()
                            : value;

                    var text = new fabric.IText(obj.text, obj.toObject());
                    text.set({
                        text: value,
                        type: 'i-text'
                    });

                    var left = 0;
                    var _textAlign = obj.get("textAnchor")
                        ? obj.get("textAnchor")
                        : "left";
                    switch (_textAlign) {
                        case "center":
                            left = obj.left - text.getScaledWidth() / 2;
                            break;
                        case "right":
                            left = obj.left - text.getScaledWidth();
                            break;
                        default:
                            left = obj.left;
                            break;
                    }

                    text.set({
                        left: left,
                        textAlign: _textAlign
                    });
                    canvas.add(text).renderAll();
                } else {
                    canvas.add(obj).renderAll();
                }
            });

            canvas.setWidth(options.width);
            canvas.setHeight(options.height);
            canvas.calcOffset();

And this is SVG file for testing: https://drive.google.com/open?id=1laXzM0SyK1URzHVy9blCFeBDcaWlV78P

@haki9 I used your method, it works just fine for styling, but as soon as I put multiple lines in my text, the top positioning of the text gets messed up.

Any updated with this?

No, no one worked on it.

Any updates with this??

no zero, zero.
Although with dx,dy support we have someone could try to implement it.
A new svg parser would make things easeir

@asturur Something new nowadays or any alternatives?

No, not at all. I ll probably get it done after i made curved text better and after i improved the masks pr

Hi @asturur, is it fixable. any workaround ?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

keanass picture keanass  ·  5Comments

urcoder picture urcoder  ·  5Comments

semiadam picture semiadam  ·  3Comments

Vivek-KT picture Vivek-KT  ·  3Comments

medialwerk picture medialwerk  ·  5Comments