Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

measureText differs significantly from browser implementations #331

Open
kangax opened this issue Sep 10, 2013 · 14 comments
Open

measureText differs significantly from browser implementations #331

kangax opened this issue Sep 10, 2013 · 14 comments

Comments

@kangax
Copy link
Collaborator

kangax commented Sep 10, 2013

Even though node-canvas now supports font registering and more precise font rendering, I noticed measureText still returns "significantly" different result comparing to some of the popular browser implementations.

For example, rendering "Awesome!" string in "30px Arial" gives me this width:

  • node-canvas 1.1.0: 137
  • FF 18, 26: 139.5
  • IE9, IE10, IE11: 140
  • Opera 12: 141
  • Chrome 31, WebKit, Opera 15: 141

All browsers differ by <=1.5px while node-canvas is 2.5-4px away.

Is there a way to move this somehow closer to other browsers range? In this particular case, anything from 139 to 141 would already be better. 140-141 would be ideal.

Simple example used for testing:

var Canvas = require('canvas')
  , canvas = new Canvas(300, 300)
  , ctx = canvas.getContext('2d')
  , Font = Canvas.Font;

var font = new Font('Arial', __dirname + '/Arial.ttf');
ctx.addFont(font);

ctx.font = '30px Arial';
ctx.fillText("hello world", 0, 50);

console.log(ctx.measureText('Awesome!').width);
@jakeg
Copy link
Contributor

jakeg commented Feb 19, 2015

See also #472

@jakeg
Copy link
Contributor

jakeg commented Feb 19, 2015

Hmm, I've done a bit of experimenting and have found what seems like it might be consistent across different browsers (latest Chrome, Firefox and IE tested on Windows so far). I haven't checked node-canvas yet but I'm expecting (hoping?) it'll be consistent in that too. Here's what I'm doing at the moment to get consistent measurements:

  • rather than measureText('Whole string') measure each character separately and add them together
  • before adding them together, round each one separately

Before:

ctx.measureText(str).width

After:

for (var i=0; i<str.length; ++i) {
  added_rounded_w += Math.round(ctx.measureText(str[i]).width);
}

Just measuring each character separately isn't enough - you need to round them too, as that's what IE does so if you want consistency with IE you need to do that in all browsers.

Also, I've found it doesn't work with small font sizes. From patchy tests I've done so far I haven't had one string fail being absolutely consistently measured across these 3 browsers as long as the font size is at least 16px. More tests needed though. For smaller font sizes it might make sense to measure text at a larger (say x2) size then halve the result to get the consistency needed.

I've done these tests on various latin strings so far with spaces and special characters thrown in, of various lengths. The consistency seems to hold true even for paragraph-long strings.

I'll update again tomorrow once I've tested for consistency in node-canvas and made my tests more thorough.

@jakeg
Copy link
Contributor

jakeg commented Feb 20, 2015

A few more tests just done - my findings from the last post seem to hold for latest Chrome and Safari on OSX as well as Chrome and Safari on iPad, Safari on iPod Touch.

Unfortunately doesn't hold for Chrome 38 on Android Nexus 7 or Chrome 39 or Firefox on Android Moto G :( Though this may be due to lack of used font - going to try using a webfont to see if that fixes the issue there.

...Ok, so using a web font and waiting for it to load and I'm now getting consistent results in everything listed above except for Firefox on Android Moto G. Need to extend the tests further but this is very promising. Next to try node-canvas too.

... node-canvas results not very promising at all so far for consistency against this, and results are wildly different for PNG vs PDF/SVG output too. Going to try hacking rounded ints into doubles in pango bindings in case that helps.

... correct hack to pango node-canvas bindings at least gives same output for PNG vs PDF/SVG output (PDF and SVG change as a result, PNG stays the same):

pango_layout_get_extents(layout, &ink_rect, &logical_rect); // pango_layout_get_pixel_extents will round which we don't want
double precision_width = (double) logical_rect.width/PANGO_SCALE; // PANGO_UNITS is 1024

Next to try changing pango bindings to see if any of ceil/floor/round give results consistent with the browsers (already much closer though)...

