Search code examples
node.jssvgpaperjs

Paper.js reorient: SVG subpaths are lost


For generating an icon font, I have a workflow that involves processing SVGs so that they are suitable for using the Node.js package @tancredi/fantasticon.

In general, all packages I found so far, have trouble dealing with SVGs that contain paths which are drawn specifically with fill-rule: evenodd. Hence, I would like to counter that, by converting those evenodd paths into nonzero paths.

Enter Paper.js, and specifically its reorient function. According to its documentation:

Fixes the orientation of the sub-paths of a compound-path, assuming that non of its sub-paths intersect, by reorienting them so that they are of different winding direction than their containing paths, except for disjoint sub-paths, i.e. islands, which are oriented so that they have the same winding direction as the the biggest path.

Parameters:

  • nonZero: Boolean
    • controls if the non-zero fill-rule is to be applied, by counting the winding of each nested path and discarding sub-paths that do not contribute to the final result
    • optional, default: false
  • clockwise: Boolean
    • if provided, the orientation of the root paths will be set to the orientation specified by clockwise, otherwise the orientation of the largest root child is used.
    • optional

Returns: PathItem — a reference to the item itself, reoriented

Based on Fantasticon and Paper.js, I set out to build a Node.js script that would process SVGs and reorient them. I just started using Paper.js, and I am still trying to navigate its documentation. I think I am doing something wrong, but the reorient method removes the inner subpaths, leaving me with the outer-most path. Question is: what is going on, and how can I make sure that reorient correctly converts my SVG from evenodd to nonzero?

I've searched for issues online but could not find any related issues. I tried to reduce my code to the point of only reorienting the intermediate SVG with Paper.js, I can say that it still produces the same issue. So I know where the issue is located, but I can neither explain it, nor resolve it.

This is the SVG: it's a path with several circular vectors placed inside one another.

Visual representation Outlines indicating path start and path direction
The SVG fed into Paper.js; a path with several circular vectors placed inside one another Outlined path, with arrows indicating subpath direction
<svg xmlns="http://www.w3.org/2000/svg" width="2048" height="2048" viewBox="0 0 2048 2048" version="1.1">
        <path d="M 1065 219.667 C 928.697 225.578, 801.334 266.997, 689.735 341.706 C 524.794 452.125, 411.967 624.641, 377.580 819 C 361.326 910.866, 363.060 1008.833, 382.547 1099.717 C 425.408 1299.614, 552.256 1473.626, 730.162 1576.581 C 860.821 1652.194, 1015.175 1685.465, 1165.500 1670.418 C 1307.062 1656.249, 1439.242 1602.217, 1549.459 1513.468 C 1595.696 1476.236, 1640.162 1430.254, 1675.679 1382.943 C 1751.032 1282.569, 1798.840 1165.254, 1815.028 1041 C 1819.491 1006.746, 1820.409 990.922, 1820.455 947.500 C 1820.499 906.193, 1819.935 894.433, 1816.425 863.500 C 1799.401 713.460, 1735.998 572.921, 1634.389 460 C 1620.962 445.078, 1589.621 413.929, 1575.065 401.038 C 1462.087 300.988, 1318.201 237.992, 1168.500 223.039 C 1133.893 219.582, 1095.676 218.337, 1065 219.667 M 1056.500 336.594 C 1054.300 336.802, 1046.875 337.454, 1040 338.042 C 921.267 348.206, 807.023 393.869, 713.471 468.555 C 539.513 607.434, 455.343 825.867, 491.035 1045.804 C 515.339 1195.565, 595.531 1331.434, 715.500 1426.113 C 812.884 1502.968, 927.829 1547.254, 1052.869 1556.092 C 1074.559 1557.625, 1134.337 1556.718, 1154.500 1554.551 C 1256.184 1543.619, 1346.321 1511.559, 1430 1456.559 C 1499.036 1411.184, 1558.316 1351.792, 1604.293 1281.940 C 1661.702 1194.718, 1694.368 1098.753, 1703.158 991.500 C 1704.731 972.314, 1704.723 920.949, 1703.146 901.500 C 1696.405 818.409, 1676.210 745.349, 1639.966 672.935 C 1582.102 557.325, 1488.514 462.628, 1373.674 403.489 C 1304.671 367.955, 1235.140 347.355, 1154 338.408 C 1140.379 336.906, 1067.530 335.551, 1056.500 336.594 M 1063 481.614 C 1045.975 483.081, 1026.390 485.473, 1014 487.599 C 876.166 511.249, 755.207 597.264, 686.483 720.500 C 656.159 774.876, 637.578 833.933, 630.323 899 C 627.854 921.148, 627.858 971.370, 630.332 994 C 640.747 1089.276, 676.463 1173.297, 737.876 1247 C 750.503 1262.154, 778.390 1289.977, 793.909 1302.904 C 870.840 1366.985, 960.987 1403.601, 1060.246 1411.084 C 1080.332 1412.598, 1127.719 1411.744, 1145.500 1409.547 C 1181.245 1405.132, 1207.016 1399.526, 1238.500 1389.320 C 1383.446 1342.330, 1498.581 1223.546, 1540.952 1077.286 C 1565.629 992.103, 1565.639 901.596, 1540.982 815.874 C 1509.997 708.154, 1437.338 612.202, 1340.952 551.717 C 1281.365 514.325, 1217.517 492.075, 1144.978 483.423 C 1132.197 481.899, 1074.483 480.625, 1063 481.614 M 1070.500 641.029 C 1017.975 645.313, 967.954 662.547, 925.001 691.160 C 906.278 703.632, 896.557 711.504, 880.172 727.465 C 829.070 777.242, 797.980 840.332, 789.483 911.500 C 787.442 928.590, 787.418 964.307, 789.436 981.115 C 796.705 1041.670, 819.662 1095.392, 858.381 1142.450 C 876.560 1164.544, 907.109 1191.110, 932.487 1206.891 C 1017.298 1259.632, 1123.623 1267.351, 1215.812 1227.462 C 1307.180 1187.927, 1374.526 1104.617, 1393.990 1007.046 C 1398.538 984.249, 1399.500 973.677, 1399.500 946.500 C 1399.500 920.132, 1398.558 909.256, 1394.424 887.897 C 1370.927 766.486, 1274.205 669.448, 1153.331 646.016 C 1128.542 641.211, 1093.868 639.123, 1070.500 641.029 M 1081.500 813.629 C 1028.010 819.433, 984.228 854.263, 967.492 904.329 C 962.654 918.800, 960.884 930.093, 960.884 946.500 C 960.884 962.903, 962.654 974.200, 967.488 988.660 C 982.604 1033.877, 1019.988 1067.036, 1067.266 1077.160 C 1080.508 1079.996, 1105.092 1080.260, 1118 1077.706 C 1144.390 1072.483, 1166.676 1060.901, 1185.975 1042.377 C 1238.948 991.532, 1240.841 908.071, 1190.227 854.884 C 1170.637 834.298, 1147.025 821.269, 1119.557 815.887 C 1109.816 813.978, 1089.427 812.769, 1081.500 813.629" stroke="none" fill="black" fill-rule="evenodd"/>
</svg>

This is what my code regarding reorienting the SVG looks like:

