0

I have created svg elements using the p5.js-svg librarie. Now i want to convert the svgs into paths so i can create glyphs with it. and the push the glyphs into a new fontfile. In the README file i found the Path.fromSVG() method whitch i think would do exactly what i try to do. Im using the following script tag to load opentype.js:

Current Behavior

However when im running my code i get the error that “Path.fromSVG is not a function”. I have logged the Path object to the console and look at the provided prototype functions. and fromSVG is not in this list.

Possible Solution

In the opentype.js repository under /src i have found the path.js file in whitch i have found a fromSVG() function. So probably it dont work because i have linked to cdn instead of downloading opentype.js and so the path.js file isnt available in the browser.

Another possible reason could be that the fromSVG() function is still work in progress and not yet published in the opetnype.min.js 1.3.4,

Im Happy for every Help/Suggestions;)

Expected Behavior

I was expecting that the function gives me a Path whitch i can use to create a new Glyph. And then push the Glyphs in to a new Font File.

1 Answers1

0

The current version of Opentype.js doesn't include this method.

You could write a similar helper function to convert svg pathdata to opentype.js native command object.

You need to draw/align all your svg glyphs in a consistent layout similar to an EM Square (actually rather a rectangle) used in font design applications to define metrics like left and right sidebearings, descender etc.

SVG em square

The above illustration shows different letters all drawn in a viewBox with a height of 100 units and variable widths specifying the desired sidebearings of the glyph.

All glyphs are vertically aligned to the baseline (green line) at y=80.

Now we can use this design pattern to convert from svg to opentype commands. Fonts use a Cartesian coordinate system:

Red dot: x/y = 0 in svg
Green dot: x/y = 0 in font (baseline position)

Flipping pathdata

So we need to flip svg y-coordinates and shift the glyph according to the glyphs bbox height.

I'm using getPathData() polyfill to retrieve the path commands.
The following example converts all commands to absolute commands also converting quadratic and arc commands to cubic via normalize parameter:

// parse path and normalize to absolute
let pathData = el.getPathData({
    normalize: true
}); 

If you need to keep e.g quadratic commands you could also parse the path without normalizing el.getPathData().

In this case your path data must not contain relative or shorthand commands. However you could also convert these as described here: "Convert SVG Path to Relative Commands"

// define descender, baseline respectively
let descender = -200;


/**
 * define new glyphs
 */
let glyphs = [];

// .notdef glyph is recommended to be included
let notDefPath = new opentype.Path();
// svg path is aready scaled
notDefPath.commands = svgToOpentypeCommands('M100 0 L100 700 L600 700 L600 0 M200 100 L500 100 L500 600 L200 600')

let notdefGlyph = new opentype.Glyph({
  name: '.notdef',
  unicode: 0,
  advanceWidth: 700,
  path: notDefPath
});
glyphs.push(notdefGlyph)


//space
let space = new opentype.Glyph({
  name: 'space',
  unicode: 32,
  advanceWidth: 500,
  path: ''
});
glyphs.push(space)


let glyphPaths = document.querySelectorAll('path[data-glyph]');
glyphPaths.forEach(el => {
  let svg = el.closest('svg');
  let svgvB = svg.getAttribute('viewBox');
  let svgH = svgvB ? svgvB.split(' ')[3] : 1000;
  let scale = svgH != 1000 ? 1000 / svgH : 1;
  //let descenderSvg = Math.abs(descender) / scale;
  let svgW = svgvB ? svgvB.split(' ')[2] : 1000;
  let svgGlyph = svgToGlyphData(el, svgW, svgH, scale);
  glyphs.push(svgGlyph);
})


/**
 * create new font
 * containing previously aded glyphs 
 */
let font = new opentype.Font({
  familyName: 'New Font',
  styleName: 'Normal',
  unitsPerEm: 1000,
  ascender: 800,
  descender: descender,
  glyphs: glyphs
});

//console.log(font);
let buffer = font.toArrayBuffer();
font = opentype.parse(buffer);


