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

toSVG() font outlines #100

Closed
Reggino opened this issue Jan 9, 2012 · 42 comments
Closed

toSVG() font outlines #100

Reggino opened this issue Jan 9, 2012 · 42 comments
Labels

Comments

@Reggino
Copy link

Reggino commented Jan 9, 2012

Great work on fabric.js, really impressive!! I was just trying the new toSVG feature, fantastic!

I still have a small feature request though: it seems that text is exported as text with a defined font. Maybe it would be nice if font outlines are exported so the svg can be used by people who don't have that particular font installed....

@kangax
Copy link
Member

kangax commented Jan 9, 2012

I'm actually working on this at the moment. Text is one of the few remaining things left to polish in SVG export. In browsers, text already works as expected — it's being rendered with correct font. Illustrator, however, doesn't seem to allow linking to fonts. I'm not yet sure how to fix it (without embedding font in SVG itself). Any help would be great.

@kangax
Copy link
Member

kangax commented Apr 8, 2012

It turned out that Illustrator does render text objects properly as long as corresponding fonts are present in the system (and have identical name). So it works pretty well now. Font outlines is something we can look at in the future, but now it's not a priority.

@kangax kangax closed this as completed Apr 8, 2012
@luklukls
Copy link

"fill:none;fill-opacity:1;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"

@wesleyguirra
Copy link

@kangax Is it have been or will be implemented?

@asturur
Copy link
Member

asturur commented May 20, 2019

no font in outlines.
Is definitely out of scope since we need at least openType.js and and other dependencies to be implemented.

Writing your on toSVG code is not hard anyway if you know how to handle the path transformation.

@wesleyguirra
Copy link

I've tried to understand path transformation, but it's not easy

@asturur
Copy link
Member

asturur commented May 20, 2019

can you get from font to path and you have difficulties positioning them on the SVG or you have difficulties moving from text to path?

@dpinheiro
Copy link

dpinheiro commented Aug 6, 2019

Any updates on this? I'm also interested in converting font to path so the users don't need to have the font installed if they want to edit the end result in some other software

@asturur
Copy link
Member

asturur commented Aug 6, 2019

No one is working on that that i know of.
Also unsure if is something you want to have in fabricJS.
Would be better maybe to have glyphs support in fabricJS and have an external utility that creates gliphs from fonts.

@timoostrich
Copy link

Any updates on this? I'm also interested in converting font to path so the users don't need to have the font installed if they want to edit the end result in some other software

That's exactly what I want to do, too.
Did you find a solution?

@asturur
Copy link
Member

asturur commented Jun 30, 2020

The solution would be an integration with https://opentype.js.org/

@timoostrich
Copy link

The solution would be an integration with https://opentype.js.org/

Thanks. I will have a look at it.
Btw: I love fabric.js - thank you so much for this fantastic library.
This is one of the tools I created based on fabric.js: https://lettering.org/lettering-generator/

@juzhiyuan
Copy link
Contributor

@timoostrich wow! good tools!

@asturur
Copy link
Member

asturur commented Jul 1, 2020

i had to look into this for work yesterday by chance and i have to say opentype.js integration is super simple and super useful.

@timoostrich
Copy link

i had to look into this for work yesterday by chance and i have to say opentype.js integration is super simple and super useful.

How did you manage the SVG fonts to paths problem? Could you share some details? That would be awesome.

@asturur
Copy link
Member

asturur commented Jul 1, 2020

from opentype.js docs

const font = await opentype.load('fonts/Roboto-Black.ttf');
const path = font.getPath('Hello, World!', 0, 150, 72);
const svgPath = path.toPathData(3); // 3 is decimal places

it gives you the outline for that particular font and text.

@juzhiyuan
Copy link
Contributor

image
image
image

It seems work now!

@asturur
Copy link
Member

asturur commented Jul 1, 2020

Did you use the custom control api?

@juzhiyuan
Copy link
Contributor

Yep!

  1. Get paths from Text;
  2. Build polygon by points;
  3. Use control api

@asturur
Copy link
Member

asturur commented Jul 1, 2020

