How do you recreate/get this data shown in this UI?
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:
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?Any help would be appreciated.
The properties you're searching for are stored globally and not in the glyphs' data (e.g in the os2 table).
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>
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.
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.
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="
" 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=""" 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>
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>