2

I am building a UI as part of a product to make it easy to choose, select and style Google fonts. I am challenged by variable fonts because I cannot find a good way to get info about these. The developer API provides metadata for all Google Fonts via a large JSON string. However, it does not seem to contain any data that would allow a developer to discern which fonts are variable. They all “appear” to be standard fonts.

Is there a fast way to get such data? By fast, I am talking about something similar to Google Font’s developer API, but with data for the various variable fonts that would include:

  • Which fonts are variable?
  • Which axes do the variable fonts ship with?

Currently, the only recommended approach I’ve seen for exploring variable fonts and their axes is to load the fonts into a web page and use Firefox’s Font editor in the developer tools to manually get the data. But with the current 112 variable fonts in Google Fonts, it could take days to collect this info. So my question is: Is there a faster way to get meta data for the variable fonts in Google Fonts?

Stephen Miller
  • 503
  • 3
  • 11

2 Answers2

1

I am working on a font picker plugin and I ran into a similar problem, so I started investigating the google fonts main distribution site until I found what I was looking for. Google's fonts site executes a call to the following API endpoint.

https://fonts.google.com/metadata/fonts

Which returns the following text file.

)]}'{"axisRegistry": [
{
  "tag": "FILL",
  "displayName": "Fill",
  "min": 0.0,
  "defaultValue": 0.0,
  "max": 1.0,
  "precision": -2,
  "description": "The Fill axis is intended to provide a treatment of the design that fills in transparent forms with opaque ones (and sometimes interior opaque forms become transparent, to maintain contrasting shapes). Transitions often occur from the center, a side, or a corner, but can be in any direction. This can be useful in animation or interaction to convey a state transition. The numbers indicate proportion filled, from 0 (no treatment) to 1 (completely filled).",
  "fallbacks": [
    {
      "name": "Normal",
      "value": 0.0
    },
    {
      "name": "Filled",
      "value": 1.0
    }
  ]
} ...],"familyMetadataList": [{
  "family": "Alegreya",
  "displayName": null,
  "category": "Serif",
  "size": 425570,
  "subsets": [
    "menu",
    "cyrillic",
    "cyrillic-ext",
    "greek",
    "greek-ext",
    "latin",
    "latin-ext",
    "vietnamese"
  ],
  "fonts": {
    "400": {
      "thickness": 4,
      "slant": 1,
      "width": 6,
      "lineHeight": 1.361
    },
    "400i": {
      "thickness": 4,
      "slant": 4,
      "width": 6,
      "lineHeight": 1.361
    },
    "500": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "500i": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "600": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "600i": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "700": {
      "thickness": 7,
      "slant": 1,
      "width": 7,
      "lineHeight": 1.361
    },
    "700i": {
      "thickness": 6,
      "slant": 4,
      "width": 6,
      "lineHeight": 1.361
    },
    "800": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "800i": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "900": {
      "thickness": 8,
      "slant": 1,
      "width": 7,
      "lineHeight": 1.361
    },
    "900i": {
      "thickness": 8,
      "slant": 4,
      "width": 6,
      "lineHeight": 1.361
    }
  },
  "axes": [
    {
      "tag": "wght",
      "min": 400.0,
      "max": 900.0,
      "defaultValue": 400.0
    }
  ],
  "unsupportedAxes": [],
  "designers": [
    "Juan Pablo del Peral",
    "Huerta Tipográfica"
  ],
  "lastModified": "2021-02-11",
  "dateAdded": "2011-12-19",
  "popularity": 159,
  "trending": 828,
  "defaultSort": 164,
  "androidFragment": null,
  "isNoto": false
}...],...}

Please note that while the above looks like a JSON file, it will be treated as a text file, so you will have to remove this part )]}' from the top of the text file, so you can then parse it as a JSON file. The only top-level property that matters (as far as your use case is concerned) is the "familyMetadataList" property, as the name implies it includes all the fonts metadata, which includes the axes any given font has. You will have to loop on the "familyMetadataList" prop and see if the font's axes member has an array that is not empty, from there we can deduce that it is a variable font. You can do something as simple as this to figure out which font is variable.