function svgToGlyphData(el, svgW, svgH, scale) {
  let id = el.dataset.glyph ? el.dataset.glyph : el.id;
  let yOff = svgH * scale + descender;
  let charBB = el.getBBox();

  // create new glyph 
  let newPath = new opentype.Path();
  // convert svg commands to cartesian and scale
  newPath.commands = svgToOpentypeCommands(el, scale, yOff);

  console.log(newPath.commands);
  let thisGlyph = new opentype.Glyph({
    name: id,
    unicode: id.charCodeAt(0),
    advanceWidth: svgW * scale,
    leftSideBearing: charBB.x * scale,
    path: newPath
  });
  return thisGlyph;
};



function svgToOpentypeCommands(el, scale = 1, yOff = 0) {
  /**
   * is d pathdata
   * create temporary path
   */
  if (el instanceof SVGGeometryElement === false) {
    let d = el;
    let ns = 'http://www.w3.org/2000/svg';
    let svgTmp = document.createElementNS(ns, 'svg');
    el = document.createElementNS(ns, 'path');
    el.setAttribute('d', d)

    svgTmp.append(el)
    document.body.append(svgTmp);
    let bb = el.getBBox();
    yOff = bb.height;
    svgTmp.remove();
  }

  // parse path and normalize to absolute
  let pathData = el.getPathData({
    normalize: true
  });


  let commands = [];
  pathData.forEach(function(el, p) {
    let commandType = el.type;

    //scale coordinates if viewBox < 1000 units
    let coords = scale != 1 ? el.values.map(val => {
      return val * scale
    }) : el.values;

    let thisCommand = {
      'type': commandType
    };
    // convert svg commands
    if (commandType == 'M' || commandType == 'L') {
      thisCommand['x'] = coords[0];
      thisCommand['y'] = yOff - coords[1];

    } else if (commandType == 'C') {
      thisCommand['x1'] = coords[0];
      thisCommand['y1'] = yOff - coords[1];
      thisCommand['x2'] = coords[2];
      thisCommand['y2'] = yOff - coords[3];
      thisCommand['x'] = coords[4];
      thisCommand['y'] = yOff - coords[5];
    } else if (commandType == 'Q') {
      thisCommand['x1'] = coords[0];
      thisCommand['y1'] = yOff - coords[1];
      thisCommand['x'] = coords[2];
      thisCommand['y'] = yOff - coords[3];
    }

    commands.push(thisCommand);
  });
  //console.log(commands);
  return commands;
}




/**
 * optional
 * render preview on canvas 
 */
renderFontPreview(font, fontPreview)

function renderFontPreview(font, target) {
  target.innerHTML = '<h2>' + font.names.fontFamily.en + '</h2>';
  for (let i = 0; i < font.glyphs.length; i++) {
    let glyph = font.glyphs.get(i);
    let ctx = createGlyphCanvas(target, glyph, 150);
    let x = 50;
    let y = 100;
    let fontSize = 72;
    glyph.draw(ctx, x, y, fontSize);
    glyph.drawPoints(ctx, x, y, fontSize);
    glyph.drawMetrics(ctx, x, y, fontSize);
  }
}

function createGlyphCanvas(target, glyph, size) {
  let canvasId, html, glyphsDiv, wrap, canvas, ctx;
  canvasId = 'c' + glyph.index;
  html = '<div class="wrapper" style="width:' + size + 'px"><canvas id="' + canvasId + '" width="' + size +
    '" height="' + size + '"></canvas><span>' + glyph.name + '</span></div>';

  target.insertAdjacentHTML('beforeend', html)
  canvas = document.getElementById(canvasId);
  ctx = canvas.getContext('2d');
  return ctx;
}
body {
  font: 13px Helvetica, arial, freesans, sans-serif;
  line-height: 1.4;
  color: #333;
  margin: 2em padding: 0;
}

svg {
  display: inline-block;
  height: 40vh;
  width: auto;
  overflow: visible;
  border: 1px solid #ccc
}

.svgWrp {
  margin-bottom: 2em;
}

.wrapper {
  display: inline-block;
  margin: 5px;
  border: 1px solid #ccc;
}
<script src="https://cdn.jsdelivr.net/npm/opentype.js@1.3.4/dist/opentype.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@latest/path-data-polyfill.min.js"></script>


