fabricjs / fabric.js

Javascript Canvas Library, SVG-to-Canvas (& canvas-to-SVG) Parser
http://fabricjs.com
Other
28.75k stars 3.49k forks source link

Implement Native WebFont Loading API to fix numerous Text/I-Text bugs #2027

Open gordyr opened 9 years ago

gordyr commented 9 years ago

There are several bugs within fabric caused by not always knowing when a webFont has been loaded.

A very easily reproducible one is as follows:

  1. Create an Interactive Canvas. Along with an I-Text object with several reasonable wide lines of text.
  2. Load in a custom webFont that has unusual character widths (I have been using Nosifer from the google WebFonts directory as it is much wider than any native web safe fonts).
  3. Apply a backgroundColor to the IText box.
  4. Save the JSON then clear your browser cache.
  5. Reload the JSON into either a static or interactive canvas using loadFromJSON.

You will find that although the font gets drawn correctly (Browsers now account for this when using html5 canvas) the dimensions of the object are incorrect and therefore any background color or text that falls outside those incorrect dimensions, fails to render.

As shown here:

example1

When it should look like this:

example2

The reason for this is that fabric, rightfully, check and caches character widths of the loaded font and stores them in an object _charWidthsCache in order to prevent them having to be checked constantly during rendering.

Now, although browsers will now wait for the font to be loaded (as long as the correct css for the font is in place before attempting to render it) they completely fail to do so when you check character widths. This results in fabrics cached character widths always being out of date, no matter how long you wait (since the cache never gets cleared when the font has loaded).

In the past @kangax has stated that he feels that font loading should be handled by the users app and for the most part I agree. However, when using loadFromJSON this is deal breaking as in many apps, you may not know what font a user has selected in advance and cannot therefore preload any fonts without parsing the fabric JSON yourself before loading it, which I'm sure everyone would agree is a poor solution.

Recently modern browsers have added a native font loading API to deal with this problem. Chrome has had it enable by default for a while now and Firefox is enabling it in version 37 in a couple of weeks. (right now you can enable it manually in about:flags.

For more info see here:

https://dev.opera.com/articles/better-font-face/

And here for the spec:

http://dev.w3.org/csswg/css-font-loading/#font-load-event-examples

There are also several polyfills out there which mimic this behaviour on older browsers and appear to work perfectly.

The one I am using is a slightly modified version of this one:

https://github.com/zachleat/fontfaceonload

The only modifications I have made is to check for native websafe fonts first and abort the loading check and firing the callback right away if we know the browser already supports them (on fonts like Arial and Times new roman etc.) This is actually quite important as this particular fontloader may fail if attempting to load a metric compatible font. There are many others out there that may have this built in, but the solution is a trivial indexOf check versus an array of native websafe fonts.

Anyway, the above fontLoader will use the new native font loading api of the browsers where available and fallback gracefully on older ones.

Fixing most font loading bugs are then as simple as patching fabric like so:

   fabric.IText.async = true;

    fabric.IText.fromObject = function(object, callback) {
        FontFaceOnload(object.fontFamily, {
            success: function() {
                //font is now loaded so fire the callback
                callback && callback(new fabric.Text(object.text, fabric.util.object.clone(object)));
            },
            error : function(err){
                //an error has occurred and the font has not been loaded. still fire the callback but consider throwing an error
               callback && callback(new fabric.Text(object.text, fabric.util.object.clone(object)));
            }
        });
    };

with the above polyfill and this tiny patch added loadFromJSON now works correctly with every webfont I have thrown at it, whatever the dimensions of the characters. Therefore fixing issue #2018 and many others.

I believe that the polyfill could easily be added into the fabric.util class (perhaps modifying it like I do to avoid attempting to load websafe native fonts) and the above path could be applied to both Text and IText with only a tiny amount of code gain. This would solve a lot of peoples problems and prevent the constant recurring issues on here regarding this.

Also, I would consider adding it into the set method when a user changes fontFamily in the same manner that loadImage is currently used.

The reason I have not created a PR is simply that I just do not have the time right now, I am swamped. But I would like to get the information here for those of us who have been frustrated with this issue.

Hopefully @kangax could consider adding it in himself at some point. I hope this helps!

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

kangax commented 9 years ago

@gordyr thanks for an excellently-articulated rationale! Considering the small size of @zachleat's polyfill, I wouldn't mind using it as an optional dependency, similar to what we do with @mudcube's Event.js for gestures on mobile. We could then detect presence of FontFaceOnload and calculate chars width automatically (in fromObject and/or other places)

asturur commented 9 years ago

i could try to set it up.

kangax commented 9 years ago

@asturur Andrea, that would be great

jafferhaider commented 9 years ago

Is FontFaceOnload library similar to Google's WebFont? Because when preloading fonts with WebFont, you still need to create a DOM element that uses that font-family before using that font-family in Fabric. Otherwise, that font doesn't get used unless you click on the canvas once.

See discussions here: https://groups.google.com/forum/#!topic/fabricjs/sV_9xanu6Bg http://stackoverflow.com/questions/28877001/canvas-only-renders-custom-webfont-on-click-event-with-fabric-js

I've found this method of "creating (off screen) a DOM element that uses the font family right before using it in the Canvas for the first time" to be reliable in ensuring that the font really gets used. You still need some preloading mechanism with callbacks like WebFont or FontFaceOnload. Was just wondering if FontFaceOnload prevents this hack of adding a DOM element.

gordyr commented 9 years ago

No the new font loading API is entirely different to googles webfont loader which is just an abstraction built in JavaScript to approximate the detection of loaded webfonts

The new API is native to browsers and does not use any Dom elements.

zachleat commented 9 years ago

Hm? Author of FontFaceOnload here.

When the CSS Font Loading API is available, it does not use DOM elements, sure. However, the polyfill behavior does. Native API support is limited http://caniuse.com/#feat=font-loading

TypeKit (which is what Google WebFonts uses) currently uses the polyfill behavior in all browsers (no CSS Font Loading API use yet).

gordyr commented 9 years ago

Exactly, that's why I suggested using your library. So that we support browsers without native font loading.

jafferhaider commented 9 years ago

Nice! Thanks for the clarification @gordyr @zachleat

ajck commented 8 years ago

@gordyr would you be willing to share your modifications of zachleat's fontfaceonload ? (Re: "The one I am using is a slightly modified version of this one: https://github.com/zachleat/fontfaceonload The only modifications I have made is to check for native websafe fonts first and abort the loading check and firing the callback right away if we know the browser already supports them (on fonts like Arial and Times new roman etc.) Thanks!

AlexanderIstomin commented 7 years ago

I wrote functions to load fonts from google webfonts library. Also i solved all issues with characters width/lines width and loading texts from json with google web fonts applied

ajck commented 7 years ago

@AlexanderIstomin how did you solve the issues with characters width and lines width? I am still getting extra spaces when using diacritics (modifying characters) in e.g. Arabic etc. Care to share any code? :)

AlexanderIstomin commented 7 years ago

i will create fiddle tomorrow with latest version of my code

AlexanderIstomin commented 7 years ago

just copied and pasted some code parts to jsfiddle. https://jsfiddle.net/765doq2b/1/

asturur commented 7 years ago

i would follow a slight different approach, instead of changing the font and then check if it needs to be loaded and resetting caches, i would load the font, and then change the font to the object. In that case no reset is necessary at all. Most font loader return a promise or take a callback, so you can change the font in the callback or in the then part of the function. Thanks for making a fiddle anyway!

AlexanderIstomin commented 7 years ago

agree. but i faced with char width problem somewhere in 1.4.x versions, so resetting is (as i remember) solved that issue.