From c6a3bbd066fb0206c3ed3bcbda8802198fe226c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Tue, 18 Jun 2024 20:02:36 +0200 Subject: [PATCH] Overrride the minimum font size when rendering the text layer Browsers have an accessibility option that allows user to enforce a minimum font size for all text rendered in the page, regardless of what the font-size CSS property says. For example, it can be found in Firefox under `font.minimum-size.x-western`. When rendering the s in the text layer, this causes the text layer to not be aligned anymore with the underlying canvas. While normally accessibility features should not be worked around, in this case it is *not* improving accessibility: - the text is transparent, so making it bigger doesn't make it more readable - the selection UX for users with that accessibility option enabled is worse than for other users (it's basically unusable). While there is tecnically no way to ignore that minimum font size, this commit does it by multiplying all the `font-size`s in the text layer by minFontSize, and then scaling all the ``s down by 1/minFontSize. --- src/display/text_layer.js | 44 +++++++++++++++++++++++++--- test/integration/text_layer_spec.mjs | 39 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/display/text_layer.js b/src/display/text_layer.js index 1c0d247779131d..d2858533f8a6f0 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -83,6 +83,8 @@ class TextLayer { static #canvasContexts = new Map(); + static #minFontSize = null; + static #pendingTextLayers = new Set(); /** @@ -114,6 +116,7 @@ class TextLayer { div: null, properties: null, ctx: null, + minFontSize: null, }; const { pageWidth, pageHeight, pageX, pageY } = viewport.rawDims; this.#transform = [1, 0, 0, -1, -pageX, pageY + pageHeight]; @@ -196,6 +199,7 @@ class TextLayer { div: null, properties: null, ctx: TextLayer.#getCtx(this.#lang), + minFontSize: TextLayer.#getMinFontSize(), }; for (const div of this.#textDivs) { params.properties = this.#textDivProperties.get(div); @@ -242,7 +246,8 @@ class TextLayer { if (this.#disableProcessItems) { return; } - this.#layoutTextParams.ctx ||= TextLayer.#getCtx(this.#lang); + this.#layoutTextParams.ctx ??= TextLayer.#getCtx(this.#lang); + this.#layoutTextParams.minFontSize ??= TextLayer.#getMinFontSize(); const textDivs = this.#textDivs, textContentItemsStr = this.#textContentItemsStr; @@ -326,7 +331,11 @@ class TextLayer { divStyle.left = `${scaleFactorStr}${left.toFixed(2)}px)`; divStyle.top = `${scaleFactorStr}${top.toFixed(2)}px)`; } - divStyle.fontSize = `${scaleFactorStr}${fontHeight.toFixed(2)}px)`; + // We multiply the font size by #getMinFontSize(), and then #layout will + // scale the element by 1/#getMinFontSize(). This allows us to effectively + // ignore the minimum font size enforced by the browser, so that the text + // layer s can always match the size of the text in the canvas. + divStyle.fontSize = `${scaleFactorStr}${(TextLayer.#getMinFontSize() * fontHeight).toFixed(2)}px)`; divStyle.fontFamily = fontFamily; textDivProperties.fontSize = fontHeight; @@ -386,9 +395,15 @@ class TextLayer { } #layout(params) { - const { div, properties, ctx, prevFontSize, prevFontFamily } = params; + const { div, properties, ctx, prevFontSize, prevFontFamily, minFontSize } = + params; const { style } = div; + let transform = ""; + if (minFontSize > 1) { + transform = `scale(${1 / minFontSize})`; + } + if (properties.canvasWidth !== 0 && properties.hasText) { const { fontFamily } = style; const { canvasWidth, fontSize } = properties; @@ -403,7 +418,7 @@ class TextLayer { const { width } = ctx.measureText(div.textContent); if (width > 0) { - transform = `scaleX(${(canvasWidth * this.#scale) / width})`; + transform = `scaleX(${(canvasWidth * this.#scale) / width}) ${transform}`; } } if (properties.angle !== 0) { @@ -452,6 +467,27 @@ class TextLayer { return canvasContext; } + /** + * @returns {number} The minimum font size enforced by the browser + */ + static #getMinFontSize() { + if (this.#minFontSize === null) { + const div = document.createElement("div"); + div.style.opacity = 0; + div.style.lineHeight = 1; + div.style.fontSize = "1px"; + div.textContent = "X"; + document.body.append(div); + // In `display:block` elements contain a single line of text, + // the height matches the line height (which, when set to 1, + // matches the actual font size). + this.#minFontSize = div.getBoundingClientRect().height; + div.remove(); + } + + return this.#minFontSize; + } + static #getAscent(fontFamily, lang) { const cachedAscent = this.#ascentCache.get(fontFamily); if (cachedAscent) { diff --git a/test/integration/text_layer_spec.mjs b/test/integration/text_layer_spec.mjs index 1fd8296b808ba1..794922b6e97cee 100644 --- a/test/integration/text_layer_spec.mjs +++ b/test/integration/text_layer_spec.mjs @@ -290,4 +290,43 @@ describe("Text layer", () => { }); }); }); + + describe("when the browser enforces a minimum font size", () => { + let browser; + let page; + + beforeAll(async () => { + // Only testing in Firefox because, while Chrome has a setting similar to + // font.minimum-size.x-western, it is not exposed through its API. + browser = await startBrowser({ + browserName: "firefox", + startUrl: "", + extraPrefsFirefox: { "font.minimum-size.x-western": 24 }, + }); + page = await browser.newPage(); + await page.goto( + `${global.integrationBaseUrl}?file=/test/pdfs/tracemonkey.pdf#zoom=100` + ); + await page.bringToFront(); + await page.waitForSelector( + `.page[data-page-number = "1"] .endOfContent`, + { timeout: 0 } + ); + }); + + afterAll(async () => { + await browser.close(); + }); + + it("renders spans with the right size", async () => { + const rect = await getSpanRectFromText( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-" + ); + + expect(rect.width).toBeCloseTo(318, -1); + expect(rect.height).toBeCloseTo(12, 0); + }); + }); });