<div class="svgWrp">
  <h2>SVG glyphs</h2>
  <svg class="svgGlyph" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54.3 100">
                <path data-glyph="g"
                    d="M52.3 26.5 v5.1l-9.9 1.2c0.9 1.1 1.7 2.6 2.4 4.5s1.1 3.9 1.1 6.2c0 5.2-1.8 9.4-5.4 12.5s-8.5 4.7-14.7 4.7c-1.6 0-3.1-0.1-4.5-0.4c-3.5 1.8-5.2 4.1-5.2 6.9c0 1.5 0.6 2.6 1.8 3.3s3.3 1 6.2 1h9.5c5.8 0 10.2 1.2 13.3 3.7s4.7 6 4.7 10.6c0 5.9-2.4 10.5-7.1 13.6s-11.7 4.6-20.8 4.6c-7 0-12.4-1.3-16.2-3.9s-5.7-6.3-5.7-11c0-3.3 1-6.1 3.1-8.5s5-3.9 8.8-4.8c-1.4-0.6-2.5-1.6-3.4-2.9s-1.4-2.8-1.4-4.5c0-2 0.5-3.7 1.5-5.1s2.7-2.9 5-4.3c-2.8-1.1-5.1-3.1-6.8-5.8s-2.6-5.9-2.6-9.4c0-5.8 1.8-10.4 5.3-13.5s8.5-4.8 14.9-4.8c2.8 0 5.3 0.3 7.6 1h18.5zm-42.7 62.5c0 2.9 1.2 5.1 3.7 6.6s5.9 2.2 10.5 2.2c6.8 0 11.8-1 15.1-3s4.9-4.8 4.9-8.3c0-2.9-0.9-4.9-2.7-6s-5.2-1.7-10.1-1.7h-9.7c-3.7 0-6.6 0.9-8.6 2.6s-3.1 4.3-3.1 7.6zm4.4-45.4c0 3.8 1.1 6.6 3.2 8.5s5 2.9 8.8 2.9c7.9 0 11.9-3.8 11.9-11.5c0-8.1-4-12.1-12-12.1c-3.8 0-6.8 1-8.8 3.1s-3.1 5.1-3.1 9.1z" />

                <line id="baseline" fill="none" stroke="green" stroke-dasharray="0 3" stroke-linecap="round" x1="0"
                    y1="80" x2="100%" y2="80" />
                <circle cx="0" cy="0" r="2" fill="red" />
                <circle cx="0" cy="80" r="2" fill="green" />
            </svg>

  <svg viewBox="0 0 53 100">
                <path data-glyph="x"
                    d="M21.5 52.6l-18.6-26.1h9.2l14.1 20.5l14.1-20.5h9.1l-18.6 26.1l19.6 27.4h-9.2l-15-21.7l-15.1 21.7h-9.2l19.6-27.4z" />
                <line id="baseline" fill="none" stroke="green" stroke-dasharray="0 3" stroke-linecap="round" x1="0"
                    y1="80" x2="100%" y2="80" />

            </svg>


  <svg viewBox="0 0 25 100">
                <path data-glyph="1"
                    d="M7.9 12c0-1.9 0.5-3.2 1.4-4.1s2-1.3 3.4-1.3s2.4 0.4 3.4 1.3s1.4 2.3 1.4 4.1s-0.5 3.2-1.4 4.1s-2.1 1.3-3.4 1.3s-2.5-0.4-3.4-1.3s-1.4-2.3-1.4-4.1zm8.8 68h-8.1v-53.5h8.1v53.5z" />
                <line id="baseline" fill="none" stroke="green" stroke-dasharray="0 3" stroke-linecap="round" x1="0"
                    y1="80" x2="100%" y2="80" />
            </svg>


</div>


<div id="fontPreview"></div>
<button onclick="font.download()">Download Font</button>

See also example for writing fonts in opentype.js repository and documentation

Obviously you need to adjust the scaling and descender values according to your layout.

herrstrietzel
  • 11,541
  • 2
  • 12
  • 34