I'm looking into Variable Fonts. Contrary to the claims, the variable fonts I've looked at have massive file sizes. One is 40 times the size the 'static' weight of the same font.
To reduce the file size of Variable Fonts, do Adobe Fonts or Google Fonts, provide the option to subset the variable font file – so that it only includes the required axis?
For example, the font I'd like to use has the width axis and the slant axis – neither of which I need. Can these be removed from the variable font served to my site?
UPDATE Adobe Fonts provides the complete Variable Font file, with all the axis. I have not found a way to remove axis from a Variable Font file provided by Adobe Fonts. Meaning that the Variable Fonts provided by Adobe Fonts are often massive and far larger than a handful of the same static fonts. Making their Variable Fonts an unattractive option.
You can retrieve subsets by specifying the appropriate query parameters.
For instance: get Roboto Flex – only weight axis:
https://fonts.googleapis.com/css2?family=Roboto+Flex:wght@100..900
Unfortunately, it's not very convenient to get the correct URL and some browsers might get static font (e.g. Opera although it supports variable fonts flawlessly).
Some families are available both as static and variable version – using the same family name. Others are only available under a variable specific family name e.g "Roboto Flex" (VF), "Roboto" (static).
Example: Open Sans
https://fonts.googleapis.com/css2?family=Open+Sans:wght@300..800
separating the weight values by ".." periods will return the variable font @font-face
rules.
BTW: you need to know the range of supported weight. E.g 100..1000
won't work as it exceeds the supported weight range of "Open Sans".
Use the type tester tab in google font's UI to get the correct ranges.
https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800
We#re using ";" semicolon as separator – it returns the static fonts for weights 300–900;
Example: Roboto
https://fonts.googleapis.com/css2?family=Roboto+Flex:wdth,wght@25..151,100..900
works!
https://fonts.googleapis.com/css2?family=Roboto:wdth,wght@25..151,100..900
doesn't work!
/* latin */
@font-face {
font-family: 'Roboto Flex';
font-style: normal;
font-weight: 100 900;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/robotoflex/v9/NaNNepOXO_NexZs0b5QrzlOHb8wCikXpYqmZsWI-__OGfttPZktqc2VdZ80KvCLZaPcSBZtOx2MifRuWR28sPJtUMbsFEK6cRrleUx9Xgbm3WLHa_F4Ep4Fm0PN19Ik5Dntczx0wZGzhPlL1YNMYKbv9_1IQXOw7AiUJVXpRJ6cXW4O8TNGoXjC79QRyaLshNDUf3e0O-gn5rrZCu20YNau4OPE.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
body{
font-family: 'Roboto Flex';
}
.w900{
font-weight:900;
}
.w100{
font-weight:100;
}
<h1 class="w900">Head weight 900</h1>
<p class="w100">Test paragraph weight 100</p>
For some reasons the API will always keep the Parametric Thick Stroke axis (XOPQ).
let fontFamily = '';
let fontSrc =
"https://fonts.gstatic.com/s/robotoflex/v9/NaPccZLOBv5T3oB7Cb4i0zu6RME.woff2";
getVFAxes(fontSrc);
upload.addEventListener('change', (e) => {
// Encode the file using the FileReader API
const file = e.target.files[0];
getVFAxes(file);
})
inputFont.addEventListener('input', (e) => {
let current = e.currentTarget;
let fontUrl = current.value;
let datalistId = current.getAttribute('list');
let datalistOption = document.querySelector(`#${datalistId} option[value="${current.value}"]`);
fontFamily = datalistOption ? datalistOption.textContent.trim() : 'Test font';
if (fontUrl) {
getVFAxes(fontUrl);
}
})
async function getVFAxes(fontSrc) {
let font = await loadFont(fontSrc);
let fontFamily = font.tables.name.fontFamily.en;
let axes = [];
try {
axes = font.tables.fvar.axes;
} catch {
alert('Font has no axes data - is not a variable font');
console.log(font)
return false;
}
let cssArr = [];
AxesWrp.innerHTML = "";
console.log('reset', fontFamily)
console.log(axes)
let axesInfo = [];
axes.forEach((axis, a) => {
let axisName = axis.tag;
let min = axis.minValue;
let max = axis.maxValue;
let defaultValue = axis.defaultValue;
//create range sliders according to min/max axes values
let rangSliderLabel = document.createElement("label");
let rangSlider = document.createElement("input");
rangSlider.setAttribute("type", "range");
rangSlider.setAttribute("min", min);
rangSlider.setAttribute("max", max);
rangSlider.setAttribute("value", defaultValue);
rangSlider.setAttribute("step", 0.1);
rangSliderLabel.textContent = axisName + ": ";
AxesWrp.appendChild(rangSliderLabel);
rangSliderLabel.appendChild(rangSlider);
// set default style
cssArr.push(`"${axisName}" ${defaultValue}`);
axesInfo.push(
`name:"${axisName}"; min:${min}; max:${max}; default:${defaultValue};`
);
// update values by range sliders
rangSlider.addEventListener("input", function(e) {
cssArr[a] = `"${axisName}" ${e.currentTarget.value}`;
fontPreview.style.fontVariationSettings = cssArr.join(", ");
variationProp.textContent = `{font-variation-settings: ${fontPreview.style.fontVariationSettings}}`;
});
fontPreview.style.fontVariationSettings = cssArr.join(', ');
variationProp.textContent =
`{font-variation-settings: ${fontPreview.style.fontVariationSettings} }`;
// append style for preview
let fontStyle = 'normal';
let fontWeight = 400;
/*
let base64 = arrayBufferToBase64(fontSrc);
let fontFaceUrl = `data:font/${format};charset=utf-8;base64,${base64}`;
fontFaceUrl = `src: url(${fontUrl});`;
*/
fontStyleEl.textContent = `
@font-face{
font-family: "${fontFamily}";
src: url(${fontSrc});
font-style: ${fontStyle};
font-weight: ${fontWeight};
}
.fontPreview{
font-family: "${fontFamily}";
}
`;
});
}
/**
* 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';
console.log(src);
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;
}
/**
* load google font subset
* containing all glyphs used in document
*/
async function getGoogleFontSubsetFromContent(url, documentText = '') {
// get all characters used in body
documentText = documentText ? documentText : document.body.innerText.trim()
.replace(/[\n\r\t]/g, "")
.replace(/\s{2,}/g, " ")
.replaceAll(' ', "");
url = url + '&text=' + encodeURI(documentText);
let fetched = await fetch(url);
let res = await fetched.text();
let src = res.match(/[^]*?url\((.*?)\)/)[1];
return src;
}
/**
* load fonts from google helper
*/
async function getGoogleFontUrl(url, options = {}) {
let src;
let subset = options.subset ? options.subset : 'latin';
let subsetText = options.subsetText ? options.subsetText : '';
// get subset based on used characters
if (subsetText) {
src = getGoogleFontSubsetFromContent(url, subsetText);
return src;
}
let fetched = await fetch(url);
let res = await fetched.text();
// get language subsets
let subsetObj = {};
let subsetRules = res.split("/*").filter(Boolean);
for (let i = 0; i < subsetRules.length; i++) {
let subsetRule = subsetRules[i];
let rule = subsetRule.split("*/");
let subset = rule[0].trim();
let src = subsetRule.match(/[^]*?url\((.*?)\)/)[1];
subsetObj[subset] = src;
}
src = subsetObj[subset];
if (src === undefined) {
console.log(subsetRules);
src = subsetRules[0].match(/[^]*?url\((.*?)\)/)[1];
}
return src;
}
function arrayBufferToBase64(buffer) {
let binary = '';
let bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
keepDatalistOptions();
function keepDatalistOptions(selector = "") {
// select all input fields by datalist attribute or by class/id
selector = !selector ? "input[list]" : selector;
let datalistInputs = document.querySelectorAll(selector);
if (datalistInputs.length) {
for (let i = 0; i < datalistInputs.length; i++) {
let input = datalistInputs[i];
input.addEventListener("input", function(e) {
e.target.setAttribute("placeholder", e.target.value);
e.target.blur();
});
input.addEventListener("focus", function(e) {
e.target.setAttribute("placeholder", e.target.value);
e.target.value = "";
});
input.addEventListener("blur", function(e) {
e.target.value = e.target.getAttribute("placeholder");
});
}
}
}
body {
font-family: sans-serif;
}
label {
font-family: monospace;
margin-right: 0.5em;
display: inline-block;
}
.fontPreview {
font-size: 10vmin;
line-height: 1em;
transition: 0.3s;
border: 1px solid #ccc;
outline: none;
}
<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>
<style id="fontStyleEl"></style>
<p>
<label>Font file URL: <input class="inputChange" id="inputFont" type="text" value="https://fonts.googleapis.com/css2?family=Roboto+Flex:wdth,wght@25..151,100..900" list="fontList"></label>
<datalist id="fontList">
<option value="https://fonts.gstatic.com/s/robotoflex/v9/NaPccZLOBv5T3oB7Cb4i0zu6RME.woff2">Roboto Flex - all Axes
</option>
<option value="https://fonts.gstatic.com/s/robotoflex/v9/NaNeepOXO_NexZs0b5QrzlOHb8wCikXpYqmZsWI-__OGfttPZktqc2VdZ80KvCLZaPcSBZtOx2MifRuWR28sPJtUMbsFEK6cRrleUx9Xgbm3WLHa_F4Ep4Fm0PN19Ik5Dntczx0wZGzhPlL1YNMYKbv9_1IQXOw7AiUJVXpRJ6cXW4O8TNGoXjC79QRyaLshNDUf9-EmFw.woff2" title="https://fonts.googleapis.com/css2?family=Roboto+Flex:wdth,wght@25..151,100..900">Roboto Flex - width and weight</option>
<option value="https://fonts.gstatic.com/s/robotoflex/v9/NaNNepOXO_NexZs0b5QrzlOHb8wCikXpYqmZsWI-__OGfttPZktqc2VdZ80KvCLZaPcSBZtOx2MifRuWR28sPJtUMbsFEK6cRrleUx9Xgbm3WLHa_F4Ep4Fm0PN19Ik5Dntczx0wZGzhPlL1YNMYKbv9_1IQXOw7AiUJVXpRJ6cXW4O8TNGoXjC79QRyaLshNDUf3e0O-gn5rrZCu20YNau4OPE.woff2">Roboto Flex - only weight</option>
<option value="https://fonts.gstatic.com/s/opensans/v34/mem8YaGs126MiZpBA-UFVZ0b.woff2">Open Sans
</option>
<option value="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap&text=abc123
">Open Sans static</option>
<label>Load font: <input type="file" id="upload"></label>
</p>
<div id="AxesWrp"></div>
<div class="preview">
<p><span id="fontNameCaption"></span>
<pre id="variationProp"></pre>
</p>
<div class="fontPreview" id="fontPreview" contenteditable>FontPreview: Type something</div>
</div>
https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,slnt,wdth,wght,GRAD,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC@8..144,-10..0,25..151,100..1000,-200..150,323..603,25..135,649..854,-305..-98,560..788,416..570,528..760
https://fonts.googleapis.com/css2?family=Roboto+Flex:wdth,wght@25..151,100..900
https://fonts.googleapis.com/css2?family=Roboto+Flex:wght@100..900
https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600;700
It really depends on the total number of weights and styles.