diff --git a/src/display/text_layer.js b/src/display/text_layer.js index df35dfb4e5b73..d7b96eff78c5e 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(); /** @@ -120,6 +122,8 @@ class TextLayer { this.#pageWidth = pageWidth; this.#pageHeight = pageHeight; + TextLayer.#ensureMinFontSizeComputed(); + setLayerDimensions(container, viewport); // Always clean-up the temporary canvas once rendering is no longer pending. @@ -242,7 +246,7 @@ class TextLayer { if (this.#disableProcessItems) { return; } - this.#layoutTextParams.ctx ||= TextLayer.#getCtx(this.#lang); + this.#layoutTextParams.ctx ??= TextLayer.#getCtx(this.#lang); const textDivs = this.#textDivs, textContentItemsStr = this.#textContentItemsStr; @@ -326,7 +330,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 #minFontSize, and then #layout will + // scale the element by 1/#minFontSize. 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.#minFontSize * fontHeight).toFixed(2)}px)`; divStyle.fontFamily = fontFamily; textDivProperties.fontSize = fontHeight; @@ -388,7 +396,12 @@ class TextLayer { #layout(params) { const { div, properties, ctx, prevFontSize, prevFontFamily } = params; const { style } = div; + let transform = ""; + if (TextLayer.#minFontSize > 1) { + transform = `scale(${1 / TextLayer.#minFontSize})`; + } + if (properties.canvasWidth !== 0 && properties.hasText) { const { fontFamily } = style; const { canvasWidth, fontSize } = properties; @@ -403,7 +416,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) { @@ -456,6 +469,26 @@ class TextLayer { return canvasContext; } + /** + * Compute the minimum font size enforced by the browser. + */ + static #ensureMinFontSizeComputed() { + if (this.#minFontSize !== null) { + return; + } + 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(); + } + 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 d34b519c6853e..83779da82f9ff 100644 --- a/test/integration/text_layer_spec.mjs +++ b/test/integration/text_layer_spec.mjs @@ -296,4 +296,47 @@ 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": 40 }, + }); + 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 closeSinglePage(page); + 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-" + ); + + // The difference between `a` and `b`, as a percentage of the lower one + const getPercentDiff = (a, b) => Math.max(a, b) / Math.min(a, b) - 1; + + expect(getPercentDiff(rect.width, 315)).toBeLessThan(0.03); + expect(getPercentDiff(rect.height, 12)).toBeLessThan(0.03); + }); + }); });