Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TSPAN SUPPORT #1280

Open
Siyfion opened this issue Apr 15, 2014 · 46 comments
Open

TSPAN SUPPORT #1280

Siyfion opened this issue Apr 15, 2014 · 46 comments
Assignees

Comments

@Siyfion
Copy link

Siyfion commented Apr 15, 2014

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.

@kangax
Copy link
Member

kangax commented Apr 16, 2014

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.

@asturur asturur self-assigned this Sep 20, 2014
@asturur asturur changed the title SVG Parsing Bugs... TSPAN SUPPORT Oct 26, 2014
@christopherney
Copy link

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;
  };

@kangax
Copy link
Member

kangax commented May 15, 2015

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

@liatK
Copy link

liatK commented Aug 29, 2016

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!

@asturur
Copy link
Member

asturur commented Aug 29, 2016

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

@DukeCityDigital
Copy link

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

@asa9
Copy link

asa9 commented Jun 8, 2017

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

@asturur
Copy link
Member

asturur commented Jun 8, 2017

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

@asturur
Copy link
Member

asturur commented Jun 8, 2017

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

@asa9
Copy link

asa9 commented Jun 8, 2017

@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.

@asturur asturur removed this from the 2.0.0 milestone Jul 3, 2017
@chileap
Copy link

chileap commented Apr 10, 2020

Any updated with this?

@asturur
Copy link
Member

asturur commented Apr 11, 2020

No, no one worked on it.

@rohitkatlaa
Copy link

Any updates with this??

@asturur
Copy link
Member

asturur commented Jun 20, 2020

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

@reinkepatrick
Copy link

reinkepatrick commented Oct 30, 2020

@asturur Something new nowadays or any alternatives?

@asturur
Copy link
Member

asturur commented Nov 1, 2020

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

@shubhamdogra7042
Copy link

Hi @asturur, is it fixable. any workaround ?

@mr-ehsan-hashmi
Copy link

@asturur any update regarding ### TSpan working in SVG?

@asturur
Copy link
Member

asturur commented Oct 12, 2021

no one right now has time to work on this

@kevinreuss
Copy link

@asturur is there a solution?
I am trying to load an SVG file with tspans and the text gets rendered much too large

@bluegaspode
Copy link

bluegaspode commented Sep 17, 2022

I just added a bounty of 200$ on this open bug. Maybe others can join as well, as there seem to be multiple people interested in a fix.

https://app.bountysource.com/issues/1676489-tspan-support

I created the following smaller testcase, with some texts of different sizes and guidelines, which makes it easy to check the results. It was created with Inkscape and optimized for better readability with svgo

text-testcase1

@designlook
Copy link

designlook commented Feb 3, 2023

@bluegaspode Maybe something like this? I just did it quickly and didnt really flush out everything.. but just wanted to see.

https://jsfiddle.net/scottyu/0f3jkq7h/18/

@bluegaspode
Copy link

bluegaspode commented Feb 3, 2023

For the given example it works already, thats cool!

I have a second example, with just 4 aligned text-blocks, where it doesn't work though :(
Again it was created with Inkscape and optimized via svgo and I don't see any strong specialties in the markup.

<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100" id="svg4136" version="1.1">
    <g id="layer1">
        <text xml:space="preserve" style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" x="30" y="55" id="text4933"><tspan id="tspan4935" x="30" y="55" style="font-size:15px;line-height:1.25"> </tspan></text>
        <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" x="100.129" y="36" id="text4969"><tspan id="tspan4971" x="100.129" y="36" style="font-size:12.5px;line-height:1.25">###LINE2</tspan><tspan x="100.129" y="52" id="tspan4992" style="font-size:12.5px;line-height:1.25">###LINE3</tspan></text>
        <rect style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.59996504;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="rect4994" width="200.12" height="279.7" x="0" y="0" ry=".337"/>
        <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" x="100.129" y="18" id="text4996"><tspan id="tspan4998" x="100.129" y="18" style="font-size:12.5px;line-height:1.25">###LINE1</tspan></text>
        <text xml:space="preserve" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" x="99.717" y="71" id="text4996-5"><tspan id="tspan4998-9" x="99.717" y="71.508" style="font-size:10px;line-height:1.25">###KNX</tspan></text>
        <path style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" d="M0 60.275h200" id="path4200"/>
        <path style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.60000002;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M0 75h200" id="path4200-8"/>
    </g>
