Canvas.measureText differences on browsers are huge - javascript

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'):
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.

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.

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));
}

Related

Different font widths in html5-canvas

i have a pretty simple but very annoying problem. I try to read the width of a string in a canvas. I know how to do this, and it works. But the results differ from browser to browser.
ctx.font = "10px Arial";
var txt = "This is a text demonstration. Why is the width of this text different in every browser??";
ctx.fillText("width:" + ctx.measureText(txt).width, 10, 50);
ctx.fillText(txt, 10, 100);
Here a little fiddle: http://jsfiddle.net/83v7c4jv/
Chrome: 390px, IE: 375px, Firefox: 394px. Only IE is accurate, since C# gives me the same result if i try this there. Does anybody know why and how i can get Chrome and Firefox to render and calculate the same values like IE?
you need to read this answer
it worked for me in some project , i used this code to get height of the text because it not exist it will work the same as text width
obj.lineHeight = function(){
var testDiv = document.createElement('div'); // creating div to measure text in
testDiv.style.padding= "0px";
testDiv.style.margin = "0px";
testDiv.style.backgroundColor = "white";
testDiv.textContent = "Hello World !";
testDiv.style.fontSize = obj.size+"px";
testDiv.style.fontFamily = obj.fontFamily;
testDiv.style.clear = "both";
testDiv.style.visibility="hidden";
document.body.appendChild(testDiv);
var __height__ = testDiv.clientHeight;
testDiv.style.display = "none";
document.body.removeChild(testDiv);
return __height__ ;
};
First of all, the same font may be rendered different in different browsers. Pay attention on the following picture. I just putted together screenshots of your JSFIDDLE running on Chrome (the first) and IE (the second). As you can see, the text width actually is not the same, and the numbers that ctx.measureText returns are correct in the both cases.
C# gives you the same number as IE because they use the same text rendering algorithm, but it has no meaning when your page runs on other browser.
You can found some tricks and hacks in this thread, but in fact you cannot really control the browser rendering mechanism. If you want to ensure your text to be shown exactly the same on all the browsers and only way is to turn it into an image.

Detecting browser supports SVG stacking

