Skip to content

v4.0.0-rc1: measureText collapses trailing/isolated whitespace to zero width (regression from v3, diverges from browsers) #2605

Description

@lvcabral

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions