Search code examples
javascriptfonts

Approach to getting font glyph data to display in the UI?


How do you recreate/get this data shown in this UI?

enter image description here

Mainly, the ascender, cap-height, x-height, etc.. The stuff on the right you can derive from the unicode glyph or get from the unicode data, but the left stuff appears to be font-specific glyph data.

Questions are:

  1. What font format should I use to get this glyph data (ttf, otf, woff, woff2, or svg)?
  2. I know of the opentype JS API, https://github.com/opentypejs/opentype.js . Specifically the Glyph object seems to be what I want, but the only seemingly relevant props are advanceWidth, leftSideBearing, xMin, xMax, yMin, and yMax, but that doesn't seem the same as that image.... Does it have everything from that image?
  3. Are there any better JS libraries for working with a particular font format to get this data?
  4. If no JS libraries exist, what low-level font properties from a particular font format do I need to look into reading from? For example, at one point in time I parsed through the cmap and related font tables, so maybe it's in there somewhere?

Any help would be appreciated.


Solution

  • The properties you're searching for are stored globally and not in the glyphs' data (e.g in the os2 table).

    Parse and render via opentype.js:

    let fontSrc =
      "https://fonts.gstatic.com/s/robotoflex/v9/NaPccZLOBv5T3oB7Cb4i0zu6RME.woff2";
    getVerticalMetrics(fontSrc);
    
    async function getVerticalMetrics(fontSrc) {
      let font = await loadFont(fontSrc);
      let fontFamily = font.tables.name.fontFamily.en;
      
      // collect data records
      let {unitsPerEm, ascender, descender} = font
    
      //get x-height from os2 table
      let xHeight = font.tables.os2.sxHeight
      let capHeight = font.tables.os2.sCapHeight
      
      //optional:  get x height from glyph data
      let xHeight_x = font.charToGlyph('x').yMax
      let capHeight_H = font.charToGlyph('H').yMax
      
      let data = {
        fontFamily: fontFamily,
        xHeight: xHeight,
        capHeight: capHeight,
        ascender: ascender,
        descender: descender,
        unitsPerEm: unitsPerEm
      }
      
      pre.textContent= JSON.stringify(data, null, ' ');
      
      
      // render example
      let fontSize = 100;
      let scale = fontSize / unitsPerEm;
      let lineHeight = (ascender + Math.abs(descender) ) * scale;
      let ratAsc = ascender / unitsPerEm;
      let yOffset = fontSize * ratAsc;
      let path = font.getPath('Hxg', 0, yOffset, fontSize)
      
      let pathData = path.toPathData(1)
      preview.setAttribute('d', pathData)
      let yBaseline = ascender*scale
      let yXHeight = yBaseline-xHeight*scale
      let ypathCapHeight = yBaseline - capHeight*scale
      pathBaseline.setAttribute('y1',yBaseline );
      pathBaseline.setAttribute('y2',yBaseline );
      
      pathXheight.setAttribute('y1',yXHeight );
      pathXheight.setAttribute('y2',yXHeight );
      
      pathCapHeight.setAttribute('y1',ypathCapHeight );
      pathCapHeight.setAttribute('y2',ypathCapHeight );
      
      svg.setAttribute('viewBox', [0, 0, 200, lineHeight])
    
    
    }
    
    
    /**
    * opentype.js helper
    * Based on @yne's comment
    * https://github.com/opentypejs/opentype.js/issues/183#issuecomment-1147228025
    * will decompress woff2 files
    */
    async function loadFont(src, options = {}) {
      let buffer = {};
      let font = {};
      let ext = 'woff2';
      let url;
    
      // 1. is file
      if (src instanceof Object) {
        // get file extension to skip woff2 decompression
        let filename = src.name.split(".");
        ext = filename[filename.length - 1];
        buffer = await src.arrayBuffer();
      }
    
      // 2. is base64 data URI
      else if (/^data/.test(src)) {
        // is base64
        let data = src.split(";");
        ext = data[0].split("/")[1];
    
        // create buffer from blob
        let srcBlob = await (await fetch(src)).blob();
        buffer = await srcBlob.arrayBuffer();
      }
    
      // 3. is url
      else {
    
    
        // if google font css - retrieve font src
        if (/googleapis.com/.test(src)) {
          ext = 'woff2';
          src = await getGoogleFontUrl(src, options);
        }
    
    
        // might be subset - no extension
        let hasExt = (src.includes('.woff2') || src.includes('.woff') || src.includes('.ttf') || src.includes('.otf')) ? true : false;
        url = src.split(".");
        ext = hasExt ? url[url.length - 1] : 'woff2';
    
        let fetchedSrc = await fetch(src);
        buffer = await fetchedSrc.arrayBuffer();
      }
    
      // decompress woff2
      if (ext === "woff2") {
        buffer = Uint8Array.from(Module.decompress(buffer)).buffer;
      }
    
      // parse font
      font = opentype.parse(buffer);
      return font;
    }
    svg{
      border: 1px solid #ccc;
    }
    <!-- neeeded for woff2 fonts/brotli decompression -->
    <script src="https://unpkg.com/wawoff2@2.0.1/build/decompress_binding.js"></script>
    <script src='https://cdn.jsdelivr.net/npm/opentype.js@latest/dist/opentype.min.js'></script>
    
    <div id="sample">
      <svg id="svg" viewBox="0 0 200 100">
        <path id="preview" />
        <line x1="0" y1="0" x2="100%"  y2="" id="pathBaseline" stroke="red" fill="none" />
        <line x1="0" y1="0" x2="100%"  y2="" id="pathXheight" stroke="green" fill="none" />
        <line x1="0" y1="0" x2="100%"  y2="" id="pathCapHeight" stroke="#ccc" fill="none" />
        
      </svg>
    </div>
    
    <code>
      <pre id="pre"></pre>
    </code>

    Coordinates in font files

    Fonts use the classsic Cartesian coordinate space (y-axis goes from bottom to top) - whereas svg has a vertically flipped y-axis (top-to-bottom).
    Therefore you need to convert values for svg or canvas rendering.

    Units in font files

    Keep in mind all units are relative value to the font's UPM (units per em values).

    So you may need to calculate a relative value to the desired font size for rendering. E.g an .otf opentype would return values based on 1000 UPM whereas a truetype .ttf version returns values (usually) based on a 2048 UPM ratio.

    Ideal font format

    The font format only matters for decompression before parsing:

    woff2 requires a complex brotli decompression script which increases the total loading and processing time significantly.

    When loading woff or truetype you can use the already included deflate functions.

    .svg fonts are deprecated. However if you also have a svg version of your current font you may also load it via fetch() and write your own svg font parser

    let font = new DOMParser().parseFromString(svgFont, 'text/html').querySelector('font')
    let fontface = font.querySelector('font-face')
    let data = [...fontface.attributes].map( item=>{let obj={}; obj[item.name]=item.value; return obj})
    console.log(data)
    <script>
    
    let svgFont = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
    <metadata>
    Created by FontForge 20201107 at Mon Jun 22 12:22:04 2020
     By Jimmy Wärting
    Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
    </metadata>
    <defs>
    <font id="Poppins-Medium" horiz-adv-x="678" >
      <font-face 
        font-family="Poppins Medium"
        font-weight="500"
        font-stretch="normal"
        units-per-em="1000"
        panose-1="0 0 6 0 0 0 0 0 0 0"
        ascent="800"
        descent="-200"
        x-height="551"
        cap-height="695"
        bbox="-28 -272 1048 969"
        underline-thickness="50"
        underline-position="-100"
        unicode-range="U+000D-2215"
      />
    <missing-glyph horiz-adv-x="500" 
    d="M0 700h500v-700h-500v700zM420 650h-340l170 -255zM50 95l170 255l-170 255v-510zM450 95v510l-170 -255zM420 50l-170 255l-170 -255h340z" />
        <glyph glyph-name=".notdef" horiz-adv-x="500" 
    d="M0 700h500v-700h-500v700zM420 650h-340l170 -255zM50 95l170 255l-170 255v-510zM450 95v510l-170 -255zM420 50l-170 255l-170 -255h340z" />
        <glyph glyph-name="NULL" horiz-adv-x="0" 
     />
        <glyph glyph-name="NULL" horiz-adv-x="0" 
     />
        <glyph glyph-name="CR" unicode="&#xd;" horiz-adv-x="260" 
     />
        <glyph glyph-name="space" unicode=" " horiz-adv-x="260" 
     />
        <glyph glyph-name="exclam" unicode="!" horiz-adv-x="321" 
    d="M218 695l-13 -485h-95l-13 485h121zM109 14q-21 21 -21 52t21 52t52 21q30 0 51 -21t21 -52t-21 -52t-51 -21q-31 0 -52 21z" />
        <glyph glyph-name="quotedbl" unicode="&#x22;" horiz-adv-x="323" 
    d="M137 797l-12 -205h-78l-13 205h103zM288 797l-12 -205h-78l-13 205h103z" />
        <glyph glyph-name="numbersign" unicode="#" horiz-adv-x="872" 
    d="M658 458l-37 -173h132v-100h-153l-40 -185h-109l40 185h-196l-40 -185h-109l40 185h-155v100h176l37 173h-154v100h175l39 182h109l-39 -182h196l39 182h109l-39 -182h133v-100h-154zM549 458h-196l-37 -173h196z" />
      </font>
    </defs></svg>`
    </script>

    Font info without rendering features via lib-font.js

    lib-font.js can be an alternative if you don't need rendering features e.g for converting glyph outlines to svg.
    lib-font.js usually provides the most complete data table info – especially variable font data (like axes info) is often incompletely implemented by other parsers.

    // retrieve font data after all required assets are loaded (e.g for decompression)
    let fontSrc =
      "https://fonts.gstatic.com/s/robotoflex/v9/NaPccZLOBv5T3oB7Cb4i0zu6RME.woff2";
    
    window.addEventListener("DOMContentLoaded", (e) => {
      getVerticalMetrics(fontSrc);
    });
    
    async function getVerticalMetrics(fontSrc) {
      let font = new Font("fontname", {
        skipStyleSheet: true
      });
      font.src = fontSrc;
      font.onload = (evt) => {
        let font = evt.detail.font;
        let tables = font.opentype.tables;
        let os2 = tables["OS/2"];
        let ascender = os2.sTypoAscender;
        let descender = os2.sTypoDescender;
        let xHeight = os2.sxHeight;
        let capHeight = os2.sCapHeight;
        //console.log(font);
        //console.log(os2);
    
        let data = {
          xHeight: xHeight,
          capHeight: capHeight,
          ascender: ascender,
          descender: descender,
          unitsPerEm: tables.head.unitsPerEm
        };
    
        pre.textContent = JSON.stringify(data, null, " ");
      };
      
    }
    <!-- add brotli decompression needed for woff2 -->
    <script src="https://cdn.jsdelivr.net/npm/lib-font@2.4.0/lib/unbrotli.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lib-font@2.4.0/lib-font.browser.js" type="module"></script>
    
    <code>
      <pre id="pre"></pre>
    </code>