Move text layer scaling logic to CSS

This commit moves all the logic to scale up&down `<span>`s in the text
layer, introduced in #18283, to CSS.

The motivation for this change is that #18283 is still not enough for
all cases. That PR fixed the problem in Chrome&Firefox desktop, which
allow users to set an actual minimum font size in the browser settings.
However, other browsers (e.g. the Chrome-based WebView on Android) have
more complex logic and they scale up small text rather than simply
applying a minimum.

A workaround for that behavior is probably out of scope for PDF.js
itself as it only affects not officially supported platforms. However,
having access to the actual expected font height (through
`--font-height`) allows embedders of PDF.js to implement a workaround by
themselves.
This commit is contained in:
Nicolò Ribaudo 2025-12-08 15:57:07 +01:00
parent f75812b0af
commit eb2b7c2c86
No known key found for this signature in database
GPG Key ID: AAFDA9101C58F338
3 changed files with 28 additions and 23 deletions

View File

@ -128,6 +128,7 @@ class TextLayer {
this.#pageHeight = pageHeight; this.#pageHeight = pageHeight;
TextLayer.#ensureMinFontSizeComputed(); TextLayer.#ensureMinFontSizeComputed();
container.style.setProperty("--min-font-size", TextLayer.#minFontSize);
setLayerDimensions(container, viewport); setLayerDimensions(container, viewport);
@ -342,7 +343,6 @@ class TextLayer {
top = tx[5] - fontAscent * Math.cos(angle); top = tx[5] - fontAscent * Math.cos(angle);
} }
const scaleFactorStr = "calc(var(--total-scale-factor) *";
const divStyle = textDiv.style; const divStyle = textDiv.style;
// Setting the style properties individually, rather than all at once, // Setting the style properties individually, rather than all at once,
// should be OK since the `textDiv` isn't appended to the document yet. // should be OK since the `textDiv` isn't appended to the document yet.
@ -351,14 +351,10 @@ class TextLayer {
divStyle.top = `${((100 * top) / this.#pageHeight).toFixed(2)}%`; divStyle.top = `${((100 * top) / this.#pageHeight).toFixed(2)}%`;
} else { } else {
// We're in a marked content span, hence we can't use percents. // We're in a marked content span, hence we can't use percents.
divStyle.left = `${scaleFactorStr}${left.toFixed(2)}px)`; divStyle.left = `calc(var(--total-scale-factor) * ${left.toFixed(2)}px)`;
divStyle.top = `${scaleFactorStr}${top.toFixed(2)}px)`; divStyle.top = `calc(var(--total-scale-factor) * ${top.toFixed(2)}px)`;
} }
// We multiply the font size by #minFontSize, and then #layout will divStyle.setProperty("--font-height", `${fontHeight.toFixed(2)}px`);
// 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 <span>s can always match the size of the text in the canvas.
divStyle.fontSize = `${scaleFactorStr}${(TextLayer.#minFontSize * fontHeight).toFixed(2)}px)`;
divStyle.fontFamily = fontFamily; divStyle.fontFamily = fontFamily;
textDivProperties.fontSize = fontHeight; textDivProperties.fontSize = fontHeight;
@ -421,11 +417,6 @@ class TextLayer {
const { div, properties, ctx } = params; const { div, properties, ctx } = params;
const { style } = div; const { style } = div;
let transform = "";
if (TextLayer.#minFontSize > 1) {
transform = `scale(${1 / TextLayer.#minFontSize})`;
}
if (properties.canvasWidth !== 0 && properties.hasText) { if (properties.canvasWidth !== 0 && properties.hasText) {
const { fontFamily } = style; const { fontFamily } = style;
const { canvasWidth, fontSize } = properties; const { canvasWidth, fontSize } = properties;
@ -435,14 +426,11 @@ class TextLayer {
const { width } = ctx.measureText(div.textContent); const { width } = ctx.measureText(div.textContent);
if (width > 0) { if (width > 0) {
transform = `scaleX(${(canvasWidth * this.#scale) / width}) ${transform}`; style.setProperty("--scale-x", (canvasWidth * this.#scale) / width);
} }
} }
if (properties.angle !== 0) { if (properties.angle !== 0) {
transform = `rotate(${properties.angle}deg) ${transform}`; style.setProperty("--rotate", `${properties.angle}deg`);
}
if (transform.length > 0) {
style.transform = transform;
} }
} }

View File

@ -101,11 +101,12 @@ describe("textLayer", function () {
const getTransform = container => { const getTransform = container => {
const transform = []; const transform = [];
for (const span of container.childNodes) { for (const { style } of container.childNodes) {
const t = span.style.transform; transform.push({
expect(t).toMatch(/^scaleX\([\d.]+\)$/); fontHeight: style.getPropertyValue("--font-height"),
scaleX: style.getPropertyValue("--scale-x"),
transform.push(t); rotate: style.getPropertyValue("--rotate"),
});
} }
return transform; return transform;
}; };

View File

@ -38,9 +38,25 @@
transform-origin: 0% 0%; transform-origin: 0% 0%;
} }
/* We multiply the font size by --min-font-size, and then scale the text
* elements by 1/--min-font-size. This allows us to effectively ignore the
* minimum font size enforced by the browser, so that the text layer <span>s
* can always match the size of the text in the canvas. */
--min-font-size: 1;
--text-scale-factor: calc(var(--total-scale-factor) * var(--min-font-size));
--min-font-size-inv: calc(1 / var(--min-font-size));
> :not(.markedContent), > :not(.markedContent),
.markedContent span:not(.markedContent) { .markedContent span:not(.markedContent) {
z-index: 1; z-index: 1;
--font-height: 0; /* set by text_layer.js */
font-size: calc(var(--text-scale-factor) * var(--font-height));
--scale-x: 1;
--rotate: 0deg;
transform: rotate(var(--rotate)) scaleX(var(--scale-x))
scale(var(--min-font-size-inv));
} }
/* Only necessary in Google Chrome, see issue 14205, and most unfortunately /* Only necessary in Google Chrome, see issue 14205, and most unfortunately