Warm tip: This article is reproduced from serverfault.com, please click

How can I detect when a font is downloaded via CSS's "unicode-range" descriptor?

发布于 2020-12-01 17:09:20

I'm trying to display a custom web font on a <canvas> element, however it doesn't always display correctly first time as the font has not been pre-loaded:

How it currently looks:

Canvas without web font loaded

Here's how it should look:

Canvas with web font loaded correctly


I'm aware that several methods of pre-loading web fonts exist but unfortunately they're not applicable to this situation because the page can use up to 90 fonts.

The reason for the large number is because I'm using a Chinese font which has been segmented into many separate .woff2 files so that my CSS file can make use of the unicode-range descriptor like so:

/* unicode range [1] */
@font-face {
  font-family: 'ma-shan-zheng';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('Ma Shan Zheng Regular'), local('MaShanZheng-Regular'), url(ma-shan-zheng.5.woff2) format('woff2');
  unicode-range: U+fee3, U+fef3, U+ff03-ff04, U+ff07, U+ff0a, U+ff17-ff19, U+ff1c-ff1d, U+ff20-ff3a, U+ff3c, U+ff3e-ff5b, U+ff5d, U+ff61-ff65, U+ff67-ff6a, U+ff6c, U+ff6f-ff78, U+ff7a-ff7d, U+ff80-ff84, U+ff86, U+ff89-ff8e, U+ff92, U+ff97-ff9b, U+ff9d-ff9f, U+ffe0-ffe4, U+ffe6, U+ffe9, U+ffeb, U+ffed, U+fffc, U+1f004, U+1f170-1f171, U+1f192-1f195, U+1f198-1f19a, U+1f1e6-1f1e8;
}

/* unicode range [2] */
@font-face {
  font-family: 'ma-shan-zheng';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('Ma Shan Zheng Regular'), local('MaShanZheng-Regular'), url(ma-shan-zheng.6.woff2) format('woff2');
  unicode-range: U+f0a7, U+f0b2, U+f0b7, U+f0c9, U+f0d8, U+f0da, U+f0dc-f0dd, U+f0e0, U+f0e6, U+f0eb, U+f0fc, U+f101, U+f104-f105, U+f107, U+f10b, U+f11b, U+f14b, U+f18a, U+f193, U+f1d6-f1d7, U+f244, U+f27a, U+f296, U+f2ae, U+f471, U+f4b3, U+f610-f611, U+f880-f881, U+f8ec, U+f8f5, U+f8ff, U+f901, U+f90a, U+f92c-f92d, U+f934, U+f937, U+f941, U+f965, U+f967, U+f969, U+f96b, U+f96f, U+f974, U+f978-f979, U+f97e, U+f981, U+f98a, U+f98e, U+f997, U+f99c, U+f9b2, U+f9b5, U+f9ba, U+f9be, U+f9ca, U+f9d0-f9d1, U+f9dd, U+f9e0-f9e1, U+f9e4, U+f9f7, U+fa00-fa01, U+fa08, U+fa0a, U+fa11, U+fb01-fb02, U+fdfc, U+fe0e, U+fe30-fe31, U+fe33-fe44, U+fe49-fe52, U+fe54-fe57, U+fe59-fe66, U+fe68-fe6b, U+fe8e, U+fe92-fe93, U+feae, U+feb8, U+fecb-fecc, U+fee0;
}

/* etc. all the way up to [90] */

This has the obvious benefit of only downloading the relevant .woff2 file when it's needed, but it also means that when the user downloads the web font for the first time they will see the unstyled text as shown above.

In an ideal world I'd be able to attach a callback function to the automatic download of the fonts, but it seems there is no access to this part of the browser's behaviour.


My current workaround

I've modified a solution from an old SO question about web font pre-loading - it's very hacky, but it does work.

In short, it creates a <span> element with some text in a default font, measures the width/height, sets the element font to the web font, measures the size again and compares the results. If the size has changed, it's assumed the web font has loaded:

function waitFontLoaded(font, phrase, callback) {
    var node = document.createElement("span");

    // Set node content to the desired phrase/text
    node.innerHTML = phrase;

    // Visible - so we can measure it - but not on the screen
    node.style.position = "absolute";
    node.style.left     = "-10000px";
    node.style.top      = "-10000px";

    // Large font size makes even subtle changes obvious
    node.style.fontSize = "300px";

    // Reset any font properties
    node.style.fontFamily    = "sans-serif";
    node.style.fontVariant   = "normal";
    node.style.fontStyle     = "normal";
    node.style.fontWeight    = "normal";
    node.style.letterSpacing = "0";

    document.body.appendChild(node);

    // Remember size with no applied web font
    var width  = node.offsetWidth;
    var height = node.offsetHeight;

    node.style.fontFamily = font + ", sans-serif";

    var interval;

    // Compare current size with original size
    function checkFont() {
        if (node && (node.offsetWidth !== width || node.offsetHeight !== height)) {
            node.parentNode.removeChild(node);
            node = null;

            clearInterval(interval);

            callback();

            return true;
        }

        return false;
    }

    if (!checkFont()) {
        interval = setInterval(checkFont, 50);
    }
}

As I said, this does work, but is clearly not a robust solution as it's not impossible for two characters to be the same size in both the default system font and the web font.

Another very hacky solution would be to simply refresh the <canvas> element every second, e.g. by using a setInterval.

I feel like there must be a cleaner and more elegant way of doing this. Can anyone offer any suggestions?

Questioner
user7290573
Viewed
0
Kaiido 2020-12-02 14:16:57

In an ideal world I'd be able to attach a callback function to the automatic download of the fonts

I wouldn't call it an ideal world yet, but you can actually do that.

The document.fonts.ready Promise resolves when all the fonts necessary to render visible text on the page have loaded.

Not far from there, you can iterate through document.fonts which holds all the FontFaces that have been declared and check if they have loaded or not, along with their defined unicode-range if required.

document.fonts.ready.then( () => {
  const loaded_fonts = [ ...document.fonts ]
    // simplify the objects for logging here
    .map( ({unicodeRange, status}, index) => ({ unicodeRange, status, index }) )
    .filter( ({status}) => status === "loaded" );

  console.log( loaded_fonts );
});
/* cyrillic-ext index:0 */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic index:1*/
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext index:2*/
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
/* greek index:3*/
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0370-03FF;
}
/* vietnamese index:4 - should be loaded */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext index:5 - should be loaded */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin index:6 - should be loaded */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.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;
}


body {
  font-family: "Roboto";
}
Hello thế giới

If you need to load a particular one before using it on a canvas, you can call document.fonts.load("your font", the_text_to_render) which will load all the FontFaces required to render the_text_to_render:

( async () => {
  // <DEMO only>
  // just to be sure the font was not loaded yet
  await document.fonts.ready;
  logLoadedFontsCount( "after document.fonts ready" );
  // </DEMO only>
  
  // now try to draw using that font face anyway
  const canvas = document.querySelector( "canvas" );
  const ctx = canvas.getContext( "2d" );
  const font_shorthand = "30px Roboto";
  const text = "Привет мир";

  // force loading fonts
  await document.fonts.load( font_shorthand, text );
  // now we can use it
  ctx.font = font_shorthand;
  ctx.fillText( text, 30, 50 );

  // <DEMO only>
  logLoadedFontsCount( "after loading of customs fonts" );
  // </DEMO only>
} )();

// <DEMO only>
// logs how many FontFaces are currently loaded
function logLoadedFontsCount( when = "" ) {
  const loaded_fonts = [ ...document.fonts ]
    .filter( ({status}) => status === "loaded" );
  console.log( "%s fonts loaded %s", loaded_fonts.length, when );
} 
// </DEMO only>
/* cyrillic-ext index:0 */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic index:1*/
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu5mxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext index:2*/
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7mxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
/* greek index:3*/
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4WxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0370-03FF;
}
/* vietnamese index:4 - should be loaded */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7WxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext index:5 - should be loaded */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu7GxKKTU1Kvnz.woff2) format('woff2');
  unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin index:6 - should be loaded */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.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;
}
<canvas></canvas>