w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 661 forks source link

[css-fonts] Multiple @font-face multiple src declarations #7753

Open hamishwillee opened 2 years ago

hamishwillee commented 2 years ago

CSS Fonts Module Level 4, Editor’s Draft, 15 September 2022

Says in @font-face rule:

.... When a given descriptor occurs multiple times in a given @font-face rule, only the last descriptor declaration is used and all prior declarations for that descriptor are ignored.

Then example 22 shows where we have multiple src descriptors, allowing fallback:

@font-face {
  font-family: "MyIncrementallyLoadedWebFont";
  src: url("FallbackURLForBrowsersWhichDontSupportIncrementalLoading.woff2") format("woff2");
  src: url("MyIncrementallyLoadedWebFont.otf") format(opentype)  tech(incremental);
}

My assumption is that multple src descriptors are allowed if valid. If so perhaps the initial sentence could reflect this. Not sure how - maybe something like

.... When a given descriptor occurs multiple times in a given @font-face rule, only the last actionable descriptor declaration is used and all prior declarations for that descriptor are ignored.

drott commented 2 years ago

CC @svgeesus: this sounds like a useful clarification and it's something that I discussed recently with @jfkthame.

cdoublev commented 1 year ago

My assumption is that multple src descriptors are allowed if valid.

This would prevent setting/updating src via CSSOM, assuming such use case exists and that the specs explicitly support this and define what should happen.

I guess browser implementations register font source definitions when parsing src therefore multiple declarations are already accepted and none is ignored.

The author can already define multiple sources with comma-separated values:

src: url("FallbackURLForBrowsersWhichDontSupportIncrementalLoading.woff2") format("woff2"),
     url("MyIncrementallyLoadedWebFont.otf") format(opentype)  tech(incremental);

The src descriptor value must be parsed according to section CSS Syntax 3 § 5.3.11 Parse a comma-separated list of component values.

jfkthame commented 1 year ago

I guess browser implementations register font source definitions when parsing src therefore multiple declarations are already accepted and none is ignored.

No, as far as I am aware browsers keep only the last (valid) src descriptor they see. E.g. in Chrome:

> s = document.createElement("style"); document.body.appendChild(s);
> s.textContent = "@font-face { font-family: x; src: url(x.ttf); src: url(y.ttf); }";
> document.styleSheets[document.styleSheets.length-1].cssRules[0].cssText
< '@font-face { font-family: x; src: url("y.ttf"); }'  // only the second src descriptor is present

But:
> s.textContent = "@font-face { font-family: x; src: url(x.ttf); src: url(y.ttf) format(foo); }";
> document.styleSheets[document.styleSheets.length-1].cssRules[0].cssText
< '@font-face { font-family: x; src: url("x.ttf"); }'  // second src was invalid, so the first is retained

Regarding fallbacks:

The author can already define multiple sources with comma-separated values:

src: url("FallbackURLForBrowsersWhichDontSupportIncrementalLoading.woff2") format("woff2"),
     url("MyIncrementallyLoadedWebFont.otf") format(opentype)  tech(incremental);

With this, any browser that accepts WOFF2 would use the first source, and so the incremental option would never be considered.

To have the intended effect, this should be the other way around:

src: url("MyIncrementallyLoadedWebFont.otf") format(opentype)  tech(incremental),
     url("FallbackURLForBrowsersWhichDontSupportIncrementalLoading.woff2") format("woff2");

This would be the "preferred" or modern way to express fallbacks. However, there are existing browsers that do not recognize the tech() function and will drop the entire src descriptor (not just that item from the list -- contrary to what the spec says). (They'll similarly drop the entire descriptor if any one item has some other kind of syntax error.)

Therefore, for maximum compatibility it's advisable to use multiple src descriptors, with the "modern" full descriptor last, but preceded by a simpler, backward-compatible fallback for browsers that will reject the modern version. So we end up with something like:

@font-face {
  font-family: MyFamily;
  src: url("FallbackURLForBrowsersWhichDiscardModernSrcDescriptors.ttf");
  src: url("MyIncrementallyLoadedWebFont.otf") format(opentype)  tech(incremental),
       url("FallbackURLForBrowsersWhichDontSupportIncrementalLoading.woff2") format("woff2");
}

Legacy browsers that reject the second src entirely will keep the first version of the descriptor; modern browsers will override the first src with the second, and use the first supported resource from its list.

hamishwillee commented 1 year ago

However, there are existing browsers that do not recognize the tech() function and will drop the entire src descriptor (not just that item from the list -- contrary to what the spec says). (They'll similarly drop the entire descriptor if any one item has some other kind of syntax error.)

Thanks @jfkthame . Just FYI, I did some playing and it is more like "most" than "some". FF109 is first version that drops just invalid item from list. Chrome 108 is the first version that will drop just a tech() descriptor but will drop all descriptors if you were to put in fred() say (i.e. it seems to allow just the tech() case, even if not supported).