const variableFonts=[];
const googleFontJSON = {
 "familyMetadataList": [
 {
  "family": "Alegreya",
  "displayName": null,
  "category": "Serif",
  "size": 425570,
  "subsets": [
    "menu",
    "cyrillic",
    "cyrillic-ext",
    "greek",
    "greek-ext",
    "latin",
    "latin-ext",
    "vietnamese"
  ],
  "fonts": {
    "400": {
      "thickness": 4,
      "slant": 1,
      "width": 6,
      "lineHeight": 1.361
    },
    "400i": {
      "thickness": 4,
      "slant": 4,
      "width": 6,
      "lineHeight": 1.361
    },
    "500": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "500i": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "600": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "600i": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "700": {
      "thickness": 7,
      "slant": 1,
      "width": 7,
      "lineHeight": 1.361
    },
    "700i": {
      "thickness": 6,
      "slant": 4,
      "width": 6,
      "lineHeight": 1.361
    },
    "800": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "800i": {
      "thickness": null,
      "slant": null,
      "width": null,
      "lineHeight": 1.361
    },
    "900": {
      "thickness": 8,
      "slant": 1,
      "width": 7,
      "lineHeight": 1.361
    },
    "900i": {
      "thickness": 8,
      "slant": 4,
      "width": 6,
      "lineHeight": 1.361
    }
  },
  "axes": [
    {
      "tag": "wght",
      "min": 400.0,
      "max": 900.0,
      "defaultValue": 400.0
    }
  ],
  "unsupportedAxes": [],
  "designers": [
    "Juan Pablo del Peral",
    "Huerta Tipográfica"
  ],
  "lastModified": "2021-02-11",
  "dateAdded": "2011-12-19",
  "popularity": 159,
  "trending": 828,
  "defaultSort": 164,
  "androidFragment": null,
  "isNoto": false
},
    {
      "family": "Alegreya SC",
      "displayName": null,
      "category": "Serif",
      "size": 381295,
      "subsets": [
        "menu",
        "cyrillic",
        "cyrillic-ext",
        "greek",
        "greek-ext",
        "latin",
        "latin-ext",
        "vietnamese"
      ],
      "fonts": {
        "400": {
          "thickness": 4,
          "slant": 1,
          "width": 7,
          "lineHeight": 1.361
        },
        "400i": {
          "thickness": 4,
          "slant": 4,
          "width": 7,
          "lineHeight": 1.361
        },
        "500": {
          "thickness": null,
          "slant": null,
          "width": null,
          "lineHeight": 1.361
        },
        "500i": {
          "thickness": null,
          "slant": null,
          "width": null,
          "lineHeight": 1.361
        },
        "700": {
          "thickness": 6,
          "slant": 1,
          "width": 7,
          "lineHeight": 1.361
        },
        "700i": {
          "thickness": 6,
          "slant": 3,
          "width": 7,
          "lineHeight": 1.361
        },
        "800": {
          "thickness": null,
          "slant": null,
          "width": null,
          "lineHeight": 1.361
        },
        "800i": {
          "thickness": null,
          "slant": null,
          "width": null,
          "lineHeight": 1.361
        },
        "900": {
          "thickness": 8,
          "slant": 1,
          "width": 7,
          "lineHeight": 1.361
        },
        "900i": {
          "thickness": 8,
          "slant": 3,
          "width": 7,
          "lineHeight": 1.361
        }
      },
      "axes": [],
      "unsupportedAxes": [],
      "designers": [
        "Juan Pablo del Peral",
        "Huerta Tipográfica"
      ],
      "lastModified": "2021-03-24",
      "dateAdded": "2011-12-19",
      "popularity": 436,
      "trending": 249,
      "defaultSort": 443,
      "androidFragment": null,
      "isNoto": false
    }
]}; // The array of font meta data
googleFontJSON.familyMetadataList.forEach(font => {     
  if (font.axes.length) {
    font.isVariable=true;
  } else {
    font.isVariable=false;
  }
});
console.log(googleFontJSON);

How you analyze the data is of course entirely your own prerogative. Good luck with your project, Mr.Steven. You can also acquire more information about any given variable font's axes step through the axis registry prop found JSON file found at https://fonts.google.com/metadata/fonts. Simply examine the precision prop. For example, axes with a 0.1 step like "opsz" and "wdth" have their precision set to -1, axes with a 0.01 step like "CASL" and "MONO" have their precision set to -2.

  "axisRegistry": [
    {
      "tag": "opsz",
      "displayName": "Optical size",
      "min": 6.0,
      "defaultValue": 14.0,
      "max": 144.0,
      "precision": -1, //<=== Here
      "description": "Adapt the ...",
      "fallbacks": [
        {
          "name": "6pt",
          "value": 6.0
        },
        {
          "name": "7pt",
          "value": 7.0
        }...
      ]
    },...
  • Thanks a lot for this one - great find! Do you have any idea to get the steps for the axes as they appear on [https://fonts.google.com/variablefonts](https://fonts.google.com/variablefonts)? – Stephen Miller Sep 28 '21 at 20:39
1

I love the answer from Stranger1586. But I really also need data on the steps for each axis in order to properly build UI elements such as sliders. So I decided to write a scraper to scrape the data from https://fonts.google.com/variablefonts. That page contains updated data on all variable fonts and all supported axes according to Google Font's GitHub page.

The scraper creates a JSON file with axes data for each font family. I hope it might be helpful to others having the same need. Here is the code:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.firefox.options import Options

from bs4 import BeautifulSoup
import json

def get_variable_fonts_data():
    print('Opening: Google Variable Fonts page...')
    options = Options()
    options.headless = True
    gecko_path = r'D:\Anaconda3\envs\py37\Lib\site-packages\helium\_impl\webdrivers\windows\geckodriver.exe'
    url = 'https://fonts.google.com/variablefonts'
    browser = webdriver.Firefox(options=options, executable_path=gecko_path)
    browser.get(url)
    timeout = 10  # seconds

    # Wait for the table element as it is not part of the page source but is generated with JavaScript
    try:
        WebDriverWait(browser, timeout).until(EC.presence_of_element_located((By.TAG_NAME, 'table')))
        print('Generating font table')
    except TimeoutException:
        print("Loading took too much time!")

    soup = BeautifulSoup(browser.page_source, 'html.parser')
    table = soup.find('table')
    table_head = table.find('thead').find('tr')
    header_values = []
    for cell in table_head.find_all('th'):
        header_values.append(cell.encode_contents().decode("utf-8").strip())
    table_body = table.find('tbody')
    variable_fonts_data = {}
    for row in table_body.find_all('tr'):
        axis_data = {}
        cells = row.find_all('td')
        font_family_name = cells[0].find('a').encode_contents().decode("utf-8").strip()
        if not (font_family_name in variable_fonts_data):
            variable_fonts_data[font_family_name] = {'Axes': {}}

        axis_data[header_values[2]] = cells[2].encode_contents().decode("utf-8").strip()  # Default
        axis_data[header_values[3]] = cells[3].encode_contents().decode("utf-8").strip()  # Min
        axis_data[header_values[4]] = cells[4].encode_contents().decode("utf-8").strip()  # Max
        axis_data[header_values[5]] = cells[5].encode_contents().decode("utf-8").strip()  # Step

        variable_fonts_data[font_family_name]['Axes'][cells[1].encode_contents().decode("utf-8").strip()] = axis_data

    return variable_fonts_data


with open('google_variable_fonts.json', 'w') as fonts_file:
    json.dump(get_variable_fonts_data(), fonts_file) 
Stephen Miller
  • 503
  • 3
  • 11
  • Combining the scraper with @Stranger1586's answer gives you everything you need. The scraper gives you data on each axis including steps and from the "axisRegistry" in the https://fonts.google.com/metadata/fonts endpoint, you can get the display names of each axis. – Stephen Miller Oct 01 '21 at 05:38
  • `I really also need data on the steps...` I'm not sure why you want the steps. Note that variable fonts allow _continuous_ variation on each axis (modulo granularity of 16 fractional bits). A can can modulate an axis into discrete steps, but that's not generally done. – Peter Constable Oct 17 '21 at 02:14
  • @Peter Constable. As i wrote: Because I am builiding UI elements such as sliders to control the fonts axes. They differ, you know. Some use decimals, some use whole numbers, they have different end points and some even have negative values. How would you build functional sliders to control axes without such data? – Stephen Miller Jan 27 '22 at 19:23
  • I'm not familiar with what Google's APIs or metadata files have, but can tell you what's in the font itself. A font's 'fvar' table defines the axes and their min, default, max points (all expressed as 16.16 float). It also has an array of named instances; e.g., _[wght:600, wdth:75] = "Condensed Semibold"_. Now, a key question for you is whether you want the UI to show named instances or the steps along each axis. The named instances will imply points along each axis, but only in combos of all axes. If you want steps for each axis independently, the 'STAT' table (if properly created) has that. – Peter Constable Jan 28 '22 at 17:59