/lib-font

This library adds a new Font() object to the JavaScript toolbox, similar to new Image() for images

Primary LanguageJavaScriptMIT LicenseMIT

LibFont - lifting the hood on your fonts

If you're looking for the (really) old ES5 version of LibFont, when it was still called "Font.js", you should be able to find it here.

Also, if this made a difference in your dev life, consider (temporarily, even?) becoming a patron of my work over on https://www.patreon.com/bezierinfo, or send a one-time donatation to help keep this project funded. Any amount is appreciated!

td;dr:

Node installation

Use npm install lib-font, after which the package can be imported using import { Font } from "lib-font".

Note that there is no legacy commonjs version of this library available. Node LTS 14 and above have native support for ES modules - Have a look at babel's transform-modules-commonjs plugin if you really have no choice but to use commonjs.

Browser "installation"

You can either use the pre-built lib-font.browser.js bundle, or you can download the code and unpack it into a vendor directory, then point to the lib-font.js file in that directory. Either way, remember to tell the browser that this is modern ES module code by using type="module" in the script tag, and be kind to your users and remember the async attribute, too:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>...</title>
    ...
    <script type="module" src=".../lib-font.js" async></script>
  </head>

  <body>
    ...
  </body>
</html>

(Although you'll generally want to use this in something so your HTML would look more like <script type="module" src="./js/myscript.js" async></script> with the myscript.js file having an import { Font } from "./lib-font"; instead)

Note that there is no legacy ES5 version of this library available, because there are no ES5 browsers anymore, every single still in support browser supports ES Modules, so if you really need to target dead browsers have a look at babel.

Introduction

What if you could actually inspect your fonts? In the same context that you actually use those fonts?

That's what this is for:

// On the Node side: import { Font } from "lib-font";
// In browser-ESM context: import { Font } from "your/js/dir/lib-font-browser.js";
//
// Also note that on the browser side, you can either use the ESM import 
// or you can load the library "on its own" using its own script tag,
// which sets up a global `Font` object for you to use.

// Create a font object
const myFont = new Font(`Adobe Source Code Pro`);

// Assign event handling (.addEventListener version supported too, of course)
myFont.onerror = evt => console.error(evt);
myFont.onload = evt => doSomeFontThings(evt);

// Kick off the font load by setting a source file, exactly as you would
// for an <img> or <script> element when building them as nodes in JS.
myFont.src = `./fonts/SourceCodeVariable-Roman.otf`;

// When the font's up and loaded in, let's do some testing!
function doSomeFontThings(evt) {
    // We can either rely on scoped access to font, but because the onload function
    // is not guaranteed to live in the same scope, the font is in the event, too.
    const font = evt.detail.font;

    // First, let's test some characters:
    [`a`, `→`, `嬉`].forEach(char => console.log(`Font supports '${char}': ${
        font.supports(char)
    }`));

    // Then, let's check some OpenType things
    const GSUB = font.opentype.tables.GSUB;

    // Let's figure out which writing scripts this font supports:
    console.log(`This font supports the following scripts: ${
        `"${GSUB.getSupportedScripts().join(`", "`)}"`
    }`);

    // DFLT is a given, but let's see if `latn` has any special language/system rules...
    const latn = GSUB.getScriptTable('latn');
    console.log(`Special langsys for "latn": ${
        `"${GSUB.getSupportedLangSys(latn).join(`", "`)}"`
    }`);

    // Wow, "Northern Sami" support? Really? Which OpenType features does that use?
    const nsm = GSUB.getLangSysTable(latn, "NSM ");
    console.log(`OpenType features for the Northern Sami version of latin script:`,
        `"${GSUB.getFeatures(nsm).map(f => f.featureTag).join(`", "`)}"`
    );

    // Oh wait, this is a variable font, isn't it.
    console.log(`This is a variable font: ${!!font.opentype.tables.fvar}`);

    // Which axes does it support?
    console.log(`This variable font supposed the following axes: ${
        `"${font.opentype.tables.fvar.getSupportedAxes().join(`", "`)}"`
    }`);
}

You can also pass in a file directly, e.g. using the HTML Drag and Drop API. In order to deal with the font as bytecode, you can use the font.fromDataBuffer(bytecode, filename) function to kick off the font loading:

const myFont = new Font(`Adobe Source Code Pro`);

// Grab file frop drop event or file upload
const file = e.target.files[0];

// Use FileReader to, well, read the file
const reader = new FileReader();
reader.readAsArrayBuffer(file);

reader.onload = function() {
    // Pass the buffer, and the original filename
    myFont.fromDataBuffer(reader.result, file.name);
    myFont.onload = e => {
        // ...
    };
};

What about a tag/element?

This library does not offer a <font src="..." ...> tag, in part because proper custom elements must have a hyphen in their name, but primarily because the only DOM related work that a <font> tag would be useful for is already handled by <style> (for declaring a @font-face) and <link> (for importing a @font-face stylesheet).

API

The uplifted library API is still pending... As of right now, a lot of the functions and properties are pretty easily found if you know your way around an OpenType font already by looking at the source as well as the tests, but that's not ideal - API docs will be forthcoming but can always use help.

That said, this section will keep getting expanded as the API gets consolidated.

Font

const f = new Font(fontFamilyName, optionsObject)

The family name is use for stylesheet building, and the options object can contain the following fields:

fieldname type default description
skipStyleSheet boolean false Determines whether not to build the @font-face stylesheet.
styleRules object {} A set of key/value pairs representing CSS rules to add to the @font-face declaration
errorOnStyle boolean false Whether to error out, or merely warn, if the font type cannot be determined from the src assignment.

f.onerror = evt => ... / f.addEventListener('error', evt => ...)

Error handling for anything font related

f.onload = evt => ... / f.addEventListener('load', evt => ...)

Load handling for anything font related

f.src = ...

Bind a font to a source URL. This can be a fully qualified URL, a relative URL, a Blob URL, or a Data-URL.

const actualFont = f.opentype

The actual opentype font representation is font in the .opentype property.

const fontTables = f.opentype.tables

This is the main access point for any font table, where each table is accessed directly by name. E.g. in order to access the cmap table, you use const cmap = f.opentype.tables.cmap, GSUB is const GSUB = f.opentype.tables.GSUB, etc.

Development

The main entry point for this library is ./lib-font.js, with all code dependencies found in the ./src directory,.

Testing

The npm test command should be all you need in order to run the tests, provided you ran npm install first, of course.

  • Node based testing uses Jest tests, found in the ./testing/node dir.
  • Browser based testing uses Puppetter, found in the ./testing/browser/tests dir.

Compatibility

This library is designed to run both in any browser and version of Node.js versions that supports es modules.

  • Browsers: see the caniuse matrix (tl;dr: basically everything except IE11).
  • Node: native support as of v14 (--experimental-modules runtime option as of v12).

Preemptive answers to questions

Why don't woff/woff2 work?

They do, but they rely on having the gzip inflater and brotli decoder libraries loaded. You can find those in the ./lib dir, as they are optional: without them regular parsing still works fine, but with inflate loaded, woff parsing will succeed, and with unbrotli loaded, woff2 parsing will succeed.

To make this work on your own pages, add the following bit to your document head, with the appropriate path to these two libraries, of course:

    <script src="./lib/inflate.js" defer></script>
    <script src="./lib/unbrotli.js" defer></script>

Why can't this draw stuff??

Because you already have lots of text shaping engines available. In the browser, it's literally your browser (you can already draw all the text you need, properly shaped and typeset, both in HTML and on a Canvas). In node, it's whatever graphics library you're using to already draw everything else you need to draw.

Proper OpenType text shaping is incredibly complex and requires a lot of specialized code; there is no reason for this library to pretend it supports text shaping when it's guaranteed to do it worse than other technologies you're already using.

Why would I use this instead of OpenType.js or Fontkit or something?

I don't have a good answer to that. Those are some great projects, you probably should use them if they do what you need? The reason I needed this is because it doesn't do text shaping: it just lets me query the opentype data to get me the information I need, without being too big of a library. And I've written enough OpenType parsers to know how much code goes into the actual shaping.

How do I use this with webpack?

By first remembering that bundling was born out of JS not having a module system, where we needed to deliver code in a way that required a single <script> element instead of tens or even hundreds of script tags that all had to load in a specific order - an order that could not be guaranteed so needed additional code to manage execution dependencies. Bundling solved that problem, but at a terrible price: it broke browser caching. Your well organized code consisting of individual files for each role was now a single, megabyte-plus monster file and if you needed to change even a single letter, you'd invalidate the cache entry for that entire bundle. Chunking tried to solve that, but was a classic "deal with the symptom instead of addressing the problem" solution. The real solution landed quite a while ago now: JS got its own module system called ES modules, often just referred to as "ESM", and every single browser on the market supports it. Bundling has not been necessary since the summer of 2022, when Microsoft finally killed off the Internet Explorer line of browsers, removing the last reason people still had for using things like Browserify or Webpack (and really, it hasn't been necessary since a few years prior, because plenty of sites had the luxury of just not bothering to support IE once Edge was available concurrently, because even Microsoft was vocal about the fact that IE was going to get killed off for being hopelessly out of date and incompatible with the web).

This is something you should give serious thought to: bundling is a legacy practice, and if your codebase permits it, stop bundling. You don't need to, and your pages and even web apps will be more performant if you don't.

Second, if you're somehow still stuck with "needing" to bundle, consider switching to esbuild, which is remarkably straight forward to switch to from webpack, even with complex configurations, and puts your project on a modern and both (much) faster and better documented bundler instead. JS tooling like formatters, bundlers, and minifiers should not themselves be written in JS.

However, if you're absolutely stuck with webpack, then as least be on the latest version of Webpack, and tell it to ignore fs and zlib.

// webpack.config.js
module.exports = {
    ...
    resolve: {
        fallback: {
            "fs": false,
            "zlib": false
        },
    }
}

These modules are not loaded "up front" by lib-font, they are dynamic imports that only run when necessary, so if you're running lib-font in the browser those imports will never trigger, and the fact that webpack didn't bundle anything in for them will never result in a code failure or error. Simply make webpack ignore them, because the browser will, too.

Alright, what if I have opinions?

Drop me a message! @TheRealPomax should do nicely, but if you want to have an in-depth discussion, I'd recommend filing an issue, since 280 characters per message is not really sufficient to dig into OpenType details.

And if I just want to use this?

This code is MIT licensed, do whatever you want with it.