you are missing the control points controls of the beizer curves :)

@juzhiyuan
Copy link
Contributor

juzhiyuan commented Jul 2, 2020

🤔 OOO! It's called Bezier curves! I will do a search for it!

@juzhiyuan
Copy link
Contributor

you are missing the control points controls of the beizer curves :)

Hi @asturur , not sure if it's because there have some errors while calculating Paths or sub path, or as you said, I missed the control points of the beizer curves, how could I resolve this issue? Could you please provide some examples? Thanks!

@asturur
Copy link
Member

asturur commented Jul 26, 2020

i had to write curve transformation few days ago, i realized that to have full control:

  • the path has to use only absolute commands
  • V and H needs to be changed in L
  • A must be swapped with a series of C or

@juzhiyuan
Copy link
Contributor

https://yqnn.github.io/svg-path-editor/

still have no idea :(

@juzhiyuan
Copy link
Contributor

Could you please provide some links or samples when you are free?

@juzhiyuan
Copy link
Contributor

The SVG paths generated by opentype.js is perfectly shown, but once I transform the paths to points, the Polygon is what https://user-images.githubusercontent.com/2106987/86267366-2c238e80-bbf9-11ea-863f-3f4aa7605768.png like.

@juzhiyuan
Copy link
Contributor

  • swapped
  1. Convert Paths to absolute commands: http://phrogz.net/convert-svg-path-to-all-absolute-commands

@asturur
Copy link
Member

asturur commented Jul 28, 2020

ok wait, you can't convert a path to polygon, not easily and not with many many points.

You can apply the control api logic to a path.
to make it work you need to convert the commands all to absolute, and eliminate the A entirely.
Swap V and H with L. I can provide code for everything but not the A because i do not have ready yet.

Once the swap is done, you can position the controls following specific values.
Tomorrow i should be able to paste some useful code.

@juzhiyuan
Copy link
Contributor

ok wait, you can't convert a path to polygon, not easily and not with many many points.

You can apply the control api logic to a path.
to make it work you need to convert the commands all to absolute, and eliminate the A entirely.
Swap V and H with L. I can provide code for everything but not the A because i do not have ready yet.

Once the swap is done, you can position the controls following specific values.
Tomorrow i should be able to paste some useful code.

That would be great!! I'm looking forward to those codes!

@asturur
Copy link
Member

asturur commented Jul 29, 2020

This first function makes a path absolute ( Does not take care of removing A yet )
It takes fabricPath.path as argument and return a new one.
you can use it myPath.path = makePathAbsolute(myPath.path)

const makePathAbsolute = (path) => {
  // x and y represent the last point of the path. the previous command point.
  // we add them to each relative command to make it an absolute comment.
  // we also swap the v V h H with L, because are easier to transform.
  let x = 0;
  let y = 0;
  let current;
  const newPath = path.map((subPath) => subPath.slice(0));
  for (let i = 0, len = newPath.length; i < len; ++i) {
    current = newPath[i];
    switch (current[0]) { // first letter
      case 'l': // lineto, relative
        current[0] = 'L';
        current[1] += x;
        current[2] += y;
        // falls through
      case 'L':
        x = current[1];
        y = current[2];
        break;

      case 'h': // horizontal lineto, relative
        current[1] += x;
        // falls through
      case 'H':
        current[0] = 'L';
        current[2] = y;
        x = current[1];
        break;

      case 'v': // vertical lineto, relative
        current[1] += y;
        // falls through
      case 'V':
        current[0] = 'L';
        y = current[1];
        current[1] = x;
        current[2] = y;
        break;

      case 'm': // moveTo, relative
        current[0] = 'M';
        current[1] += x;
        current[2] += y;
        // falls through
      case 'M':
        x = current[1];
        y = current[2];
        break;

      case 'c': // bezierCurveTo, relative
        current[0] = 'C';
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
        current[5] += x;
        current[6] += y;
        // falls through
      case 'C':
        x = current[5];
        y = current[6];
        break;

      case 's': // shorthand cubic bezierCurveTo, relative
        current[0] = 'S';
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
        // falls through
      case 'S':
        x = current[3];
        y = current[4];
        break;

      case 'q': // quadraticCurveTo, relative
        current[0] = 'Q';
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
        // falls through
      case 'Q':
        x = current[3];
        y = current[4];
        break;

      case 't': // shorthand quadraticCurveTo, relative
        current[0] = 'T';
        current[1] += x;
        current[2] += y;
        // falls through
      case 'T':
        x = current[1];
        y = current[2];
        break;

      case 'a':
        current[0] = 'A';
        current[6] += x;
        current[7] += y;
        // falls through
      case 'A':
        x = current[6];
        y = current[7];
        break;
      default:
    }
  }
  return newPath;
};

This other transforms the path commands coordinates in absolute coordinates.
And i ll change it later, for now i paste it here or i loose it.

const getPathScaledForTransform = (path, transform) => {
  const absolutePath = makePathAbsolute(path);
  const indexesWithXMap = {
    L: [1],
    M: [1],
    C: [1, 3, 5],
    S: [1, 3],
    Q: [1, 3],
    T: [1],
    A: [6],
  };
  absolutePath.forEach((subPath) => {
    const command = subPath[0];
    if (command === 'z') {
      return;
    }
    const indexes = indexesWithXMap[command];
    indexes.forEach((index) => {
      const point = { x: subPath[index], y: subPath[index + 1] };
      const transformedPoint = fabric.util.transformPoint(point, transform);
      subPath[index] = transformedPoint.x;
      subPath[index + 1] = transformedPoint.y;
    });
  });
  return pathDataToString(absolutePath);
};

@juzhiyuan
Copy link
Contributor

wow!! Thanks for your detailed reply! I will have a try tomorrow morning, thanks anyway!

@juzhiyuan
Copy link
Contributor

i had to write curve transformation few days ago, i realized that to have full control:

  • the path has to use only absolute commands
  • V and H needs to be changed in L
  • A must be swapped with a series of C or

Morning @asturur :)

  1. 1st A must be swapped with a series of C or? It seems there has something were missing?
  2. A means Elliptical Arc Curve? And it should be replaced with Cubic Bézier Curve, so I could resolve toSVG() font outlines #100 (comment) ?
  3. I noticed there has one function called pathDataToString in toSVG() font outlines #100 (comment) , but without the functionality, so I just need to implement one as the name means.

