1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 22:58:07 +02:00

Ensure that textLayers can be rendered in parallel, without interfering with each other

Note that the textContent is returned in "chunks" from the API, through the use of `ReadableStream`s, and on the main-thread we're (normally) using just one temporary canvas in order to measure the size of the textLayer `span`s; see the [`#layout`](5b4c2fe1a8/src/display/text_layer.js (L396-L428)) method.

*Order of events, for parallel textLayer rendering:*
 1. Call [`render`](5b4c2fe1a8/src/display/text_layer.js (L155-L177)) of the textLayer for page A.
 2. Immediately call `render` of the textLayer for page B.
 3. The first text-chunk for pageA arrives, and it's parsed/layout which means updating the cached [fontSize/fontFamily](5b4c2fe1a8/src/display/text_layer.js (L409-L413)) for the textLayer of page A.
 4. The first text-chunk for pageB arrives, which means updating the cached fontSize/fontFamily *for the textLayer of page B* since this data is unique to each `TextLayer`-instance.
 5. The second text-chunk for pageA arrives, and we don't update the canvas-font since the cached fontSize/fontFamily still apply from step 3 above.

Where this potentially breaks down is between the last steps, since we're using just one temporary canvas for all measurements but have *individual* fontSize/fontFamily caches for each textLayer.
Hence it's possible that the canvas-font has actually changed, despite the cached values suggesting otherwise, and to address this we instead cache the fontSize/fontFamily globally through a new (static) helper method.

*Note:* Includes a basic unit-test, using dummy text-content, which fails on `master` and passes with this patch.

Finally, pun intended, ensure that temporary textLayer-data is cleared *before* the `render`-promise resolves to avoid any intermittent problems in the unit-tests.
This commit is contained in:
Jonas Jenwald 2024-09-10 11:34:55 +02:00
parent 5b4c2fe1a8
commit 5b3d3c7dd9
2 changed files with 187 additions and 24 deletions

View file

@ -83,6 +83,8 @@ class TextLayer {
static #canvasContexts = new Map();
static #canvasCtxFonts = new WeakMap();
static #minFontSize = null;
static #pendingTextLayers = new Set();
@ -111,8 +113,6 @@ class TextLayer {
this.#scale = viewport.scale * (globalThis.devicePixelRatio || 1);
this.#rotation = viewport.rotation;
this.#layoutTextParams = {
prevFontSize: null,
prevFontFamily: null,
div: null,
properties: null,
ctx: null,
@ -128,13 +128,13 @@ class TextLayer {
// Always clean-up the temporary canvas once rendering is no longer pending.
this.#capability.promise
.catch(() => {
// Avoid "Uncaught promise" messages in the console.
})
.then(() => {
.finally(() => {
TextLayer.#pendingTextLayers.delete(this);
this.#layoutTextParams = null;
this.#styleCache = null;
})
.catch(() => {
// Avoid "Uncaught promise" messages in the console.
});
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
@ -195,8 +195,6 @@ class TextLayer {
onBefore?.();
this.#scale = scale;
const params = {
prevFontSize: null,
prevFontFamily: null,
div: null,
properties: null,
ctx: TextLayer.#getCtx(this.#lang),
@ -394,7 +392,7 @@ class TextLayer {
}
#layout(params) {
const { div, properties, ctx, prevFontSize, prevFontFamily } = params;
const { div, properties, ctx } = params;
const { style } = div;
let transform = "";
@ -406,12 +404,7 @@ class TextLayer {
const { fontFamily } = style;
const { canvasWidth, fontSize } = properties;
if (prevFontSize !== fontSize || prevFontFamily !== fontFamily) {
ctx.font = `${fontSize * this.#scale}px ${fontFamily}`;
params.prevFontSize = fontSize;
params.prevFontFamily = fontFamily;
}
TextLayer.#ensureCtxFont(ctx, fontSize * this.#scale, fontFamily);
// Only measure the width for multi-char text divs, see `appendText`.
const { width } = ctx.measureText(div.textContent);
@ -444,8 +437,8 @@ class TextLayer {
}
static #getCtx(lang = null) {
let canvasContext = this.#canvasContexts.get((lang ||= ""));
if (!canvasContext) {
let ctx = this.#canvasContexts.get((lang ||= ""));
if (!ctx) {
// We don't use an OffscreenCanvas here because we use serif/sans serif
// fonts with it and they depends on the locale.
// In Firefox, the <html> element get a lang attribute that depends on
@ -460,13 +453,26 @@ class TextLayer {
canvas.className = "hiddenCanvasElement";
canvas.lang = lang;
document.body.append(canvas);
canvasContext = canvas.getContext("2d", {
ctx = canvas.getContext("2d", {
alpha: false,
willReadFrequently: true,
});
this.#canvasContexts.set(lang, canvasContext);
this.#canvasContexts.set(lang, ctx);
// Also, initialize state for the `#ensureCtxFont` method.
this.#canvasCtxFonts.set(ctx, { size: 0, family: "" });
}
return canvasContext;
return ctx;
}
static #ensureCtxFont(ctx, size, family) {
const cached = this.#canvasCtxFonts.get(ctx);
if (size === cached.size && family === cached.family) {
return; // The font is already set.
}
ctx.font = `${size}px ${family}`;
cached.size = size;
cached.family = family;
}
/**
@ -497,9 +503,8 @@ class TextLayer {
}
const ctx = this.#getCtx(lang);
const savedFont = ctx.font;
ctx.canvas.width = ctx.canvas.height = DEFAULT_FONT_SIZE;
ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`;
this.#ensureCtxFont(ctx, DEFAULT_FONT_SIZE, fontFamily);
const metrics = ctx.measureText("");
// Both properties aren't available by default in Firefox.
@ -510,7 +515,6 @@ class TextLayer {
this.#ascentCache.set(fontFamily, ratio);
ctx.canvas.width = ctx.canvas.height = 0;
ctx.font = savedFont;
return ratio;
}
@ -550,7 +554,6 @@ class TextLayer {
}
ctx.canvas.width = ctx.canvas.height = 0;
ctx.font = savedFont;
const ratio = ascent ? ascent / (ascent + descent) : DEFAULT_FONT_ASCENT;
this.#ascentCache.set(fontFamily, ratio);