Skip to content

Ensure TextLayer hiddenCanvasElement is layout-neutral by default#21218

Open
RolandWArnold wants to merge 1 commit intomozilla:masterfrom
RolandWArnold:fix/text-layer-hidden-canvas-layout-neutral
Open

Ensure TextLayer hiddenCanvasElement is layout-neutral by default#21218
RolandWArnold wants to merge 1 commit intomozilla:masterfrom
RolandWArnold:fix/text-layer-hidden-canvas-layout-neutral

Conversation

@RolandWArnold
Copy link
Copy Markdown

TextLayer creates a helper canvas with class hiddenCanvasElement and appends it directly to document.body, which can adversely affect the host-page layout/scrolling behavior.

The stock viewer stylesheet web/pdf_viewer.css already makes this element layout-neutral via .hiddenCanvasElement { ... display: none; }, but direct pdfjs-dist / TextLayer consumers are not required to load web/pdf_viewer.css. Consumers that scope the viewer stylesheet to avoid global CSS leakage also lose this protection, since the helper canvas is appended outside the scoped viewer subtree.

This change applies the essential layout-neutralizing rule at the creation site, so the display-layer helper does not depend on viewer CSS to avoid participating in host-page layout.

Related context:

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 3, 2026

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 55.92%. Comparing base (a55cec4) to head (ceb7875).
⚠️ Report is 12 commits behind head on master.

Files with missing lines Patch % Lines
web/pdf_viewer.js 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #21218      +/-   ##
==========================================
- Coverage   55.97%   55.92%   -0.06%     
==========================================
  Files         218      220       +2     
  Lines       59033    58991      -42     
==========================================
- Hits        33041    32988      -53     
- Misses      25992    26003      +11     
Flag Coverage Δ
fonttest 8.65% <ø> (-0.02%) ⬇️
unittest 55.18% <50.00%> (-0.05%) ⬇️
unittestcli 55.66% <0.00%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Collaborator

@Snuffleupagus Snuffleupagus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that using the textLayer requires also using the pdf_viewer.css file, since otherwise things are simply not guaranteed to work, it's not clear (at least to me) why this effectively duplicated code should be added here!?

@RolandWArnold
Copy link
Copy Markdown
Author

Thanks for the quick review.

I understand that TextLayer is expected to be used with web/pdf_viewer.css. The case I was trying to cover here is that this particular rule is unusual because the element it targets is not inside the viewer subtree: TextLayer creates the canvas and appends it directly to document.body.

Consumers building custom viewer chrome may include the viewer CSS but scope/rewrite its selectors to avoid leaking PDF.js viewer styles into the surrounding application. In that setup, the rest of the text-layer CSS is present, but .hiddenCanvasElement can be scoped along with everything else and no longer matches the body-appended helper canvas.

That's why I thought applying the essential layout-neutralizing property at the creation site might be appropriate: the body-appended helper would not depend on a viewer-subtree stylesheet rule to avoid participating in host-page layout. react-pdf also had to add the .hiddenCanvasElement rule downstream in wojtekmaj/react-pdf#1815.

If the project’s position is that TextLayer consumers must load web/pdf_viewer.css without selector scoping, and scoped or rewritten viewer CSS is unsupported, I’m happy to close this.

Comment thread src/display/text_layer.js
ctx = canvas.getContext("2d", {
alpha: false,
willReadFrequently: true,
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we removed canvas from the document after getting the ctx instead, does the Context2D retain the locale inherited from the body? If it does, then it might be preferable to just fully remove the canvas from the DOM rather than "trying harder" to hide it.

Copy link
Copy Markdown
Author

@RolandWArnold RolandWArnold May 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested this - the CanvasRenderingContext2D does retain the relevant language state after detachment. With lang set on the canvas before getContext, measureText produces language-sensitive widths (for example, Korean differs from Japanese/Simplified Chinese for some CJK characters), and those widths are preserved after canvas.remove().

Detaching the canvas is the cleaner fix. I’ve updated this accordingly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested this - the CanvasRenderingContext2D does retain the relevant language state after detachment. With lang set on the canvas before getContext, measureText produces language-sensitive widths (for example, Korean differs from Japanese/Simplified Chinese for some CJK characters), and those widths are preserved after canvas.remove().

Detaching the canvas is the cleaner fix. I’ve updated this accordingly.

Tested this - the CanvasRenderingContext2D does retain the relevant language state after detachment. With lang set on the canvas before getContext, measureText produces language-sensitive widths (for example, Korean differs from Japanese/Simplified Chinese for some CJK characters), and those widths are preserved after canvas.remove().

Detaching the canvas is the cleaner fix. I’ve updated this accordingly.

Do you have a link around the canvas specification explaining that we've the guarantee the lang setting will be constant over the time ?
If not, I don't think we'll take this patch as is.
But I'm not opposed to have something like:

canvas.style.cssText =
    "position:absolute;top:0;left:0;width:0;height:0;display:none";

@Snuffleupagus can you imagine any drawbacks ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I'm not opposed to have something like:

canvas.style.cssText =
    "position:absolute;top:0;left:0;width:0;height:0;display:none";

@Snuffleupagus can you imagine any drawbacks ?

Not technically. (Only maintainability wise, since it's generally better to not define styles inline. In this case though the element is explicitly invisible, so that's probably less of a concern here.)

However, if that change is done then the hiddenCopyElement in the viewer ought to be handled the same way such that the CSS rules can be removed from the web/pdf_viewer.css file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks both. I’ve updated the PR to:

  • apply inline layout-neutral styles to hiddenCanvasElement
  • apply the same treatment to hiddenCopyElement
  • remove the now-redundant shared rule from web/pdf_viewer.css

@RolandWArnold RolandWArnold force-pushed the fix/text-layer-hidden-canvas-layout-neutral branch 3 times, most recently from fcfd0f3 to 3b05d2e Compare May 5, 2026 14:22
Copy link
Copy Markdown
Collaborator

@Snuffleupagus Snuffleupagus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also remember to write a good commit message, since a single line is rarely sufficient.

Comment thread src/display/text_layer.js Outdated
@@ -473,6 +473,8 @@ class TextLayer {
// OffscreenCanvas.
const canvas = document.createElement("canvas");
canvas.className = "hiddenCanvasElement";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this line be removed now?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed - confirmed no other references.

Comment thread web/pdf_viewer.js
@@ -967,6 +967,8 @@ class PDFViewer {
const element = (this.#hiddenCopyElement =
document.createElement("div"));
element.id = "hiddenCopyElement";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this line be removed now?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept this one: #hiddenCopyElement is used by test/integration/copy_paste_spec.mjs as a loadAndWait selector and via document.getElementById in the selectAll helper, so removing the id would break those tests.

Set layout-neutral styles at the creation sites for the hidden TextLayer
canvas and PDFViewer copy element rather than relying on the shared
web/pdf_viewer.css rule.

This keeps the helper elements invisible and out of layout when viewer CSS
selectors are scoped or omitted, and removes the obsolete hiddenCanvasElement
class and shared CSS rule.
@RolandWArnold RolandWArnold force-pushed the fix/text-layer-hidden-canvas-layout-neutral branch from 3b05d2e to ceb7875 Compare May 5, 2026 18:34
@RolandWArnold
Copy link
Copy Markdown
Author

Updated again:

  • removed the now-unused hiddenCanvasElement class
  • kept hiddenCopyElement’s id because test/integration/copy_paste_spec.mjs uses #hiddenCopyElement to wait for/query the copy helper element
  • expanded the commit message

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants