4

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.

Michael
  • 93
  • 1
  • 6
  • Your results table was made with the string `"S"`? If so I've got `15.73333..` on firefox for android on the first font, which is closer to your safari results than to any other. Don't have access to a real keyboard for now, but what happens when you force the font-weight? – Kaiido Oct 09 '17 at 23:27
  • Yes, was created with the string "S" - didn't even touch the realm of mobile browsers - that might open up even more issues ;) - will try the font-weight – Michael Oct 10 '17 at 07:23

2 Answers2

3

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.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Awesome - have had a quick look and with integer font sizes in pixels it does indeed seem to be consistent (need to make a few more checks). A new fiddle demonstrates that and allows to edit the font size on the fly: http://jsfiddle.net/aj7v5e4L/17/ Thank you so much Kaiido, that really helped. – Michael Oct 10 '17 at 16:31
1

I found below solution from MDN more helpful for scenarios where fonts are slanted/italic which was for me the case with some google fonts

copying the snippet from here - https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics#Measuring_text_width

const computetextWidth = (text, font) => {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');

    context.font = font;
    const { actualBoundingBoxLeft, actualBoundingBoxRight } = context.measureText(text);
    return Math.ceil(Math.abs(actualBoundingBoxLeft) + Math.abs(actualBoundingBoxRight));
}
Vinujan.S
  • 1,199
  • 14
  • 27