var size = new paper.Size(2048, 2048);
paper.setup(size);
var paperItem = paper.project.importSVG(
   `<svg xmlns="http://www.w3.org/2000/svg" width="2048" height="2048" viewBox="0 0 2048 2048" version="1.1">
       <path d="
                M 1065 219.667 C 928.697 225.578, 801.334 266.997, 689.735 341.706 C 524.794 452.125, 411.967 624.641, 377.580 819 C 361.326 910.866, 363.060 1008.833, 382.547 1099.717 C 425.408 1299.614, 552.256 1473.626, 730.162 1576.581 C 860.821 1652.194, 1015.175 1685.465, 1165.500 1670.418 C 1307.062 1656.249, 1439.242 1602.217, 1549.459 1513.468 C 1595.696 1476.236, 1640.162 1430.254, 1675.679 1382.943 C 1751.032 1282.569, 1798.840 1165.254, 1815.028 1041 C 1819.491 1006.746, 1820.409 990.922, 1820.455 947.500 C 1820.499 906.193, 1819.935 894.433, 1816.425 863.500 C 1799.401 713.460, 1735.998 572.921, 1634.389 460 C 1620.962 445.078, 1589.621 413.929, 1575.065 401.038 C 1462.087 300.988, 1318.201 237.992, 1168.500 223.039 C 1133.893 219.582, 1095.676 218.337, 1065 219.667
                M 1056.500 336.594 C 1054.300 336.802, 1046.875 337.454, 1040 338.042 C 921.267 348.206, 807.023 393.869, 713.471 468.555 C 539.513 607.434, 455.343 825.867, 491.035 1045.804 C 515.339 1195.565, 595.531 1331.434, 715.500 1426.113 C 812.884 1502.968, 927.829 1547.254, 1052.869 1556.092 C 1074.559 1557.625, 1134.337 1556.718, 1154.500 1554.551 C 1256.184 1543.619, 1346.321 1511.559, 1430 1456.559 C 1499.036 1411.184, 1558.316 1351.792, 1604.293 1281.940 C 1661.702 1194.718, 1694.368 1098.753, 1703.158 991.500 C 1704.731 972.314, 1704.723 920.949, 1703.146 901.500 C 1696.405 818.409, 1676.210 745.349, 1639.966 672.935 C 1582.102 557.325, 1488.514 462.628, 1373.674 403.489 C 1304.671 367.955, 1235.140 347.355, 1154 338.408 C 1140.379 336.906, 1067.530 335.551, 1056.500 336.594
                M 1063 481.614 C 1045.975 483.081, 1026.390 485.473, 1014 487.599 C 876.166 511.249, 755.207 597.264, 686.483 720.500 C 656.159 774.876, 637.578 833.933, 630.323 899 C 627.854 921.148, 627.858 971.370, 630.332 994 C 640.747 1089.276, 676.463 1173.297, 737.876 1247 C 750.503 1262.154, 778.390 1289.977, 793.909 1302.904 C 870.840 1366.985, 960.987 1403.601, 1060.246 1411.084 C 1080.332 1412.598, 1127.719 1411.744, 1145.500 1409.547 C 1181.245 1405.132, 1207.016 1399.526, 1238.500 1389.320 C 1383.446 1342.330, 1498.581 1223.546, 1540.952 1077.286 C 1565.629 992.103, 1565.639 901.596, 1540.982 815.874 C 1509.997 708.154, 1437.338 612.202, 1340.952 551.717 C 1281.365 514.325, 1217.517 492.075, 1144.978 483.423 C 1132.197 481.899, 1074.483 480.625, 1063 481.614
                M 1070.500 641.029 C 1017.975 645.313, 967.954 662.547, 925.001 691.160 C 906.278 703.632, 896.557 711.504, 880.172 727.465 C 829.070 777.242, 797.980 840.332, 789.483 911.500 C 787.442 928.590, 787.418 964.307, 789.436 981.115 C 796.705 1041.670, 819.662 1095.392, 858.381 1142.450 C 876.560 1164.544, 907.109 1191.110, 932.487 1206.891 C 1017.298 1259.632, 1123.623 1267.351, 1215.812 1227.462 C 1307.180 1187.927, 1374.526 1104.617, 1393.990 1007.046 C 1398.538 984.249, 1399.500 973.677, 1399.500 946.500 C 1399.500 920.132, 1398.558 909.256, 1394.424 887.897 C 1370.927 766.486, 1274.205 669.448, 1153.331 646.016 C 1128.542 641.211, 1093.868 639.123, 1070.500 641.029
                M 1081.500 813.629 C 1028.010 819.433, 984.228 854.263, 967.492 904.329 C 962.654 918.800, 960.884 930.093, 960.884 946.500 C 960.884 962.903, 962.654 974.200, 967.488 988.660 C 982.604 1033.877, 1019.988 1067.036, 1067.266 1077.160 C 1080.508 1079.996, 1105.092 1080.260, 1118 1077.706 C 1144.390 1072.483, 1166.676 1060.901, 1185.975 1042.377 C 1238.948 991.532, 1240.841 908.071, 1190.227 854.884 C 1170.637 834.298, 1147.025 821.269, 1119.557 815.887 C 1109.816 813.978, 1089.427 812.769, 1081.500 813.629" stroke="none" fill="black" fill-rule="evenodd"/>
    </svg>`,
(pathItem) => {
   let compoundPaths = pathItem.getItems({
      className: 'CompoundPath'
   });
   compoundPaths = compoundPaths.filter((c) => !c.clipMask);
   for (const path of compoundPaths) {
      path.reorient(true);
   }
   var reorientedSVG = paper.project.exportSVG({asString:true});
   console.log("reorientedSVG",reorientedSVG);
});
paper.project.clear();
paper.view.draw();
// reorientedSVG contains the reoriented SVG with its subpaths removed

This is the SVG that has been reoriented, but has at the same time its subpaths removed after I called reorient. What's more, the path is still set to "fill-rule: evenodd". This makes me suspect that I am actually doing something wrong.

The resulting SVG image, as exported from Paper.js after reorienting compound paths

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 2048 2048">
   <defs></defs>
   <clipPath id="a"></clipPath>
   <path fill="none" d="M0 0h2048v2048H0z"></path>
   <g fill="none" fill-rule="none" stroke-miterlimit="10" clip-path="url(#a)" font-family="none" font-size="none" font-weight="none" style="mix-blend-mode:normal" text-anchor="none"></g>
   <path fill="#000" fill-rule="evenodd" d="M1065 219.667c-136.303 5.911-263.666 47.33-375.265 122.039C524.794 452.125 411.967 624.641 377.58 819c-16.254 91.866-14.52 189.833 4.967 280.717 42.861 199.897 169.709 373.909 347.615 476.864 130.659 75.613 285.013 108.884 435.338 93.837 141.562-14.169 273.742-68.201 383.959-156.95 46.237-37.232 90.703-83.214 126.22-130.525 75.353-100.374 123.161-217.689 139.349-341.943 4.463-34.254 5.381-50.078 5.427-93.5.044-41.307-.52-53.067-4.03-84-17.024-150.04-80.427-290.579-182.036-403.5-13.427-14.922-44.768-46.071-59.324-58.962-112.978-100.05-256.864-163.046-406.565-177.999-34.607-3.457-72.824-4.702-103.5-3.372"></path>
</svg>

Does anyone know how to use Paper.js' reorient? Or am I using Paper.js wrong in its entirety? Anyway, I could use some guidance with trying to change an SVG path from evenodd to nonzero. I appreciate any help, pointers or comments.


