In canvas@4.0.0-rc1 ( PR #2602 ), ctx.measureText() does not count the advance of trailing whitespace. In consequence:
- A whitespace-only string (
" ", "\t") measures as width: 0.
" " (two spaces) measures as one space's width.
"word " measures barely wider than "word" (the trailing space advance is mostly dropped).
- A leading space is fine:
" word" correctly measures as space + word.
v3.x counted all whitespace advances, matching browser behavior (Chrome/Firefox/Safari all return the real space advance for measureText(" "), per the spec's definition of text advance width — the sum of glyph advances, with no trailing-whitespace trimming).
This breaks a common layout technique: tokenizing a string into words and spaces, measuring each token separately, and advancing a cursor by each token's width (e.g. word-wrap with per-word styling, like rich-text/multi-font labels). On v4 every isolated space token measures 0, so all words render butted together — while the same code renders correctly in browsers and on canvas v3.
Steps to reproduce
const { createCanvas } = require("canvas");
const ctx = createCanvas(100, 100).getContext("2d");
ctx.font = "36px sans-serif";
console.log(ctx.measureText(" ").width); // v4: 0 v3: ~10
console.log(ctx.measureText(" ").width); // v4: ~10 (!) v3: ~20
console.log(ctx.measureText("\t").width); // v4: 0 v3: ~80
Measured comparison (same machine, 36px sans-serif)
| Input |
v3.2.3 |
v4.0.0-rc1 |
Browser-consistent? |
" " |
10.00 |
0 |
❌ |
" " |
20.00 |
10.01 |
❌ |
"\t" |
80.02 |
0 |
❌ |
"word" |
78.03 |
75.67 |
— (font engine changed, fine) |
"word " |
88.03 |
78.01 |
❌ (space adds only ~2.3) |
" word" |
88.03 |
85.68 |
✅ (leading space kept) |
"a b" − "ab" |
10.00 |
10.01 |
✅ (interior space kept) |
The pattern: the advance of whitespace at the end of the measured string is dropped.
Full TextMetrics for " " on v4 — width and all actualBoundingBox* fields are 0, only the em-height fields are populated:
{
"width": 0,
"actualBoundingBoxLeft": 0,
"actualBoundingBoxRight": 0,
"actualBoundingBoxAscent": 0,
"actualBoundingBoxDescent": 0,
"emHeightAscent": 27.72,
"emHeightDescent": -8.28,
"alphabeticBaseline": 0
}
Environment
- canvas: 4.0.0-rc1 (prebuilt binary from npm,
canvas-darwin-arm64)
- Node.js: v22.22.2
- OS: macOS 15.7.7 (Darwin 24.6.0), Apple Silicon (arm64)
- Baseline: canvas 3.2.3 on the same machine
Expected behavior
Match v3 / the HTML spec / browsers: measureText returns the full advance width including trailing whitespace, so:
measureText(" ").width > 0
measureText("word ") ≈ measureText("word") + measureText(" ")
Possibly related
I also reported the empty-string SIGTRAP crash (measureText("") aborts the process) — both look like the new text layout path treating degenerate/whitespace paragraphs specially, so they may share a fix.
In
canvas@4.0.0-rc1( PR #2602 ),ctx.measureText()does not count the advance of trailing whitespace. In consequence:" ","\t") measures aswidth: 0." "(two spaces) measures as one space's width."word "measures barely wider than"word"(the trailing space advance is mostly dropped)." word"correctly measures as space + word.v3.x counted all whitespace advances, matching browser behavior (Chrome/Firefox/Safari all return the real space advance for
measureText(" "), per the spec's definition of text advance width — the sum of glyph advances, with no trailing-whitespace trimming).This breaks a common layout technique: tokenizing a string into words and spaces, measuring each token separately, and advancing a cursor by each token's width (e.g. word-wrap with per-word styling, like rich-text/multi-font labels). On v4 every isolated space token measures 0, so all words render butted together — while the same code renders correctly in browsers and on canvas v3.
Steps to reproduce
Measured comparison (same machine,
36px sans-serif)" "" ""\t""word""word "" word""a b"−"ab"The pattern: the advance of whitespace at the end of the measured string is dropped.
Full
TextMetricsfor" "on v4 —widthand allactualBoundingBox*fields are 0, only the em-height fields are populated:{ "width": 0, "actualBoundingBoxLeft": 0, "actualBoundingBoxRight": 0, "actualBoundingBoxAscent": 0, "actualBoundingBoxDescent": 0, "emHeightAscent": 27.72, "emHeightDescent": -8.28, "alphabeticBaseline": 0 }Environment
canvas-darwin-arm64)Expected behavior
Match v3 / the HTML spec / browsers:
measureTextreturns the full advance width including trailing whitespace, so:measureText(" ").width > 0measureText("word ") ≈ measureText("word") + measureText(" ")Possibly related
I also reported the empty-string SIGTRAP crash (
measureText("")aborts the process) — both look like the new text layout path treating degenerate/whitespace paragraphs specially, so they may share a fix.