... no luck, and running out of ideas to get node-canvas to match the browsers :(

@crazyyi
Copy link

crazyyi commented Dec 15, 2016

@jakeg Thanks your solution is indeed much better than the vanilla measureText function.

@zbjornson
Copy link
Collaborator

Some updated numbers (Win10):

  • node-canvas 2.x: 137 (same as OP)
  • FF 64a1: 139.5 (same as OP)
  • Chrome 69: 139.51171875 (1.5 less than OP)
  • Safari 11.1: 139.51171875 (1.5 less than OP)

At least the browsers have converged on a right answer. Node-canvas is still off.


Measuring characters individually, rounding and summing their widths shouldn't be the right approach. That will defeat kerning and ligatures. Below demonstrates the effect of kerning (output from Chrome):

> [ctx.measureText("A").width, ctx.measureText("V").width]
[17.33203125, 17.33203125]
ctx.measureText("AV")
TextMetrics {width: 31.5703125} // not 34.66

@zbjornson
Copy link
Collaborator

This looks like a difference in pango vs. the layout engine browsers use for actual text rendering. That is, the TextMetrics themselves look correct.

The red rectangle is the width according to node-canvas, and blue according to Chrome; ran in Chrome:

image

With some reference lines drawn:

image

@darkowic
Copy link

@zbjornson The image you posted above is an example of font kerning - https://en.wikipedia.org/wiki/Kerning

@kmcclellan
Copy link

kmcclellan commented Jun 17, 2021

the TextMetrics themselves look correct

@zbjornson Just to be clear, you mean that the metrics match the dimensions of the text in Pango? The problem (and the reason this issue is still open) is that text in Pango has different dimensions/kerning than the same font rendered in a browser?

I don't suppose they will differ consistently in a way that we can predict? Is there a width factor I can scale by so that the Node canvas generates text that more closely matches text drawn by a browser? I'm using Node to generate HTML for browser consumption and any help is appreciated!

@kmcclellan
Copy link

I've had some success in finding constant factors to adjust measured text values. However, you have to take into account the font itself, the font size, and (to some extent) the target browser. Safari/Chrome/Edge are fairly synoptic in their renderings, with Firefox as the oddball. See the attached measurements for Times New Roman, Arial, and Courier New. Here is the script I used to calculate these values:

// NODE: var ctx = new (require('canvas').Canvas)().getContext('2d');
// BROWSER: var ctx = document.createElement('canvas').getContext('2d');

// From https://www.typography.com/blog/text-for-proofing-fonts
var text = 'Angel Adept Blind Bodice Clique Coast Dunce Docile Enact Eosin Furlong Focal Gnome Gondola Human Hoist Inlet Iodine Justin Jocose Knoll Koala Linden Loads Milliner Modal Number Nodule Onset Oddball Pneumo Poncho Quanta Qophs Rhone Roman Snout Sodium Tundra Tocsin Uncle Udder Vulcan Vocal Whale Woman Xmas Xenon Yunnan Young Zloty Zodiac. Angel angel adept for the nuance loads of the arena cocoa and quaalude. Blind blind bodice for the submit oboe of the club snob and abbot. Clique clique coast for the pouch loco of the franc assoc and accede. Dunce dunce docile for the loudness mastodon of the loud statehood and huddle. Enact enact eosin for the quench coed of the pique canoe and bleep. Furlong furlong focal for the genuflect profound of the motif aloof and offers. Gnome gnome gondola for the impugn logos of the unplug analog and smuggle. Human human hoist for the buddhist alcohol of the riyadh caliph and bathhouse. Inlet inlet iodine for the quince champion of the ennui scampi and shiite. Justin justin jocose for the djibouti sojourn of the oranj raj and hajjis. Knoll knoll koala for the banknote lookout of the dybbuk outlook and trekked. Linden linden loads for the ulna monolog of the consul menthol and shallot. Milliner milliner modal for the alumna solomon of the album custom and summon. Number number nodule for the unmade economic of the shotgun bison and tunnel. Onset onset oddball for the abandon podium of the antiquo tempo and moonlit. Pneumo pneumo poncho for the dauphin opossum of the holdup bishop and supplies. Quanta quanta qophs for the inquest sheqel of the cinq coq and suqqu. Rhone rhone roman for the burnt porous of the lemur clamor and carrot. Snout snout sodium for the ensnare bosom of the genus pathos and missing. Tundra tundra tocsin for the nutmeg isotope of the peasant ingot and ottoman. Uncle uncle udder for the dunes cloud of the hindu thou and continuum. Vulcan vulcan vocal for the alluvial ovoid of the yugoslav chekhov and revved. Whale whale woman for the meanwhile blowout of the forepaw meadow and glowworm. Xmas xmas xenon for the bauxite doxology of the tableaux equinox and exxon. Yunnan yunnan young for the dynamo coyote of the obloquy employ and sayyid. Zloty zloty zodiac for the gizmo ozone of the franz laissez and buzzing.';

var fonts = [
  'Times New Roman',
  'Arial',
  'Courier New',
];

var result = "";

for (var font of fonts) {
  for (var size = 1; size <= 72; size++) {
    ctx.font = `${size}px ${font}`;
    var metrics = ctx.measureText(text);
    result += `${font},${size},${Math.round(metrics.width)}\n`;
  }
}

console.log(result);

Canvas Font Widths.xlsx

@barisbabahan
Copy link

barisbabahan commented Jun 28, 2022

Might be not a related question but I'm trying to measure text in the NextJS application and the canvas return different text width after the build. We are using webpack to build there is 5-8px diffrence local and after build. Is there any reason for it couldn't find any solution

@eurostar-fennec-cooper
Copy link

Might be not a related question but I'm trying to measure text in the NextJS application and the canvas return different text width after the build. We are using webpack to build there is 5-8px diffrence local and after build. Is there any reason for it couldn't find any solution

Kerning seems to be slightly different per platform for some reason, even within node-canvas itself. We have been having issues with very minor differences between text generated on a Macbook vs text generated in CI.

@BrentFarese
Copy link

We seem to have this issue but are experiencing different values between Node and the Browser. It's causing an issue b/c we use measureText to measure the width of text in Node and the Browser for producing PDFs in both environments. The Node environment the PDF is just wrong in a bunch of places due to small width discrepancies b/t Node and the Browser.

What is the solution to cross-browser consistent text width measuring, and also in Node? Is there another library that will at least produce consistent results (perhaps something that doesn't rely on measureText)?

@zbjornson
Copy link
Collaborator

@BrentFarese are the text metrics from node-canvas actually wrong, or do they just not exactly match the browser you're using? See #331 (comment) for example...

@BrentFarese
Copy link

@zbjornson yes that comment looks consistent with what we’re seeing. The problem really is the inconsistency between the Browser and Node. It’d be good if there was an option to use Pango in the Browser so at least there is consistency.

To “fix” the inconsistency, we actually used a text measurement function from another library, jsPDF (https:/parallax/jsPDF). That library’s text measurements are consistent with the Browser even when run in Node. Maybe their measurement is better than Pango?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants