Search code examples
javascriptcanvassafariwebkitmeasurement

Canvas.measureText differences on browsers are huge


EDIT: originally I checked only desktop browsers - but with mobile browsers, the picture is even more complicated.

I came across a strange issue with some browsers and its text rendering capabilities and I am not sure if I can do anything to avoid this.

It seems WebKit and (less consistent) Firefox on Android are creating slightly larger text using the 2D Canvas library. I would like to ignore the visual appearance for now, but instead focus on the text measurements, as those can be easily compared.

I have used the two common methods to calculate the text width:

  • Canvas 2D API and measure text
  • DOM method

as outlined in this question: Calculate text width with JavaScript however, both yield to more or less the same result (across all browsers).

function getTextWidth(text, font) {
    // if given, use cached canvas for better performance
    // else, create new canvas
    var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas"));
    var context = canvas.getContext("2d");
    context.font = font;
    var metrics = context.measureText(text);
    return metrics.width;
};

function getTextWidthDOM(text, font) {
  var f = font || '12px arial',
      o = $('<span>' + text + '</span>')
            .css({'font': f, 'float': 'left', 'white-space': 'nowrap'})
            .css({'visibility': 'hidden'})
            .appendTo($('body')),
      w = o.width();

  return w;
}

I modified the fiddle a little using Google fonts which allows to perform text measurements for a set of sample fonts (please wait for the webfonts to be loaded first before clicking the measure button):

http://jsfiddle.net/aj7v5e4L/15/ (updated to force font-weight and style)

Running this on various browsers shows the problem I am having (using the string 'S'):

Measurements for the string 'S'

The differences across all desktop browsers are minor - only Safari stands out like that - it is in the range of around 1% and 4% what I've seen, depending on the font. So it is not big - but throws off my calculations.

UPDATE: Tested a few mobile browsers too - and on iOS all are on the same level as Safari (using WebKit under the hood, so no suprise) - and Firefox on Android is very on and off.

I've read that subpixel accuracy isn't really supported across all browsers (older IE's for example) - but even rounding doesn't help - as I then can end up having different width.

Using no webfont but just the standard font the context comes with returns the exact same measurements between Chrome and Safari - so I think it is related to webfonts only.

I am a bit puzzled of what I might be able to do now - as I think I just do something wrong as I haven't found anything on the net around this - but the fiddle is as simple as it can get. I have spent the entire day on this really - so you guys are my only hope now.

I have a few ugly workarounds in my head (e.g. rendering the text on affected browsers 4% smaller) - which I would really like to avoid.


Solution

  • It seems that Safari (and a few others) does support getting at sub-pixel level, but not drawing...

    When you set your font-size to 9.5pt, this value gets converted to 12.6666...px.

    Even though Safari does return an high precision value for this:

    console.log(getComputedStyle(document.body)['font-size']);
    // on Safari returns 12.666666984558105px oO
    body{font-size:9.5pt}

    it is unable to correctly draw at non-integer font-sizes, and not only on a canvas:

    console.log(getRangeWidth("S", '12.3px serif'));
    // safari: 6.673828125 | FF 6.8333282470703125
    console.log(getRangeWidth("S", '12.4px serif'));
    // safari: 6.673828125 | FF 6.883331298828125
    console.log(getRangeWidth("S", '12.5px serif'));
    // safari 7.22998046875 | FF 6.95001220703125
    console.log(getRangeWidth("S", '12.6px serif'));
    // safari 7.22998046875 | FF 7
    
    // High precision DOM based measurement
    function getRangeWidth(text, font) {
      var f = font || '12px arial',
          o = $('<span>' + text + '</span>')
                .css({'font': f, 'white-space': 'nowrap'})
                .appendTo($('body')),
          r = document.createRange();
     r.selectNode(o[0]);
     var w = r.getBoundingClientRect().width;
     o.remove();
     return w;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

    So in order to avoid these quirks, Try to always use px unit with integer values.