@asturur
Copy link
Member

asturur commented Jul 29, 2020

Here is the cleaned up code that takes care of the elliptic arc swap, i also removed the conversion to string since you do not really need it.

Once the path is converted, you have just L, M, C, S, T, Q commands.
Now L and M are treated exactly as polygon points.

C S T Q no.

C: is made of 'C', cx1, cy1, cx2, cy2, x ,y
3 couples of points, x and y, the last one, can be moved as a polygon point, while the first two couples are different, are the controls points.

for S, T, Q is similar, with less points and some constrain, i will try to add an example on how you actually modify those paths with the control api.

const fromArcToBeizers = (x, y, aCommand) => {
  // shimming ctx to capture commands.
  const commands = [];
  const ctx = {
    bezierCurveTo: (...args) => {
      commands.push(['C', ...args]);
    },
  };
  // short way of pulling out the conversion without modifying fabric for now
  fabric.util.drawArc(ctx, x, y, [
    aCommand[1],
    aCommand[2],
    aCommand[3],
    aCommand[4],
    aCommand[5],
    aCommand[6],
    aCommand[7],
  ]);
  return commands;
};

// taken from my fabricJS things, this is going to go away because will be in fabric
// newer versions and we can get rid of it here.
// this will change the relative parts of a path command to absolute.
const makePathAbsolute = (path) => {
  // x and y represent the last point of the path. the previous command point.
  // we add them to each relative command to make it an absolute comment.
  // we also swap the v V h H with L, because are easier to transform.
  let x = 0;
  let y = 0;
  let current;
  const newPath = path.map((subPath) => subPath.slice(0));
  const destinationPath = [];
  for (let i = 0, len = newPath.length; i < len; ++i) {
    let converted = false;
    current = newPath[i];
    switch (current[0]) { // first letter
      case 'l': // lineto, relative
        current[0] = 'L';
        current[1] += x;
        current[2] += y;
        // falls through
      case 'L':
        x = current[1];
        y = current[2];
        break;

      case 'h': // horizontal lineto, relative
        current[1] += x;
        // falls through
      case 'H':
        current[0] = 'L';
        current[2] = y;
        x = current[1];
        break;

      case 'v': // vertical lineto, relative
        current[1] += y;
        // falls through
      case 'V':
        current[0] = 'L';
        y = current[1];
        current[1] = x;
        current[2] = y;
        break;

      case 'm': // moveTo, relative
        current[0] = 'M';
        current[1] += x;
        current[2] += y;
        // falls through
      case 'M':
        x = current[1];
        y = current[2];
        break;

      case 'c': // bezierCurveTo, relative
        current[0] = 'C';
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
        current[5] += x;
        current[6] += y;
        // falls through
      case 'C':
        x = current[5];
        y = current[6];
        break;

      case 's': // shorthand cubic bezierCurveTo, relative
        current[0] = 'S';
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
        // falls through
      case 'S':
        x = current[3];
        y = current[4];
        break;

      case 'q': // quadraticCurveTo, relative
        current[0] = 'Q';
        current[1] += x;
        current[2] += y;
        current[3] += x;
        current[4] += y;
        // falls through
      case 'Q':
        x = current[3];
        y = current[4];
        break;

      case 't': // shorthand quadraticCurveTo, relative
        current[0] = 'T';
        current[1] += x;
        current[2] += y;
        // falls through
      case 'T':
        x = current[1];
        y = current[2];
        break;

      case 'a':
        current[0] = 'A';
        current[6] += x;
        current[7] += y;
        // falls through
      case 'A':
        converted = true;
        destinationPath.push(...fromArcToBeizers(x, y, current));
        x = current[6];
        y = current[7];
        break;
      default:
    }
    if (!converted) {
      destinationPath.push(current);
    }
  }
  return destinationPath;
};

