Reputation: 938
I'm at a loss here. Trying to add a google variable webfont (open sans) to my website.
<link>
for static CSS font-faces. WHY? (semicolons, no '300..700')Use on the web
To embed a font, copy the code into the of your html
[x] <link> [ ] @import <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
CSS rules to specify families
font-family: 'Open Sans', sans-serif;
On the entire page there is nowhere a download for the woff2 (which only comes from the API). The D/L button ONLY serves the .ttf. Ironically in the article about self hosting, they use woff2
as example, even if they don't provide it. Also even the official GITHUB page for the font only serves .ttf. WHY?
Ohter sources provide static files in various formats (but I didn't see variable ones there) and ppl in another thread even posted their own tools like:
After a full day, I finally found this. There another (official) tool is mentioned, for converting ttf to woff2, which seems not easily accomplishable for variable fonts. SRSLY? Is this the only way ?? And why is there no documentation whatsoever ?? (Ok Maybe I should grab the woff2 from the API, but I noticed differences across browsers, I think for example Opera gets serves only the static type not the variable one.)
The 'good' API serves this. But it only uses format('woff2')
:
But I've read, for variable fonts the syntax should be more like this, using format('woff2 supports variations')
and format('woff2-variations')
and @supports (font-variation-settings: normal)
. WHY Google doesn't use that syntax? Which is better now?
Google:
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300 800;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v34/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
How it supposedly should be done:
@font-face {
font-family: Asap;
src: url('/fonts/Asap-VariableFont_wght.woff2') format('woff2 supports variations'),
url('/fonts/Asap-VariableFont_wght.woff2') format('woff2-variations');
font-weight: 400 700;
font-display: swap;
font-style: normal;
}
Side note: From the google page I needed to manually change
https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..0,800;1,300..1,800&display=swap
to
https://fonts.googleapis.com/css2?family=Open+Sans:[email protected]&display=swap
to even get the variable font.
Upvotes: 8
Views: 979
Reputation: 17316
Unfortunately you still can't download woff2 files directly.
Format identifiers like format('woff2-variations')
date back to the days when variable font support was rather experimental – so they shouldn't be needed by any decently "modern" browser. However, there might be exceptions like very old browser versions – see caniuse report.
Since google-webfont-helper still doesn't support variable fonts I came up with a custom font loader.
async function getAllVariableFonts(
apiKey = "",
format = "woff2",
apiUrlStatic = ""
) {
let apiURL = `https://www.googleapis.com/webfonts/v1/webfonts?capability=VF&capability=${format}&sort=style&key=${apiKey}`;
// switch between API and static src
let listUrl = apiKey ? apiURL : apiUrlStatic;
// fetch font JSON
let listObj = await (await fetch(listUrl)).json();
// get only VF items
let items = listObj.items;
items = items.filter((item) => item.axes && item.axes.length);
return items;
}
async function getGoogleFontUrl(font) {
// replace whitespace
let familyQuery = font.family.replaceAll(" ", "+");
let gfontBase = `https://fonts.googleapis.com/css2?family=`;
// check variants
let variants = [...new Set(font.variants.filter(Boolean))];
let stylesItalic = variants.filter((variant) => variant.includes("italic"));
let stylesRegular = variants.filter((variant) => !variant.includes("italic"));
// sort axes alphabetically - case sensitive ([a-z],[A-Z])
let axes = font.axes;
axes = [
axes.filter((item) => item.tag.toLowerCase() === item.tag),
axes.filter((item) => item.tag.toUpperCase() === item.tag)
].flat();
let ranges = axes.map((val) => {
return val.start + ".." + val.end;
});
let tuples = [];
// italic and regular
if (stylesItalic.length && stylesRegular.length) {
tuples.push("ital");
rangeArr = [];
for (let i = 0; i < 2; i++) {
rangeArr.push(`${i},${ranges.join(",")}`);
}
}
// only italic
else if (stylesItalic.length && !stylesRegular.length) {
tuples.push("ital");
rangeArr = [];
rangeArr.push(`${1},${ranges.join(",")}`);
}
// only regular
else {
rangeArr = [];
rangeArr.push(`${ranges.join(",")}`);
}
// add axes tags to tuples
axes.map((val) => {
return tuples.push(val.tag);
});
query = tuples.join(",") + "@" + rangeArr.join(";") + "&display=swap";
let url = `${gfontBase}${familyQuery}:${query}`;
return url;
}
function updatePreview(item, googleFontUrl) {
legend.textContent = `Preview: ${item.family}`;
// add css
let im = `@impo` + `rt`;
gfCss.textContent = `
${im} '${googleFontUrl}';
.preview{
font-family: "${item.family}";
font-size: 12vmin;
}`;
let axes = item.axes;
styleSelect.innerHTML = "";
let fontVariationSettings = {};
let hasItalic = item.variants.includes("italic");
if (hasItalic) {
let checkbox = document.createElement("input");
let checkboxLabel = document.createElement("label");
checkboxLabel.textContent = "Italic ";
checkbox.type = "checkbox";
checkboxLabel.append(checkbox);
styleSelect.append(checkboxLabel);
checkbox.addEventListener("click", (e) => {
preview.style.fontStyle = checkbox.checked ? "italic" : "normal";
});
}
axes.forEach((axis) => {
let label = document.createElement("label");
let input = document.createElement("input");
input.type = "range";
input.min = axis.start;
input.max = axis.end;
input.value = axis.start;
fontVariationSettings[axis.tag] = axis.start;
label.textContent = `${axis.tag}: ${axis.start}–${axis.end} `;
styleSelect.append(label, input);
// apply style
input.addEventListener("input", (e) => {
let val = e.currentTarget.value;
fontVariationSettings[axis.tag] = val;
let cssVar = [];
for (tag in fontVariationSettings) {
cssVar.push(`"${tag}" ${fontVariationSettings[tag]}`);
}
preview.style.fontVariationSettings = cssVar.join(", ");
});
});
}
function showLink(target, url) {
target.innerHTML = "";
let link = `<a href="${url}">${url}</a>`;
target.insertAdjacentHTML("beforeend", link);
}
function populateDatalist(target, list) {
let fonts = list;
let datalistOptions = "";
fonts.forEach((font) => {
//only VF
if (font.axes) {
datalistOptions += `<option >${font.family}</option>`;
}
});
target.insertAdjacentHTML("beforeend", datalistOptions);
}
/**
* fetch
*/
async function fetchFontsFromCssAndZip(css) {
// find subset identifiers by comments
let regexComments = /\/\*\s*([^*]*(?:\*(?!\/)[^*]*)*)\*\//g;
let subsets = css.match(regexComments).map((sub) => {
return sub.replace(/(\/\*|\*\/)/g, "").trim();
});
//create and parse temporary stylesheet object
let cssSheet = new CSSStyleSheet();
cssSheet.replaceSync(css);
// filter font-face rules
let rules = [...cssSheet.cssRules].filter((rule) => {
return rule.type === 5;
});
// sanitize font-family name
let fontFamily = rules[0].style
.getPropertyValue("font-family")
.replaceAll('"', "");
let fontFamilyFilename = fontFamily.replaceAll(" ", "-");
// create zip object
let zip = new JSZip();
// loop through all rules/fonts
for (let i = 0; i < rules.length; i++) {
// get properties
let fontWeight = rules[i].style.getPropertyValue("font-weight");
let fontStyle = rules[i].style.getPropertyValue("font-style");
let fontStretch = rules[i].style.getPropertyValue("font-stretch");
fontStretch = fontStretch === "normal" ? "" : fontStretch;
let src = rules[i].style.getPropertyValue("src");
src = src.match(/\(([^)]+)\)/)[1].replaceAll('"', "");
//replace cryptic file names with readable local names
let fontName = [fontFamilyFilename, subsets[i], fontWeight, fontStyle, fontStretch]
.filter(Boolean)
.join("_") + ".woff2";
css = css.replaceAll(src, `"${fontFamilyFilename}/${fontName}"`);
// add data to zip
let fontData = await (await fetch(src)).arrayBuffer();
zip.file(`${fontFamilyFilename}/${fontName}`, fontData, {
type: "uint8array"
});
}
// add simple example HTML
let htmlDoc = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
<link rel="stylesheet" href="${fontFamilyFilename}.css">
<body style="font-family:'${fontFamily}\'">
<h1>Sample font</h1>
<p>One morning, when <em>Gregor Samsa</em> woke from <strong>troubled dreams</strong>, he found himself transformed in his bed into a horrible vermin.</p>
<p>He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover it and seemed ready to slide off any moment.</p>
</body></html>`;
zip.file("index.html", htmlDoc);
// add css
fontCss.value = css;
zip.file(`${fontFamilyFilename}.css`, css);
// create object url
let blob = await zip.generateAsync({
type: "blob"
});
blob.name = fontFamilyFilename + ".zip";
return blob;
}
:root {
--loadingImg: url("data:image/svg+xml,<svg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'><path d='M12 1A11 11 0 1 0 23 12 11 11 0 0 0 12 1Zm0 19a8 8 0 1 1 8-8A8 8 0 0 1 12 20Z' opacity='.25'/><path d='M10.14 1.16a11 11 0 0 0-9 8.92A1.59 1.59 0 0 0 2.46 12 1.52 1.52 0 0 0 4.11 10.7a8 8 0 0 1 6.66-6.61A1.42 1.42 0 0 0 12 2.69h0A1.57 1.57 0 0 0 10.14 1.16Z'><animateTransform attributeName='transform' type='rotate' dur='0.75s' values='0 12 12;360 12 12' repeatCount='indefinite'/></path></svg>")
}
body {
font-family: sans-serif
}
legend {
font-weight: bold;
}
fieldset {
margin-bottom: 1em;
}
fieldset input,
fieldset textarea {
border: none
}
input[type="text"] {
width: 100%;
display: block;
margin-bottom: 1em;
}
#inputFonts,
.inputUrl,
input[type="search"] {
font-size: 2em;
margin-bottom: 1em;
border: 1px solid #000;
border-radius: 0.3em;
}
input[type="checkbox"] {
width: auto;
display: inline-block;
margin-bottom: 1em;
}
textarea {
width: 100%;
min-height: 20em;
}
.btn-default {
text-decoration: none;
border: 1px solid #000;
background: #ccc;
color: #000;
font-weight: bold;
padding: 0.3em;
font-family: inherit;
font-size: 1em;
margin-right: 0.3em;
cursor: pointer;
}
.inactive {
pointer-events: none;
opacity: 0;
}
.btn-load .icn-loading {
opacity: 0;
}
.btn-load.loading .icn-loading {
opacity: 1;
}
/*
.btn-load.active
.icn-loading{
width:0px;
}
*/
.icn-loading {
transition: 0.3s;
transform: translateY(0.15em);
display: inline-block;
position: relative;
overflow: hidden;
width: 1em;
height: 1em;
background-image: var(--loadingImg);
background-repeat: no-repeat;
background-position: 0%;
color: transparent;
border-color: transparent;
}
<h1>Fetch variable fonts from google</h1>
<style id="gfCss"></style>
<p><button class="btn-default btn-fetch" id="fetchData">Fetch font files</button><a class="btn-default btn-load inactive" id="btnDownload" href="#" download="fontface.css">Download fontkit <span class="icn-loading"></span></a> </p>
<fieldset>
<legend>Search font by name</legend>
<input type="text" list="datalistFonts" id="inputFonts" placeholder="Enter font-family name">
<!-- autocomplete -->
<datalist id="datalistFonts">
</datalist>
<label for="">Or enter CSS URL</label>
<p><input type="text" class="inputUrl" id="inputUrl" value="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900"></p>
</fieldset>
<fieldset>
<legend id="legend">Preview</legend>
<div id="preview" class="preview">
Hamburglefonstiv
</div>
<div id="styleSelect"></div>
</fieldset>
<fieldset>
<legend>New Css</legend>
<textarea id="fontCss"></textarea>
</fieldset>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.js"></script>
<script>
window.addEventListener('DOMContentLoaded', async(e) => {
// enter you own API key
let apiKey = '';
// static copy of developer API response
let apiUrlStatic = "https://cdn.jsdelivr.net/gh/herrstrietzel/fonthelpers@main/json/gfontsAPI.json";
// inputs
const inputUrl = document.getElementById('inputUrl');
const btnDownload = document.getElementById('btnDownload');
// init example
(async() => {
/**
* get all google fonts from API
* filter only variable fonts
*/
let googleFontList = await getAllVariableFonts(apiKey, 'woff2', apiUrlStatic);
// generate autofill
populateDatalist(datalistFonts, googleFontList)
// show first
let item = googleFontList.filter(item => item.family === 'Open Sans')[0];
inputFonts.value = item.family;
//console.log(item);
googleFontUrl = await getGoogleFontUrl(item);
inputUrl.value = googleFontUrl;
//update css
updatePreview(item, googleFontUrl)
// filter fonts
inputFonts.addEventListener('change', async e => {
let family = e.currentTarget.value;
let familyQuery = family.replaceAll(' ', '+');
// filter current family
let item = googleFontList.filter(item => item.family === family)[0];
// update links
googleFontUrl = await getGoogleFontUrl(item);
inputUrl.value = googleFontUrl;
//showLink(cssUrls, [googleFontUrl])
updatePreview(item, googleFontUrl)
});
//updateGoogleCssUrl();
})();
inputUrl.addEventListener("change", async(e) => {
updateGoogleCssUrl()
});
fetchData.addEventListener("click", async(e) => {
btnDownload.classList.remove('inactive');
btnDownload.classList.add('active');
btnDownload.classList.add('loading');
updateGoogleCssUrl()
});
// fetch
async function updateGoogleCssUrl() {
// fetch css content as text
let url = inputUrl.value;
let css = await (await fetch(url)).text();
// fetch font files and zip
let blob = await fetchFontsFromCssAndZip(css);
let objectUrl = URL.createObjectURL(blob);
// update download link
btnDownload.href = objectUrl;
btnDownload.download = blob.name;
btnDownload.classList.replace('inactive', 'active');
btnDownload.classList.remove('loading');
}
})
</script>
The download functionality doesn't work in SO snippets. See working codepen example
https://fonts.googleapis.com/css2?family=Open+Sans:ital,wdth,wght@0,75..100,300..800;1,75..100,300..800&display=swap
@font-face
rulesarrayBuffer()
and add data to JSZIP objectsUnfortunately, google deploys uses agent detection and may fail to deliver Variable Fonts to some browser like Opera (despite supporting variable fonts)
Upvotes: 3