From 1ad823cafdb300ec228b31a63c2e868cb9d39c50 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 21 Aug 2026 19:34:05 +0200 Subject: [PATCH] Account for the Firefox font size quantization in the text layer Firefox rounds the font size set on a canvas 2d context, once divided by the device pixel ratio, to 7 bits of precision. Hence `measureText` can return a width up to ~1% off the one the spans are laid out with, which made `--scale-x` slightly wrong in a zoom dependent way. Measure with a size left untouched by that rounding and rescale the returned width, which is linear in the font size. --- src/display/text_layer.js | 32 ++++++++++++++++++++++++++------ test/text_layer_test.css | 4 +++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/display/text_layer.js b/src/display/text_layer.js index ef67ae5af33f4..264b4fb2245c8 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -69,6 +69,8 @@ class TextLayer { #pageWidth = 0; + #pixelRatio = OutputScale.pixelRatio; + #reader = null; #rootContainer = null; @@ -122,7 +124,7 @@ class TextLayer { this.#imagesHandler = images; - this.#scale = viewport.scale * OutputScale.pixelRatio; + this.#scale = viewport.scale * this.#pixelRatio; this.#rotation = viewport.rotation; this.#layoutTextParams = { div: null, @@ -229,6 +231,7 @@ class TextLayer { if (scale !== this.#scale) { onBefore?.(); this.#scale = scale; + this.#pixelRatio = OutputScale.pixelRatio; const params = { div: null, properties: null, @@ -424,17 +427,26 @@ class TextLayer { #layout(params) { const { div, properties, ctx } = params; const { style } = div; + const { canvasWidth, fontSize } = properties; - if (properties.canvasWidth !== 0 && properties.hasText) { + if (canvasWidth !== 0 && fontSize !== 0 && properties.hasText) { const { fontFamily } = style; - const { canvasWidth, fontSize } = properties; - - TextLayer.#ensureCtxFont(ctx, fontSize * this.#scale, fontFamily); + // Firefox quantizes canvas font sizes after dividing by the device-pixel + // ratio. Measure at a quantized size, then rescale the width. + const pixelRatio = this.#pixelRatio; + const measuredSize = + TextLayer.#quantizeFontSize((fontSize * this.#scale) / pixelRatio) * + pixelRatio; + + TextLayer.#ensureCtxFont(ctx, measuredSize, fontFamily); // Only measure the width for multi-char text divs, see `appendText`. const { width } = ctx.measureText(div.textContent); if (width > 0) { - style.setProperty("--scale-x", (canvasWidth * this.#scale) / width); + style.setProperty( + "--scale-x", + (canvasWidth * measuredSize) / (width * fontSize) + ); } } if (properties.angle !== 0) { @@ -489,6 +501,14 @@ class TextLayer { return ctx; } + // Match Firefox's 7-bit `QuantizeFontSize` implementation: + // https://searchfox.org/firefox-main/rev/04b29f9c2d2dbf5639c3f45ea812bb4c21dc81c6/dom/canvas/CanvasRenderingContext2D.cpp#4205-4215 + static #quantizeFontSize(size) { + size = Math.fround(size); + const d = Math.fround(size * ((1 << 17) + 1)); + return Math.fround(d - Math.fround(d - size)); + } + static #ensureCtxFont(ctx, size, family) { const cached = this.#canvasCtxFonts.get(ctx); if (size === cached.size && family === cached.family) { diff --git a/test/text_layer_test.css b/test/text_layer_test.css index df0d5859bf1ae..bd499da23d99a 100644 --- a/test/text_layer_test.css +++ b/test/text_layer_test.css @@ -26,7 +26,9 @@ position: absolute; white-space: pre; transform-origin: 0% 0%; - border: solid 1px rgb(255 0 0 / 0.5); + + /* Gecko may drop transformed 1px borders during rasterization. */ + border: solid 2px rgb(255 0 0 / 0.5); background-color: rgb(255 255 32 / 0.1); box-sizing: border-box; }