如何检测通过 CSS 的 "unicode-range" 描述符下载字体的时间?

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

我正在尝试在 <canvas> 元素上显示自定义 Web 字体,但第一次显示时并不总是正确,因为字体尚未预加载:

目前的样子:

这是应该的样子:


我知道有几种预加载网络字体的方法,但不幸的是它们不适用于这种情况,因为该页面最多可以使用 90 种字体。

数字大的原因是因为我使用的中文字体被分割成许多单独的 .woff2 文件,所以我的 CSS 文件可以使用 unicode-range 像这样的描述符:

/* 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] */

这样做的明显好处是只在需要时才下载相关的 .woff2 文件,但这也意味着当用户第一次下载网络字体时,他们会看到如上所示的无样式文本.

在理想情况下,我可以将回调函数附加到字体的自动下载,但似乎无法访问浏览器行为的这一部分。


我目前的解决方法

我已经修改了一个关于网络字体预加载的旧 SO 问题的解决方案 - 它非常 hacky,但它确实有效。

简而言之,它创建一个 <span> 元素,其中包含一些默认字体的文本,测量 width/height,将元素字体设置为网络字体,再次测量大小并比较结果.如果大小发生变化,则假定网络字体已加载:

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);
    }
}

正如我所说,这个 确实 有效,但显然不是一个可靠的解决方案,因为两个字符在默认系统字体和网络字体。

另一个非常 hacky 的解决方案是简单地每秒刷新 <canvas> 元素,例如通过使用 setInterval.

我觉得必须有一种更简洁、更优雅的方式来做到这一点。任何人都可以提供任何建议吗?

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

我还不能称之为理想世界,但实际上你可以做到。

当在页面上呈现可见文本所需的所有字体都已加载时,document.fonts.ready Promise 将解决。

离那里不远,如果需要,您可以遍历包含所有 FontFaces that have been declared and check if they have loaded or not, along with their defined unicode-rangedocument.fonts

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

如果您需要在 canvas 上使用之前加载特定的字体,您可以调用 document.fonts.load("your font", the_text_to_render) 它将加载所有需要的 FontFaces渲染 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>