EDIT: originally I checked only desktop browsers - but with mobile browsers, the picture is even more plicated.
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 pared.
I have used the two mon 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):
/ (updated to force font-weight and style)
Running this on various browsers shows the problem I am having (using 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 es 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.
EDIT: originally I checked only desktop browsers - but with mobile browsers, the picture is even more plicated.
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 pared.
I have used the two mon 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/aj7v5e4L/15/ (updated to force font-weight and style)
Running this on various browsers shows the problem I am having (using 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 es 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.
Share Improve this question edited Oct 10, 2017 at 8:17 Michael asked Oct 9, 2017 at 19:20 MichaelMichael 1031 silver badge6 bronze badges 2-
Your results table was made with the string
"S"
? If so I've got15.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 Commented Oct 9, 2017 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 Commented Oct 10, 2017 at 7:23
2 Answers
Reset to default 3It 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./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.
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/en-US/docs/Web/API/TextMetrics#Measuring_text_width
const putetextWidth = (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));
}