Solution

  • For some reasons it works if you set the reorient argument to 'false':

    path.reorient(false);
    

    However, I recommend

    • to parse your svg separately via new DOMParser()
    • create a new paper.js compoundPath object
      let paperPath = new paper.CompoundPath(d);
    • apply the reorient() method and replace original path:
    paperPath.reorient(false);
    let pathFixed = paperPath.exportSVG({precision:0});
    path.setAttribute('d', pathFixed.getAttribute('d'));
    

    Instead of exporting the new paper.js svg output, which contains unnecessary elements (like <g> or <clipPath> elements) we just replace the original d attribute.

    var size = new paper.Size(2048, 2048);
    paper.setup(size);
    
    // parse svg from markup
    let svg = new DOMParser().parseFromString(`
      <svg xmlns="http://www.w3.org/2000/svg" width="2048" height="2048" viewBox="0 0 2048 2048">
        <path fill-rule="evenodd" d="M 1065 219.667 C 928.697 225.578, 801.334 266.997, 689.735 341.706 C 524.794 452.125, 411.967 624.641, 377.580 819 C 361.326 910.866, 363.060 1008.833, 382.547 1099.717 C 425.408 1299.614, 552.256 1473.626, 730.162 1576.581 C 860.821 1652.194, 1015.175 1685.465, 1165.500 1670.418 C 1307.062 1656.249, 1439.242 1602.217, 1549.459 1513.468 C 1595.696 1476.236, 1640.162 1430.254, 1675.679 1382.943 C 1751.032 1282.569, 1798.840 1165.254, 1815.028 1041 C 1819.491 1006.746, 1820.409 990.922, 1820.455 947.500 C 1820.499 906.193, 1819.935 894.433, 1816.425 863.500 C 1799.401 713.460, 1735.998 572.921, 1634.389 460 C 1620.962 445.078, 1589.621 413.929, 1575.065 401.038 C 1462.087 300.988, 1318.201 237.992, 1168.500 223.039 C 1133.893 219.582, 1095.676 218.337, 1065 219.667 M 1056.500 336.594 C 1054.300 336.802, 1046.875 337.454, 1040 338.042 C 921.267 348.206, 807.023 393.869, 713.471 468.555 C 539.513 607.434, 455.343 825.867, 491.035 1045.804 C 515.339 1195.565, 595.531 1331.434, 715.500 1426.113 C 812.884 1502.968, 927.829 1547.254, 1052.869 1556.092 C 1074.559 1557.625, 1134.337 1556.718, 1154.500 1554.551 C 1256.184 1543.619, 1346.321 1511.559, 1430 1456.559 C 1499.036 1411.184, 1558.316 1351.792, 1604.293 1281.940 C 1661.702 1194.718, 1694.368 1098.753, 1703.158 991.500 C 1704.731 972.314, 1704.723 920.949, 1703.146 901.500 C 1696.405 818.409, 1676.210 745.349, 1639.966 672.935 C 1582.102 557.325, 1488.514 462.628, 1373.674 403.489 C 1304.671 367.955, 1235.140 347.355, 1154 338.408 C 1140.379 336.906, 1067.530 335.551, 1056.500 336.594 M 1063 481.614 C 1045.975 483.081, 1026.390 485.473, 1014 487.599 C 876.166 511.249, 755.207 597.264, 686.483 720.500 C 656.159 774.876, 637.578 833.933, 630.323 899 C 627.854 921.148, 627.858 971.370, 630.332 994 C 640.747 1089.276, 676.463 1173.297, 737.876 1247 C 750.503 1262.154, 778.390 1289.977, 793.909 1302.904 C 870.840 1366.985, 960.987 1403.601, 1060.246 1411.084 C 1080.332 1412.598, 1127.719 1411.744, 1145.500 1409.547 C 1181.245 1405.132, 1207.016 1399.526, 1238.500 1389.320 C 1383.446 1342.330, 1498.581 1223.546, 1540.952 1077.286 C 1565.629 992.103, 1565.639 901.596, 1540.982 815.874 C 1509.997 708.154, 1437.338 612.202, 1340.952 551.717 C 1281.365 514.325, 1217.517 492.075, 1144.978 483.423 C 1132.197 481.899, 1074.483 480.625, 1063 481.614 M 1070.500 641.029 C 1017.975 645.313, 967.954 662.547, 925.001 691.160 C 906.278 703.632, 896.557 711.504, 880.172 727.465 C 829.070 777.242, 797.980 840.332, 789.483 911.500 C 787.442 928.590, 787.418 964.307, 789.436 981.115 C 796.705 1041.670, 819.662 1095.392, 858.381 1142.450 C 876.560 1164.544, 907.109 1191.110, 932.487 1206.891 C 1017.298 1259.632, 1123.623 1267.351, 1215.812 1227.462 C 1307.180 1187.927, 1374.526 1104.617, 1393.990 1007.046 C 1398.538 984.249, 1399.500 973.677, 1399.500 946.500 C 1399.500 920.132, 1398.558 909.256, 1394.424 887.897 C 1370.927 766.486, 1274.205 669.448, 1153.331 646.016 C 1128.542 641.211, 1093.868 639.123, 1070.500 641.029 M 1081.500 813.629 C 1028.010 819.433, 984.228 854.263, 967.492 904.329 C 962.654 918.800, 960.884 930.093, 960.884 946.500 C 960.884 962.903, 962.654 974.200, 967.488 988.660 C 982.604 1033.877, 1019.988 1067.036, 1067.266 1077.160 C 1080.508 1079.996, 1105.092 1080.260, 1118 1077.706 C 1144.390 1072.483, 1166.676 1060.901, 1185.975 1042.377 C 1238.948 991.532, 1240.841 908.071, 1190.227 854.884 C 1170.637 834.298, 1147.025 821.269, 1119.557 815.887 C 1109.816 813.978, 1089.427 812.769, 1081.500 813.629" />
      </svg>
    `, "image/svg+xml").querySelector('svg');
    let path = svg.querySelector('path');
    path.removeAttribute('fill-rule');
    let d = path.getAttribute('d');
    
    // create paper.js compoundPath object
    let paperPath = new paper.CompoundPath(d);
    
    // fix path directions
    paperPath.reorient(false);
    let pathFixed = paperPath.exportSVG({
      precision: 0
    });
    
    // apply to original path
    path.setAttribute('d', pathFixed.getAttribute('d'));
    processed.appendChild(svg);
    
    // get complete svg
    let svgMarkup = new XMLSerializer().serializeToString(svg);
    console.log(svgMarkup)
    svg {
      max-width: 10em;
      height: auto;
      border: 1px solid #ccc;
    }
    
    path {
      fill: #ccc;
      stroke: #000;
      stroke-width: 1px;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.17/paper-full.min.js"></script>
    <div id="processed"></div>