const getPathScaledForTransform = (path, transform) => {
  const absolutePath = makePathAbsolute(path);
  const indexesWithXMap = {
    L: [1],
    M: [1],
    C: [1, 3, 5],
    S: [1, 3],
    Q: [1, 3],
    T: [1],
  };
  absolutePath.forEach((subPath) => {
    const command = subPath[0];
    if (command === 'z') {
      return;
    }
    const indexes = indexesWithXMap[command];
    indexes.forEach((index) => {
      const point = { x: subPath[index], y: subPath[index + 1] };
      const transformedPoint = fabric.util.transformPoint(point, transform);
      subPath[index] = transformedPoint.x;
      subPath[index + 1] = transformedPoint.y;
    });
  });
  return absolutePath;
};

@juzhiyuan
Copy link
Contributor

juzhiyuan commented Jul 30, 2020

Hello, according to above replies, I just made a jsfiddle[1] here.

This fiddle aims to transform Text(or Path) to Polygon, Not sure if we could implement this like here[2]:

  1. Get the target path, in this fiddle, please notice Line 183 in JS file;
  2. Generate new path by using new fabric.Path(targetPath);
  3. Get the final points by using getPathScaledForTransform(newPath.path, newPath.calcTransformMatrix())
  4. in Line 193, should I try to combine the final points with Custom Control Point API?

[1] https://jsfiddle.net/juzhiyuan/f18ta2cp/65/
[2] https://yqnn.github.io/svg-path-editor/

screencapture-yqnn-github-io-svg-path-editor-1596090050061

Test Path:

