javascriptsvgvector-graphicstruetype

How to get the centerline of a character using Javascript?


I want to extract the centerline of a character, through client-side JavaScript, from .ttf (TrueType Fonts). TrueType fonts store characters as 2 paths, an inner and outer paths. However, I need the centerline of the character (refer image, I am trying to achieve something similar to this.)

Reference Image

How would I go about implementing this?

I am already able to extract the outer and inner path from fonts using opentype.js


Solution

  • I'm afraid you're out of luck.
    The main problem is we can't really interpolate a center-line from outer path-geometry.

    skeleton-tracing

    An alternative approach is often referred to as "skeleton-tracing" which analyzes a bitmap to find inner skeleton structures.

    Unfortunately this approach is not well suited for letter-shapes as it introduces rather jittery "bones".

    Besides, the algorithm won't auto-detect a reasonable drawing order as expected from the usual writing conventions.

    Here's an example I wrote when experimenting with this approach. I'm using LingDong's skeleton-tracing JS script.

    let svg = document.querySelector('svg')
    
    // add scaling to improve accuracy
    let scale = 2
    svg2Skeleton(svg, scale);
    
    /**
     * svg to canvas
     */
    async function svg2Skeleton(el, scale = 1, filter = "") {
      /**
       *  clone svg to add width and height
       * for better compatibility
       * without affecting the original svg
       */
      const svgEl = el.cloneNode(true);
    
      // get dimensions
      let {
        width,
        height
      } = el.getBBox();
      let w = el.viewBox.baseVal.width ?
        svgEl.viewBox.baseVal.width :
        el.width.baseVal.value ?
        el.width.baseVal.value :
        width;
      let h = el.viewBox.baseVal.height ?
        svgEl.viewBox.baseVal.height :
        el.height.baseVal.value ?
        el.height.baseVal.value :
        height;
    
      // apply scaling
      [w, h] = [w * scale, h * scale];
    
      // add width and height for firefox compatibility
      svgEl.setAttribute("width", w);
      svgEl.setAttribute("height", h);
    
      // create canvas
      let canvas = document.createElement("canvas");
      canvas.width = w;
      canvas.height = h;
    
      // create blob
      let svgString = new XMLSerializer().serializeToString(svgEl);
      let blob = new Blob([svgString], {
        type: "image/svg+xml"
      });
      let objectURL = URL.createObjectURL(blob);
    
      let tmpImg = new Image();
      tmpImg.src = objectURL;
      tmpImg.width = w;
      tmpImg.height = h;
      tmpImg.crossOrigin = "anonymous";
    
      await tmpImg.decode();
      let ctx = canvas.getContext("2d");
      ctx.fillStyle = "#000";
      ctx.fillRect(0, 0, w, h);
    
      // apply filter to enhance contrast
      filter = 'brightness(0) grayscale(1) invert(1)';
      if (filter) {
        ctx.filter = filter;
      }
      ctx.drawImage(tmpImg, 0, 0, w, h);
    
    
      // get skeleton
      let {
        polylines
      } = await TraceSkeleton.fromCanvas(canvas)
    
      let polyArr = polylines.map(poly => poly.flat());
    
    
      // render paths
      let paths = '';
      polyArr.forEach(poly => {
        let path = `<path d="M ${poly.map(val => val / scale).join(' ')}" />`;
        paths += path;
      })
      svg.insertAdjacentHTML('beforeend', paths);
    
    }
    <script src="https://cdn.jsdelivr.net/npm/skeleton-tracing-js/dist/trace_skeleton.min.js"></script>
    
    
    <svg  viewBox="0 0 500 500">
            <style>
                path {
                    fill: none;
                    stroke: red;
                    stroke-width: 1px !important
                }
    
                path:nth-of-type(even) {
                    fill: none;
                    stroke: green;
                    stroke-width: 1px !important;
                }
    
    
            </style>
    
            <text x="10" y="200" class="text" font-family="arial" font-size="160" stroke="#fff" stroke-width="0">AE</text>
    
        </svg>

    We're rendering the SVG to a canvas and apply the tracing to the bitmap data.

    As you can see the result is rather underwhelming. Admittedly you could apply some sort of polygon simplification to get smoother skeleton lines.

    All in all you will get a way better result drawing the center-cines manually in a graphic editor.

    See also K. Palágyi's article