Friday, September 04, 2009

@font-face we are already doing wrong

Update - Now We Do Right

Thanks everybody for your tests and contributions. For those interested about why we were doing wrong please read both post and comments but for those just interested about the best way so far to serve correctly one or more font-face, this is the hack:

@font-face {
// define the font name to use later as font-family
font-family: "uni05_53";
// define the font for IE which totally ignores local() directive
src: url(../font/uni05/uni05_53.eot);
// use local to let IE jump this line and
// redefine the src for this font in order to let
// other clever browser download *only* this font rather than 2
src: local("uni05_53"), url(../font/uni05/uni05_53.ttf) format("truetype");
}

You can test directly this technique in my HTML5 Prime Directives Test Page



Credits

  • Paul Irish for its Bulletproof @font-face syntax

  • Mikuso, comments, for his suggestions about server configurations, instantly followed by Weston Ruter for its excellent article Efficiently Serving Custom Web Fonts

  • last, but not least, Scott Kimler, for his Better @font-face Syntax and his patience, testing directly inside a trace log, rather than trust 100% Fiddler or Firebug - P.S. my hat is off for that page, unfortunatly you have trolls problems as well, reading the first comment from somebody that got -1% of what you have done!
Links, snippets, tests, we have got everything we need to understand why font-face CSS plus file serving has been generally misunderstood and how we should do correctly, trolls included (the problem is not the browser).

Good Stuff!
Just last quick info for Scott Kimler, IE simply lacks "local" directive support, and this is the reason it ignores the second call.
If we put local(fontName), url(fontName.eot) it will not load the eot, neither the ttf - local is the key for this trick, but we'll have problems the day IE will understand local, unless the first src will not considered synchronous.
Hopefully, that day IE will support ttf since windows does already without problems.


Few weeks with new browsers and @font-face support that suddenly everybody started to suggest the "cross browser way" to include them ... which is 90% of cases apparently wrong.

The Wrong Way



/* IE first */
@font-face {
font-family: nomeDelFont;
src: url( /nomeDelFont.eot );
}

/* Firefox 3.5/Safari/Opera 10 */
/* but IE will download in any case */
@font-face {
font-family: nomeDelFont ;
src: url( nomeDelFont.ttf );
}

almost the equivalent showed here, via Ajaxian, and Edit ...

What Is Exactly Wrong


It does not matter if we use conditional comments, it does not matter if we put the right font for Internet Explorer before the other one (tricky, since IE does not overwrite the rule just because it does not understand the truetype format).

Our "favorite" browser, and others as well in some case, will always request the truetype as well, and being fonts not that lightweight, our page response could sensibly increase.

HTML5 Prime Directives


Inspired by RoboCop, I have created a simple test page that does not suffer the problem described before.
What we have there is an extremely compact and valid HTML5 page which size is up to 140Kb, and almost 67Kb just for the font.

Plus, as I've said, If we use common suggested snippets Internet Explorer will download in any case the non IE font, try yourself!

First Suggestions To Try To Solve The Problem



  1. never forget to specify the format, format("truetype"), which is apparently able to let IE misunderstand correctly the url and the result is a Error 404, still better than 70Kb to download

  2. moreover, use whatever strategy you know to avoid non IE file serving for IE as well (use Fiddler to monitor the network)

  3. try to create gzipped or deflated version of each font, possibly not runtime, and serve them via proper headers (69Kb down to 22Kb, as example, with 7Zip deflate)

  4. please tell us how you avoided IE wrong font download without using an horrible JavaScript document.write as I did in my little test page


Thanks, and I hope you agree about Prime Directives :D

18 comments:

Ricardo said...

Hi Andrea,

Why not use our friends conditional comments for IE? [if !IE], etc. (can't post the comment markers).

I wonder if the ttf font is downloaded by IE even if the class is not applied, if it doesn't we can use the cc + body class trick.

Paul Irish said...

Hah! Andrea, your test page is so damn excellent. :)

Your post inspired me to comment on all the varieties of doing @font-face syntax. I'd love to get your thoughts:
Bulletproof @font-face implementation syntax

Mikuso said...

I recently had to tackle the exact same problem. I decided that the best course of action was a bit of server side magic, rather than leave it to the browser to decide.

I wrote a RewriteRule in my .htaccess to serve an .eot equivalent font to any browser identifying itself as MSIE.

Looks like this:

RewriteCond %{HTTP_USER_AGENT} MSIE
RewriteRule ^(.*)\.otf$ $1.eot

Andrea Giammarchi said...

Mikuso, that is what I was thinking as well.
I have created another test page with server side code to serve the correct gzipped font.


if(isset($_GET['font'])){
$ext = strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') !== false ? 'eot' : 'ttf';
$font = basename($_GET['font']).'.'.$ext;
if(file_exists($font)){
ob_start('ob_gzhandler');
header('Content-Type: font/'.$ext);
echo file_get_contents($font);
}
}

Mikuso said...

Andrea, there is a much better way.

Using my rewrite rule above simply redirects the request internally.

You can add the gzip compression rules to the .htaccess file as well.

Here is mine:

AddOutputFilterByType DEFLATE application/vnd.oasis.opendocument.formula-template application/vnd.ms-fontobject

That's using the default MIME types for eot and otf in my apache config.

If your eot and otf files are served differently, as in your PHP example, rewrite that statement as so:

AddOutputFilterByType DEFLATE font/otf font/eot

This is much faster than using PHP and means no implementation hurdles.

One problem I can see with your PHP implementation: What happens when the browser doesn't support gzip?

If you want to add in a file_exists() type condition in the RewriteRule, that is possible too.

RewriteCond %{HTTP_USER_AGENT} MSIE
RewriteCond %{REQUEST_FILENAME} (.*)\.otf
RewriteCond %1.eot -f
RewriteRule ^(.*)\.otf$ $1.eot

The regular expressions I've written can be easily modified to allow for TTF too.

Andrea Giammarchi said...

@Mikuso , .htaccess needs to be searched, parsed, and executed, for each request that points to that folder.
I'd rather prefer httpd.conf modification and an Apache restart, for example.
My PHP code was a simple snippet for those people without access to the apache httpd.conf file, those hosting solutions without .htaccess feature, and finally the ob_start('ob_gzhandler') should manage automatically the missed support serving non deflated/gzipped content.

But honestly, being font-face a CSS3 feature, and being supported since IE5 or greater, please tell me a single case where a browser would need a font from the server without gzip support - I do not think it is a real case scenario.

On the other hand, if that browser does not support the font, I prefer a download and unused gzipped file, rather than 70, 100, or more Kb to serve with any purpose. Got my point?

Mikuso said...

I believe font-face has been in MSIE since IE4. And I also believe gzip isn't supported in IE4+5 for Mac. But I have not tested any of these myself, so I cannot be certain.

In any case, you are right, ob_start('ob_gzhandler') will compensate for browsers which do not support gzip.

Certainly there are merits to your approach in that it is much more accessible for the shared-hosting masses; but there's nothing you've done in that simple bit of PHP which couldn't be done by adding some lines to an apache config.

And I might argue, that a lot of people already use .htaccess for other purposes, negating some of the overhead of searching and parsing said file.

I'm also quite confident that parsing and executing a .htaccess file is much quicker than JIT compiling and executing a PHP file. But that's just a hunch.

In my opinion, this is a better and cleaner approach; but as you say, it's not too useful for those who do not have access to such functionality.

With either of our approaches, there would never be a case where the browser would download a font that it cannot use.

Andrea Giammarchi said...

@Mikuso ... there's nothing you've done in that simple bit of PHP which couldn't be done by adding some lines to an apache config.

Which part of "that snippet was simply an alternative" is not clear? If you want to go on I call me out of the conversation.

I'll never use that PHP script in production, I simply provided an alternative and I do not think this is the right place to talk about suppositions about .htaccess performances, don't you agree?

Regards

Weston Ruter said...

@Ricardo:
I agree that conditional comments can be used to efficiently serve EOT to IE, and TTF to non-IE. For example: http://weston.ruter.net/2008/08/27/efficiently-serving-custom-web-fonts/

In this example, IE doesn't even see the reference to the .ttf file, so it doesn't try to download it. I believe this example would do well to include format("truetype") as well.

Otherwise, without conditional comments, rewrite rules can be constructed to serve .eot files to IE and .ttf files to non-IE browsers (see link above).

Andrea Giammarchi said...

@Weston, thanks a lot, that seems to be a good solution because clever browsers will not download the eot thanks to overwritten font name.

This one is, in any case, an assumption, as I am assuming you tested network traffic with all other major browsers such Opera, Chrome, Safari, Firefox, and their multiple versions.

Would be nice to quickly set up a test case for your suggestion, able to understand which userAgent will not overwrite that rule requiring two times fonts.

I think I am going for it, if you do not mind, and after some day, if you guys help me spamming the test around, we could make our conclusion about this strategy which is, in my opinion, the best one so far.

Ricardo said...

Back to old-school CSS hacks. This seems to work on all browsers except Chrome:

@font-face {
font-family: pelego;
src: url(uni05_63.ttf) format('truetype');
src: url(uni05_63.eot) \9;
}

Andrea Giammarchi said...

Ricardo, that should still produce a 404 error because IE will try to download the ttf in any case, am I wrong?

Ricardo said...

Apparently you are! In my testing with Fiddler and IE8/IE6 it only requests the .eot file, while other browsers ignore it.

Can someone confirm this on a legit IE7?

Andrea Giammarchi said...

Ricardo, CSS hacks are a solution if there are no alternatives.
I have updated this post in order to let people understand everything about the problem, suggesting the one I think is the best, and tested, CSS solution, and without hacks.
Thanks in any case for the effort :)

stk said...

Andrea,

Thanks for the mention and pointing out the 404 issue. Understand @local(), but I suspect it's is relatively future-proof.

IE has supported @font-face since IE4, without variation. Chances are, if/when they support "local()", they'll likely support the entire lot.

It's great to have some bulletproof CSS (and to see Op10 and Chrome support).

LOL @ trolls. I guess it's the price for having an open commenting system. Logic and reasoning tends to keep them at bay. ;)

Cheers from Vancouver Island!

Richard Fink said...

Andrea,

In my tests Opera 10 on Windows is not working with local('fontname') in the syntax.
Fine in Safari and FF.
Can anyone confirm that this syntax does, indeed work in Opera?
Regards,

Richard Fink

Andrea Giammarchi said...

Richard, if I am not wrong I had no problems but I will investigate soon, thanks for the report.

Anonymous said...

I felt it was a rather neat syntax, but it didn't work at all when I tested it on Opera 10 either. I ended up having to just use IE conditionals and a separate style sheet before my site worked right everywhere.