M 323.7617 494.7578 L 334.5723 494.7578 L 334.7188 497.8633 L 334.3477 497.8633 Q 334.2402 497.043 334.0547 496.6914 L 334.0547 496.6914 Q 333.752 496.125 333.249 495.8564 Q 332.7461 495.5879 331.9258 495.5879 L 331.9258 495.5879 L 330.0605 495.5879 L 330.0605 505.7051 Q 330.0605 506.9258 330.3242 507.2285 L 330.3242 507.2285 Q 330.6953 507.6387 331.4668 507.6387 L 331.4668 507.6387 L 331.9258 507.6387 L 331.9258 508 L 326.3105 508 L 326.3105 507.6387 L 326.7793 507.6387 Q 327.6191 507.6387 327.9707 507.1309 L 327.9707 507.1309 Q 328.1855 506.8184 328.1855 505.7051 L 328.1855 505.7051 L 328.1855 495.5879 L 326.5938 495.5879 Q 325.666 495.5879 325.2754 495.7246 L 325.2754 495.7246 Q 324.7676 495.9102 324.4063 496.4375 Q 324.0449 496.9648 323.9766 497.8633 L 323.9766 497.8633 L 323.6055 497.8633 L 323.7617 494.7578 Z M 342.1309 502.4238 L 335.9492 502.4238 Q 335.9395 504.416 336.916 505.5488 L 336.916 505.5488 Q 337.8926 506.6816 339.2109 506.6816 L 339.2109 506.6816 Q 340.0898 506.6816 340.7393 506.1982 Q 341.3887 505.7148 341.8281 504.543 L 341.8281 504.543 L 342.1309 504.7383 Q 341.9258 506.0762 340.9395 507.1748 Q 341 508 338.4688 508.2734 L 338.4688 508.2734 Q 336.8574 508.2734 335.71 507.0186 Q 334.5625 505.7637 334.5625 503.6445 L 334.5625 503.6445 Q 334.5625 501.3496 335.7393 500.0654 Q 336.916 498.7813 338.6934 498.7813 L 338.6934 498.7813 Q 340.1973 498.7813 341.1641 499.7725 Q 342.1309 500.7637 342.1309 502.4238 L 342.1309 502.4238 Z M 335.9492 501.8574 L 335.9492 501.8574 L 340.0898 501.8574 Q 340.041 500.998 339.8848 500.6465 L 339.8848 500.6465 Q 339.6406 500.0996 339.1572 499.7871 Q 338.6738 499.4746 338.1465 499.4746 L 338.1465 499.4746 Q 337.3359 499.4746 336.6963 500.1045 Q 336.0566 500.7344 335.9492 501.8574 Z M 342.9609 499.416 L 342.9609 499.0547 L 347.1699 499.0547 L 347.1699 499.416 Q 346.7695 499.416 346.6084 499.5527 Q 346.4473 499.6895 346.4473 499.9141 L 346.4473 499.9141 Q 346.4473 500.1484 346.7891 500.6367 L 346.7891 500.6367 Q 346.8965 500.793 347.1113 501.125 L 347.1113 501.125 L 347.7461 502.1406 L 348.4785 501.125 Q 349.1816 500.1582 349.1816 499.9043 L 349.1816 499.9043 Q 349.1816 499.6992 349.0156 499.5576 Q 348.8496 499.416 348.4785 499.416 L 348.4785 499.416 L 348.4785 499.0547 L 351.5059 499.0547 L 351.5059 499.416 Q 351.0273 499.4453 350.6758 499.6797 L 350.6758 499.6797 Q 350.1973 500.0117 349.3672 501.125 L 349.3672 501.125 L 348.1465 502.7559 L 350.373 505.959 Q 351.1934 507.1406 351.5449 507.3799 Q 351.8965 507.6191 352.4531 507.6484 L 352.4531 507.6484 L 352.4531 508 L 348.2344 508 L 348.2344 507.6484 Q 348.6738 507.6484 348.918 507.4531 L 348.918 507.4531 Q 349 507 349.1035 507.0918 L 349.1035 507.0918 Q 349.1035 506.8672 348.4785 505.959 L 348.4785 505.959 L 347.1699 504.0449 L 345.7344 505.959 Q 345.0703 506.8477 345.0703 507.0137 L 345.0703 507.0137 Q 345.0703 507.248 345.29 507.4385 Q 345.5098 507.6289 345.9492 507.6484 L 345.9492 507.6484 L 345.9492 508 L 343.0293 508 L 343.0293 507.6484 Q 343.3809 507.5996 343.6445 507.4043 L 343.6445 507.4043 Q 344.0156 507.1211 344.8945 505.959 L 344.8945 505.959 L 346.7695 503.4688 L 345.0703 501.0078 Q 344.3477 499.9531 343.9522 499.6846 Q 343.5566 499.416 342.9609 499.416 L 342.9609 499.416 Z M 355.6172 496.1152 L 355.9199 496.1152 L 355.9199 499.0547 L 358.0098 499.0547 L 358.0098 499.7383 L 355.9199 499.7383 L 355.9199 505.5391 Q 355.9199 506.4082 356.1689 506.7109 Q 356.418 507.0137 356.8086 507.0137 L 356.8086 507.0137 Q 357.1309 507.0137 357.4336 506.8135 Q 357.7363 506.6133 357.9023 506.2227 L 357.9023 506.2227 L 358.2832 506.2227 Q 357.9414 507.1797 357.3164 507.6631 Q 356.6914 508.1465 356.0273 508.1465 L 356.0273 508.1465 Q 355.5781 508.1465 355.1484 507.8975 Q 354.7188 507.6484 354.5137 507.1846 Q 354.3086 506.7207 354.3086 505.7539 L 354.3086 505.7539 L 354.3086 499.7383 L 352.8926 499.7383 L 352.8926 499.416 Q 353.4297 499.2012 353.9912 498.6885 Q 354.5527 498.1758 354.9922 497.4727 L 354.9922 497.4727 Q 355.2168 497.1016 355.6172 496.1152 L 355.6172 496.1152 Z