SVG stacking is a technique for stuffing multiple SVG images (like icons) into a single file, to enable downloading sets of icons in a single HTTP request. It's similar to a PNG sprite, except much easier to change/maintain.
The SVG to display is selected using the # fragment identifier in the SVG url. The technique is explained here.
While this technique is arguably on shaky grounds in terms of browser support, (and Chrome doesn't support it all in CSS background-image) it works surprisingly well in most browsers if done using an <img> tag. It works in IE9+, Chrome, and Firefox as an <img> tag, so a fallback to PNG is only required if you need to support much older browsers like IE8.
Except... Safari is a bit of a problem. Even though Safari supports SVGs back to version 5 and below, SVG stacking just doesn't work in versions < 7.1. A blank space is displayed where the icon should be.
So, as of now a fallback is probably necessary. But how can we use feature detection to determine whether we need to fallback to PNG sprites (or at least hide the SVG icon so that a blank space doesn't appear.) ?
The various articles discussing SVG stacks talk about providing fallback for browsers which don't support SVGs. Essentially, the most common technique is to simply use Modernizer to detect if SVGs are supported, and if not, use PNGs, as demonstrated here.
But as far as I can see, nobody is discussing the case where a browser DOES support SVGs, but doesn't support SVG stacking. And as far as I know, at least Safari 5 thru 7.0 fall into that category: these browsers support SVGs, but apparently don't support the :target pseudo selector that enables SVG stacking to work.
So how can this condition be detected? Do we have to rely on user agent sniffing here?
Interesting question!
In general, browser cannot answer regarding a feature it doesn't know about. However, some trick came to my mind.
When the image is OK it means that the pixels in it are different, right? And if we see a blank space it means, that all the pixels in it are the same, doesn't matter if they are white, transparent or something else.
So, we can load an image into canvas, take the first pixel and compare the rest with it. If somehing different is found, so the feature is supported, otherwise not. Something like the following code:
function isSVGLayersSupported(img)
{
// create canvas and draw image to it
var canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext("2d").drawImage(img, 0, 0);
// get cancas context and image data
var ctx = canvas.getContext("2d");
var imageData = ctx.getImageData(0, 0, img.width, img.height);
// Processing image data
var data = imageData.data;
var firstR = data[0];
var firstG = data[1];
var firstB = data[2];
var firstAlpha = data[3];
for (var i = 4; i < data.length; i += 4) {
if ((data[i] != firstR) ||
(data[i+1] != firstG) ||
(data[i+2] != firstB) ||
(data[i+3] != firstAlpha))
return true;
}
return false;
}

What is the effective granularity of font size in html5 canvas text?

I've searched exhaustively for an answer, and I'm hoping that stackoverflow will come to the rescue once again.
If we set the font size of a context, it seems to allow arbitrary precision:
fontSize = 11.654321;
context.font = 'bold ' + fontSize + 'px sans-serif ';
Inspecting the context object will show that font size has been set with extreme precision. I have noticed, however, that the measureText() method of the context object always returns an integer value (presumably a ceiling value). Does anyone know the actual precision used in displaying text (when using pixel-based font sizes)?
A link to documentation containing this information would be sufficient (as long as it actually informs about precision).
In case anyone asks, I'm trying to adjust the font to fit text to a given width by doing something like this:
var fontSize = 12;
context.font = 'bold ' + fontSize + 'px sans-serif ';
var text = "whatever";
var maxWidth = 200;
var currentWidth = context.measureText(text).width;
var adjustment = maxWidth / currentWidth;
fontSize *= adjustment;
context.font = 'bold ' + fontSize " 'px sans-serif ';
For Chromium and Firefox, it appears that the effective precision is at the integer level (I have not tested Internet Explorer). I've created this jsFiddle to research the issue, and it appears from my research that in Chromium and Firefox, font sizes specified with sub-integer precision are effectively rounded to the nearest integer.
The tests I've done assume that the measureText() method is functioning correctly (which may be a false assumption). The tests have shown some odd behavior for this method (more obvious in Chromium than in Firefox).
Using a fixed length string of a repeated character ('m'), I have graphed the measured size of the string as a function of its font size. When using 0.1 for the font size input step size, the output string width remains constant for step sizes of 1.0. These "steps" are centered around the integer font size, and jumps occur at midpoints between integers.
The odd behavior observed in Chromium is that at times, the input font size can be varied as much as 2 pixels, with no change in the measured width of the string. This behavior is unexpected, but occurs at regular intervals (offset from zero). The following intervals of input values resulted in no change in measured width: [3.5, 5.5), [12.5, 14.5), [21.5, 23.5), [30.5, 32.5), and so on. The centers of these "big flat steps" seem to be 9 pixels apart (4.5, 13.5, 22.5, 31.5). The first one having a center around half the spacing of the interval between the rest maybe a clue if this is indeed a bug.
In Firefox, the odd behavior is much more subtle. If you change the input step size to 1, the graph is nearly linear (as expected).. but periodically the graph gets slightly steeper for an interval of 1. For example, this occurs at [11.5, 12.5) and [35.5, 36.5).
If I am overlooking something, or if there is some error in my methods, please comment and let me know. I am not too concerned about the Firefox behavior, but as of now, I consider the behavior in Chromium to be a bug. I may file a bug report, and if so, I will update my answer to include a link.

canvasContext.fillRect throws NS_ERROR_FAILURE exception in Firefox

I'm trying to draw a huge canvas rectangle on top of the page (some kind of lightbox background), the code is quite straightforward:
var el = document.createElement('canvas');
el.style.position = 'absolute';
el.style.top = 0;
el.style.left = 0;
el.style.zIndex = 1000;
el.width = window.innerWidth + window.scrollMaxX;
el.height = window.innerHeight + window.scrollMaxY;
...
document.body.appendChild(el);
// and later
var ctx = el.getContext("2d");
ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
ctx.fillRect(0, 0, el.width, el.height);
And sometimes (not always) the last line throws:
Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIDOMCanvasRenderingContext2D.fillRect]
I've been guessing if that happens because of image size or because of the content type beneath the canvas (e.g. embeded video playing), but apparently not.
So I'm looking for any ideas on how to isolate and/or solve this issue.
Looking at the nsIDOMCanvasRenderingContext2D.fillRect() implementation (and going through the functions it calls) - there aren't all too many conditions that will return NS_ERROR_FAILURE. It can only happen if either EnsureSurface() or mThebes->CopyPath() fail. And the following two lines in EnsureSurface() are most likely the source of your issue:
// Check that the dimensions are sane
if (gfxASurface::CheckSurfaceSize(gfxIntSize(mWidth, mHeight), 0xffff)) {
What's being checked here:
Neither the width nor the height of the canvas can exceed 65535 pixels.
The height cannot exceed 32767 pixels on Mac OS X (platform limitation).
The size of canvas data (width * height * 4) cannot exceed 2 GB.
If any of these conditions is violated EnsureSurface() will return false and consequently produce the exception you've seen. Note that the above are implementation details that can change at any time, you shouldn't rely on them. But they might give you an idea which particular limit your code violates.
You could apply a try-catch logic. Firefox seems to be the only browser which behaves this a bit odd way.
el.width = window.innerWidth + window.scrollMaxX;
el.height = window.innerHeight + window.scrollMaxY;
// try first to draw something to check that the size is ok
try
{
var ctx = el.getContext("2d");
ctx.fillRect(0, 0, 1, 1);
}
// if it fails, decrease the canvas size
catch(err)
{
el.width = el.width - 1000;
el.height = el.height - 1000;
}
I haven't found any variable that tells what is the maximum canvas size. It varies from browser to browser and device to device.
The only cross browser method to detect the maximum canvas size seems to be a loop that decreases the canvas size eg. by 100px until it doesn't produce the error. I tested a loop, and it is rather fast way to detect the maximum canvas size. Because other browsers doesn't throw an error when trying to draw on an over sized canvas, it is better to try to draw eg. red rect and read a pixel and check if it is red.
To maximize detection performance:
- While looping, the canvas should be out of the DOM to maximize speed
- Set the starting size to a well known maximum which seems to be 32,767px (SO: Maximum size of a <canvas> element)
- You can make a more intelligent loop which forks the maximum size: something like using first bigger decrease step (eg.1000px) and when an accepted size is reached, tries to increase the size by 500px. If this is accepted size, then increase by 250px and so on. This way the maximum should be found in least amount of trials.

Get hex value of clicked on color with jQuery

I want to know how to make a color picker with jQuery that will allow you to click somewhere on the page and return the hex color value of the color that you clicked on.
I know that it is possible with either javascript or jquery as not only do they have lots of color picker plugins, but I have and extension for Chrome that has that same exact functionality.
Any ideas?
Bind a global click or mouseup event listener. Then, use canvas to obtain the pixel information. The pixel positions can be retrieved through the event object (event.pageX, event.pageY).
See below for an example, which should work in future versions of FireFox. Currently, for security reasons, the drawWindow method is disabled for web pages. It should work in extensions, though. If you're truly interested, see the links for the similar methods in Chrome, Safari and Internet Explorer.
var canvas = $("<canvas>"); //Create the canvas element
//Create a layer which overlaps the whole window
canvas.css({position:"fixed", top:"0", left:"0",
width:"100%", height:"100%", "z-index":9001});
//Add an event listener to the canvas element
canvas.click(function(ev){
var x = ev.pageX, y = ev.pageY;
var canvas = this.getContext("2d");
canvas.drawWindow(window, x, y, 1, 1, "transparent");
var data = canvas.getImageData(0, 0, 1, 1).data;
var hex = rgb2hex(data[0], data[1], data[2]);
alert(hex);
$(this).remove();
});
canvas.appendTo("body:first"); //:first, in case of multiple <body> tags (hmm?)
//Functions used for conversion from RGB to HEX
function rgb2hex(R,G,B){return num2hex(R)+num2hex(G)+num2hex(B);}
function num2hex(n){
if (!n || !parseInt(n)) return "00";
n = Math.max(0,Math.floor(Math.round(n),255)).toString(16);
return n.length == 1 ? "0"+n : n;
}
References
Canvas examples - Learn more about canvas
drawWindow - FireFox method
visibleContentAsDataURL - Safari extensions
chrome.tabs.captureVisibleTab - Chrome extensions
HTA ActiveX control - Internet Explorer
Those plugins don't work by knowing the color of the pixel under the mouse; they work because the colors in the picker are laid out according to a mathematical formula, and by knowing that formula and where you clicked the mouse, the plugin can find out what color belongs there. JavaScript doesn't give you any way to get an image of the page or the "color under the cursor".
I had this problem recently, by default IE returns hex colours whereas all the good browsers return rgb values, I simply put conditions to deal with both strings, this would be more efficient..
however I think this function does the trick if you really need it
function RGBToHex(rgb) {
var char = "0123456789ABCDEF";
return String(char.charAt(Math.floor(rgb / 16))) + String(char.charAt(rgb - (Math.floor(rgb / 16) * 16)));
}

Categories