</svg>

target image should look like this (you can check with a browser or Inkscape)

image

with the given jsfiddle looks like this:
image

Once it works with the callback for fabric it would be great to see it as a pull request to fabric.js library, i.e. extending the loadSVGFromString function to support tspans 'natively'.

@somq
Copy link

somq commented Aug 1, 2023

Any update on this?

1 similar comment
@Selimology
Copy link

Any update on this?

@asturur
Copy link
Member

asturur commented Oct 26, 2023

no we are busy with other tasks sadly and we didn't do any new feature development

@ShaMan123
Copy link
Contributor

but if anyone wants to PR I can back them

@insinfo
Copy link

insinfo commented Jan 17, 2024

@gloriousjob @more-strive

Did you find any alternative solution to import a simple svg with correctly aligned text?

<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="637" height="1012" viewBox="305.5 50 637 1012" xml:space="preserve">
<desc>Created with Fabric.js 5.3.0</desc>
<defs>
</defs>
<g transform="matrix(1 0 0 1 624.5 556.5)" id="stage">
<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" x="-318.5" y="-506" rx="0" ry="0" width="637" height="1012"/>
</g>
<g transform="matrix(6.49 0 0 10.53 628.5 566.91)" id="rect-001pu2rkl">
<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(135,216,203); fill-rule: nonzero; opacity: 1;" x="-49.5" y="-49.5" rx="0" ry="0" width="99" height="99"/>
</g>
<g transform="matrix(6.66 0 0 6.66 486.08 127.53)" style="" id="itext-001k23wx5">
		<text xml:space="preserve" font-family="Calibri" font-size="20" font-style="normal" font-weight="normal" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(254,254,254); fill-rule: nonzero; opacity: 1; white-space: pre;"><tspan x="-21.69" y="6.28">Texto</tspan></text>
</g>
</svg>

svg

cracha_modelo

Fabric.js

image

@more-strive
Copy link

more-strive commented Jan 18, 2024

Most of my files are in PDF and PSD formats, and now they are parsed directly into JSON as templates
Github: https://github.com/dromara/yft-design
demo: https://yft.design
Of course, there are still some bugs, but there are no issues with parsing the content

@insinfo
Copy link

insinfo commented Jan 18, 2024

@more-strive
I accessed your demo, and saw that it has PDF and CDR import, how did you manage to implement the conversion from PDF/CDR to the fabic.js json format?

I managed to implement export to PDF, exporting from fabic.js to svg and adding it to the PDF via jsPDF

@more-strive
Copy link

more-strive commented Jan 18, 2024

PDF is parsed through pymupdf, but CDR has not yet implemented parsing
Github: https://githubfast.com/dromara/yft-design

@developersgit
Copy link

Any Update on this? Or any workaround to get the correct left and top values for text elements when importing the svg to canvas?

@asturur
Copy link
Member

asturur commented Jul 5, 2024

Hey @developersgit this is a long standing feature i was planning to take up again now that 6.0 is stable.
This one, svg masks, single object rendering, text on a path, and canvas rotation i think were the long standing one.
I still need to write some basic docs for 6.0 before going on more development work

@rryando
Copy link

rryando commented Aug 8, 2024

I found the workaround for text tag shifting , this caused by the object from loadSVGFromString func being flattened,
but when the text tag that has tspan child going to flattened, the tspan attributes are not being calculated, especially attributes that control the left and top pos (in this case x,y attributes),
so, for the workaround:

loadSVGFromString(reader.result as string).then((output) => {
    const {objects, elements} = output
    
    objects.forEach((obj, index) => {
      if (obj && obj.type === 'text') {
        const currentElement = elements[index]
        if (currentElement.children.length > 0 && currentElement.children[0].tagName === 'tspan') {
          const tspan = currentElement.children[0]
          // @ts-expect-error; TODO: define tspan types properly
          const { x, y } = tspan.attributes

          // THE FIX: Update x and y position of text object
          obj.left += Number(x.value)
          obj.top += Number(y.value)
        }
        // @ts-expect-error; TODO: define obj types properly
        const text = new Textbox(obj.text, {
          ...obj,
          snapAngle: 45,
          snapThreshold: 1,
          editable: true,
        })
        return canvas?.add(text);
      }
      obj && canvas?.add(obj);
  })
})

hope this helps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests