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

Export text on path to SVG #6958

Open
qaywsx22 opened this issue Mar 25, 2021 · 12 comments
Open

Export text on path to SVG #6958

qaywsx22 opened this issue Mar 25, 2021 · 12 comments

Comments

@qaywsx22
Copy link

Hello, an export a text on path to SVG is not working.
Test case:
https://jsfiddle.net/meugen22/ojatr2cL/5/

@asturur
Copy link
Member

asturur commented Mar 31, 2021

The only implementation of svg textpath that works across firefox and chrome is the hardest one to replicate.

@stale
Copy link

stale bot commented Apr 16, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale Issue marked as stale by the stale bot label Apr 16, 2021
@stale stale bot closed this as completed Apr 24, 2021
@hoomanaskari
Copy link

Okay, I have made some progress on this one, while it works, there are some things I am having trouble getting right.
pathSide right now only works in FF, so I don't know what to do with it. Also the size of the SVG, I have no idea how to accurately calculate it. Either way, here is my code:

My guess was that for fabric.Text we would not need tspan element in the SVG export. After getting rid of those, it was a simple solution. I am still not sure about the side effects of the following approach.

fabric.Text.prototype.toSVG = function (reviver) {
  if (this.type === 'text') {
    var noShadow = true,
      textDecoration = this.getSvgTextDecoration(this);

    var pathString = '';

    this.path?.path.forEach(p => {
      pathString += p.join(' ');
      pathString += ', ';
    });

    return this._createBaseSVGMarkup(
      this.path?.path ?
        [
          '<defs>',
          '<path id="textOnPathId" fill="none" stroke="none" d="',
          pathString,
          '" />',
          '</defs>',
          '\t\t<text xml:space="preserve" ',
          (this.fontFamily ? 'font-family="' + this.fontFamily.replace(/"/g, '\'') + '" ' : ''),
          (this.fontSize ? 'font-size="' + this.fontSize + '" ' : ''),
          (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''),
          (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''),
          (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''),
          'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >',
          '<textPath side="',
          this.pathSide,
          '" href="#textOnPathId">',
          this.text,
          '</textPath>',
          '</text>\n'
        ]
      :
        [
          '\t\t<text xml:space="preserve" ',
          (this.fontFamily ? 'font-family="' + this.fontFamily.replace(/"/g, '\'') + '" ' : ''),
          (this.fontSize ? 'font-size="' + this.fontSize + '" ' : ''),
          (this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''),
          (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''),
          (textDecoration ? 'text-decoration="' + textDecoration + '" ' : ''),
          'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >',
          this.text,
          '</text>\n'
        ],
      { reviver: reviver, noStyle: true, withShadow: true }
    );
  } else {
    return this._createBaseSVGMarkup(
      this._toSVG(),
      { reviver: reviver, noStyle: true, withShadow: true }
    );
  }
};

@asturur asturur added the feature label Jan 9, 2022
@asturur
Copy link
Member

asturur commented Jan 9, 2022

great! textOnPathId needs to be mixed with this.__uid in order to don't clash with multiple text on path on the same svg.

Also there is a common part of text code that we can reuse, right?

If you manage to get a single color, single path example working, open a PR. I m sure for styles and tSpan will be more complicated

@asturur asturur reopened this Jan 9, 2022
@stale stale bot removed the stale Issue marked as stale by the stale bot label Jan 9, 2022
@khajaamin
Copy link

facing same issue here
https://web-platform-n8kzvg.stackblitz.io

you can check using developer tool console with below command - Please add text and then run in console

canvas.loadFromJSON(canvas.toJSON(['p1', 'p0', 'p2', 'line','canvas']))

This command should add same components again

@ramonortigosa
Copy link

Hello!
Is there any improvement on this?

@nagendra-y
Copy link

https://jsfiddle.net/3btoymjd/

Does this help? Used hoomanaskari's approach and some transformations. Need to think of a bounding rect or something to make it generalise

@mudassirali007
Copy link

mudassirali007 commented Aug 14, 2023

Added more parameters.
side="right" will work on Firefox.

fabric.Text.prototype.toSVG = function (reviver) {
    if (this.path) {
        var noShadow = true,
            textDecoration = this.getSvgTextDecoration(this);

        var pathString = this.path?.path
            .map(function (segment) {
                return segment.join(" ");
            })
            .join(" ");
        const id = Math.random().toString(36).substr(2, 9);

        return this._createBaseSVGMarkup(
            this.path?.path
                ? [
                    "<defs>",
                    '<path id="textOnPath' + id + '" fill="none" stroke="none" d="',
                    pathString,
                    '" />',
                    "</defs>\n",
                    '<text xml:space="preserve" ',
                    this.fontFamily
                        ? 'font-family="' + this.fontFamily.replace(/"/g, "'") + '" '
                        : "",
                    this.fontSize ? 'font-size="' + this.fontSize + '" ' : "",
                    this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : "",
                    this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : "",
                    textDecoration ? 'text-decoration="' + textDecoration + '" ' : "",
                    'style="',
                    this.getSvgStyles(noShadow),
                    '"',
                    this.addPaintOrder(),
                    " >\n",
                    '<textPath side="' + this.pathSide + '" ',
                    'startOffset="' + this.pathStartOffset + '" ',
                    'href="#textOnPath' + id + '" ',
                    'xlink:href="#textOnPath' + id + '"> ',
                    this.text,
                    "</textPath>\n",
                    "</text>\n",
                ]
                : [
                    '\t\t<text xml:space="preserve" ',
                    this.fontFamily
                        ? 'font-family="' + this.fontFamily.replace(/"/g, "'") + '" '
                        : "",
                    this.fontSize ? 'font-size="' + this.fontSize + '" ' : "",
                    this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : "",
                    this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : "",
                    textDecoration ? 'text-decoration="' + textDecoration + '" ' : "",
                    'style="',
                    this.getSvgStyles(noShadow),
                    '"',
                    this.addPaintOrder(),
                    " >",
                    this.text,
                    "</text>\n",
                ],
            { reviver: reviver, noStyle: true, withShadow: true }
        );
    } else {
        return this._createBaseSVGMarkup(this._toSVG(), {
            reviver: reviver,
            noStyle: true,
            withShadow: true,
        });
    }
};

@herrstrietzel
Copy link

herrstrietzel commented Aug 17, 2023

Based on hoomanaskari's and mudassirali007's approaches - You might also add a function to reverse path direction.

This way we get a workaround for the unsupported "side" attribute (at least in chromium).
Besides, I added some conditions to translate fabric.js baseline alignment options to svg's dominant-basline values.

fabric.Text.prototype.toSVG = function (reviver) {
  let fontFamily = this.fontFamily.replace(/"/g, "'");
  let fontSize = this.fontSize;
  let fontStyle = this.fontStyle;
  let fontWeight = this.fontWeight;
  let fill = this.fill;

  if (this.path) {
    let path = this.path;
    let fillPath = path.fill ? path.fill : "none";
    let strokePath = path.stroke ? path.stroke : "none";
    let strokeWidth = path.strokeWidth ? path.strokeWidth : 0;

    // get path length
    let pathData = this.path.path;
    let pathInfo = fabric.util.getPathSegmentsInfo(pathData);
    let pathLength = pathInfo[pathInfo.length - 1].length;

    // reverse pathdata to emulate side="right"
    if (this.pathSide === "right") {
      // clone pathdata for reversing
      pathData = JSON.parse(JSON.stringify(pathData));
      pathData = reversePathData(pathData);
    }
    // get pathdata d string
    let d = pathData.flat().join(" ");

    let id = Math.random().toString(36).substr(2, 9);
    let dominantbaseline = "auto";
    let pathStartOffset = this.pathStartOffset;
    let dy = 0;

    // translate fabric.js baseline offsets to svg dominant baseline values
    if (this.pathAlign === "center") {
      dominantbaseline = "middle";
    } else if (this.pathAlign === "baseline") {
      dominantbaseline = "auto";
    } else if (this.pathAlign === "ascender") {
      dominantbaseline = "hanging";
    } else if (this.pathAlign === "descender") {
      dominantbaseline = "auto";
      dy = (fontSize / 100) * -22;
    }

    let textAnchor = "start";
    if (this.textAlign == "center") {
      textAnchor = "middle";
      pathStartOffset += pathLength / 2;
    }

    if (this.textAlign == "right") {
      textAnchor = "end";
      pathStartOffset += pathLength;
    }

    // append texpath to defs or as rendered element
    let textPathEl;
    if (
      (fillPath && fillPath !== "none") ||
      (!strokePath && strokePath !== "none")
    ) {
      textPathEl = `<path id="textOnPath${id}" fill="${fillPath}" stroke="${strokePath}" stroke-width="${strokeWidth}" d="${d}" />`;
    } else {
      textPathEl = `<defs>
        <path id="textOnPath${id}" d="${d}" />
      </defs>`;
    }

    return this._createBaseSVGMarkup(
      this.path?.path
        ? [
            textPathEl,
            `<text 
              font-family="${fontFamily.replace(/"/g, "'")}" 
              fill="${fill}"
              font-size="${fontSize}" 
              font-style="${fontStyle}" 
              font-weight="${fontWeight}"
              >
                <textPath text-anchor="${textAnchor}" 
                dominant-baseline="${dominantbaseline}" 
                startOffset="${pathStartOffset}" 
                href="#textOnPath${id}" 
                xlink:href="#textOnPath${id}"> 
                <tspan dy="${dy}">${this.text}</tspan>
                </textPath>
              </text>`
          ]
        : [
            `<text 
            xml:space="preserve" 
            font-family="${fontFamily}" 
            font-size="${fontSize}" 
            font-style="${fontStyle}" 
            font-weight="${fontWeight}" 
            > 
            ${this.addPaintOrder()}
            ${this.text}
            </text>`
          ],
      { reviver: reviver, noStyle: true, withShadow: true }
    );
  } else {
    return this._createBaseSVGMarkup(this._toSVG(), {
      reviver: reviver,
      noStyle: true,
      withShadow: true
    });
  }
};

/**
 * Reverse pathdata
 */
function reversePathData(pathData) {
  // start compiling new path data
  let pathDataNew = [];

  // helper to rearrange control points for all command types
  const reverseControlPoints = (values) => {
    let controlPoints = [];
    let endPoint = [];
    for (let p = 0; p < values.length; p += 2) {
      controlPoints.push([values[p], values[p + 1]]);
    }
    endPoint = controlPoints.pop();
    controlPoints.reverse();
    return [controlPoints, endPoint];
  };

  let closed =
    pathData[pathData.length - 1][0].toLowerCase() === "z" ? true : false;
  if (closed) {
    // add lineto closing space between Z and M
    pathData = addClosePathLineto(pathData);
    // remove Z closepath
    pathData.pop();
  }

  // define last point as new M if path isn't closed
  let valuesLast = pathData[pathData.length - 1];
  let valuesLastL = valuesLast.length;
  let M = closed
    ? pathData[0]
    : ["M", valuesLast[valuesLastL - 2], valuesLast[valuesLastL - 1]];
  // starting M stays the same – unless the path is not closed
  pathDataNew.push(M);

  // reverse path data command order for processing
  pathData.reverse();
  for (let i = 1; i < pathData.length; i++) {
    let com = pathData[i];
    let values = com.slice(1);
    let comPrev = pathData[i - 1];
    let typePrev = comPrev[0];
    let valuesPrev = comPrev.slice(1);
    // get reversed control points and new end coordinates
    let [controlPointsPrev, endPointsPrev] = reverseControlPoints(valuesPrev);
    let [controlPoints, endPoints] = reverseControlPoints(values);

    // create new path data
    let newValues = [];
    newValues = controlPointsPrev.flat().concat(endPoints);
    pathDataNew.push([typePrev, ...newValues]);
  }

  // add previously removed Z close path
  if (closed) {
    pathDataNew.push(["z"]);
  }
  return pathDataNew;
}

/**
 * Add closing lineto:
 * needed for path reversing or adding points
 */
function addClosePathLineto(pathData) {
  let pathDataL = pathData.length;
  let closed = pathData[pathDataL - 1][0] === "Z";
  let M = pathData[0];
  let [x0, y0] = [M[1], M[2]];
  let lastCom = closed ? pathData[pathDataL - 2] : pathData[pathDataL - 1];
  let lastComL = lastCom.length;
  let [xE, yE] = [lastCom[lastComL - 2], lastCom[lastComL - 1]];
  if (closed && (x0 !== xE || y0 !== yE)) {
    pathData.pop();
    pathData.push(["L", x0, y0], ["Z"]);
  }
  return path;
}

See codepen example

@Clemweb
Copy link

Clemweb commented Aug 21, 2023

@herrstrietzel pretty good! but I don't know why if I change the path the svg is weird (not at the good place). For example with this : M 75 0 A 10 10 0 0 1 75 -150 A 1 1 0 0 1 75 0 Z
Have you a idea ?

@celioFagundes
Copy link

@Clemweb have you found a solution to this? facing the same problem, any different path get's posicioned wrong on the export

@herrstrietzel
Copy link

@herrstrietzel, @Clemweb: sorry for the late response.
See new codepen example
Not sure, but I added a cropping/repositioning function so off-viewBox paths get aligned to x/y=0 origin point

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