BTW, in function getPathScaledForTransform , maybe there should usetoLowerCase ?

    if (command.toLowerCase() === 'z') {
      return;
    }

@juzhiyuan
Copy link
Contributor

juzhiyuan commented Jul 30, 2020

Once the swap is done, you can position the controls following specific values.
Tomorrow i should be able to paste some useful code.

Once the swap is done, you can position the controls following specific values.
Tomorrow i should be able to paste some useful code.

specific values?

With L and M, I created a Polygon here[1] without Bezier Curve

UPDATE: [1] is because of without Q command

[1] https://jsfiddle.net/juzhiyuan/f18ta2cp/114/
[2] https://jsfiddle.net/juzhiyuan/f18ta2cp/150/

screencapture-jsfiddle-net-juzhiyuan-f18ta2cp-115-1596096920643

@juzhiyuan
Copy link
Contributor

juzhiyuan commented Aug 1, 2020

Hi @asturur , would you mind having a look at those 2 jsfindle when you have time? Thanks a lot !:)

@asturur
Copy link
Member

asturur commented Aug 1, 2020

https://jsfiddle.net/hdu32bfj/13/

I stop looking at the sword codepen because seems too complex for me and also does not behave perfectly.
I modified your codepen, added some code, still did not write any action at all, but i m drawing all controls.

@juzhiyuan
Copy link
Contributor

Maybe I could integrate with those correct points with Control API, then action could be implemented, let me have a try :)

@juzhiyuan
Copy link
Contributor

https://jsfiddle.net/hdu32bfj/13/

This demo seems something wrong at anchorWrapper function:

var absolutePoint = fabric.util.transformPoint({
              x: (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x),
              y: (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y),
          }, fabricObject.calcTransformMatrix()),

because the fabricObject doesn't have the points property, so fabricObject.points[anchorIndex] will lead a JS error.

@juzhiyuan
Copy link
Contributor

juzhiyuan commented Aug 2, 2020

We may transform Path to Points I think?

BTW, it would be great if we have a actionHandler just like http://fabricjs.com/custom-controls-polygon does :DD

@asturur
Copy link
Member

asturur commented Aug 2, 2020

i did not write the anchorWrapper nor the action handler yet. I need some time, i m tryin to do also other fabric stuff. The weekend is the only time i have to really do things other than answering email.

@juzhiyuan
Copy link
Contributor

Got it, I will continue trying on this issue :DD

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

No branches or pull requests

8 participants