Hide the text in the text layer associated with MathML elements (bug 2009627)

The bug was supposed to be fixed by #20471 but here there are some annotations in the pdf.
When those annotations are added to the DOM, the struct tree has to be rendered but without
the text layer (because of asynchronicity).
So this patch is making sure that the modifications in the text layer are done once the
layer is rendered.
This commit is contained in:
Calixte Denizet 2026-01-12 11:43:08 +01:00 committed by calixteman
parent f40ab1a3f8
commit cffd54e9c6
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
5 changed files with 98 additions and 24 deletions

View File

@ -498,4 +498,50 @@ describe("accessibility", () => {
);
});
});
describe("Text elements must be aria-hidden when there's MathML and annotations", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug2009627.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the text in text layer is aria-hidden", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const isSanitizerSupported = await page.evaluate(() => {
try {
// eslint-disable-next-line no-undef
return typeof Sanitizer !== "undefined";
} catch {
return false;
}
});
const ariaHidden = await page.evaluate(() =>
Array.from(
document.querySelectorAll(".structTree :has(> math)")
).map(el =>
document
.getElementById(el.getAttribute("aria-owns"))
.getAttribute("aria-hidden")
)
);
if (isSanitizerSupported) {
expect(ariaHidden)
.withContext(`In ${browserName}`)
.toEqual(["true", "true", "true"]);
} else {
// eslint-disable-next-line no-console
console.log(
`Pending in Chrome: Sanitizer API (in ${browserName}) is not supported`
);
}
})
);
});
});
});

View File

@ -867,3 +867,4 @@
!bitmap-trailing-7fff-stripped.pdf
!bitmap.pdf
!bomb_giant.pdf
!bug2009627.pdf

BIN
test/pdfs/bug2009627.pdf Executable file

Binary file not shown.

View File

@ -493,7 +493,7 @@ class PDFPageView extends BasePDFPageView {
const treeDom = await this.structTreeLayer?.render();
if (treeDom) {
this.l10n.pause();
this.structTreeLayer?.addElementsToTextLayer();
this.structTreeLayer?.updateTextLayer();
if (this.canvas && treeDom.parentNode !== this.canvas) {
// Pause translation when inserting the structTree in the DOM.
this.canvas.append(treeDom);

View File

@ -184,6 +184,10 @@ class StructTreeLayerBuilder {
#elementsToAddToTextLayer = null;
#elementsToHideInTextLayer = null;
#elementsToStealFromTextLayer = null;
/**
* @param {StructTreeLayerBuilderOptions} options
*/
@ -304,15 +308,49 @@ class StructTreeLayerBuilder {
return true;
}
addElementsToTextLayer() {
if (!this.#elementsToAddToTextLayer) {
return;
updateTextLayer() {
if (this.#elementsToAddToTextLayer) {
for (const [id, img] of this.#elementsToAddToTextLayer) {
document.getElementById(id)?.append(img);
}
this.#elementsToAddToTextLayer.clear();
this.#elementsToAddToTextLayer = null;
}
for (const [id, img] of this.#elementsToAddToTextLayer) {
document.getElementById(id)?.append(img);
if (this.#elementsToHideInTextLayer) {
for (const id of this.#elementsToHideInTextLayer) {
const elem = document.getElementById(id);
if (elem) {
elem.ariaHidden = true;
}
}
this.#elementsToHideInTextLayer.length = 0;
this.#elementsToHideInTextLayer = null;
}
if (this.#elementsToStealFromTextLayer) {
for (
let i = 0, ii = this.#elementsToStealFromTextLayer.length;
i < ii;
i += 2
) {
const element = this.#elementsToStealFromTextLayer[i];
const ids = this.#elementsToStealFromTextLayer[i + 1];
let textContent = "";
for (const id of ids) {
const elem = document.getElementById(id);
if (elem) {
textContent += elem.textContent.trim() || "";
// Aria-hide the element in order to avoid duplicate reading of the
// math content by screen readers.
elem.ariaHidden = "true";
}
}
if (textContent) {
element.textContent = textContent;
}
}
this.#elementsToStealFromTextLayer.length = 0;
this.#elementsToStealFromTextLayer = null;
}
this.#elementsToAddToTextLayer.clear();
this.#elementsToAddToTextLayer = null;
}
#walk(node) {
@ -325,21 +363,13 @@ class StructTreeLayerBuilder {
const { role } = node;
if (MathMLElements.has(role)) {
element = document.createElementNS(MathMLNamespace, role);
let text = "";
const ids = [];
(this.#elementsToStealFromTextLayer ||= []).push(element, ids);
for (const { type, id } of node.children || []) {
if (type !== "content" || !id) {
continue;
if (type === "content" && id) {
ids.push(id);
}
const elem = document.getElementById(id);
if (!elem) {
continue;
}
text += elem.textContent.trim() || "";
// Aria-hide the element in order to avoid duplicate reading of the
// math content by screen readers.
elem.ariaHidden = "true";
}
element.textContent = text;
} else {
element = document.createElement("span");
}
@ -365,10 +395,7 @@ class StructTreeLayerBuilder {
if (!id) {
continue;
}
const elem = document.getElementById(id);
if (elem) {
elem.ariaHidden = true;
}
(this.#elementsToHideInTextLayer ||= []).push(id);
}
// For now, we don't want to keep the alt text if there's valid
// MathML (see https://github.com/w3c/mathml-aam/issues/37).