Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-minor] Render high-res partial page views when falling back to CSS zoom (bug 1492303) #19128

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

nicolo-ribaudo
Copy link
Contributor

@nicolo-ribaudo nicolo-ribaudo commented Nov 29, 2024

Commit:

Render high-res partial page views when falling back to CSS zoom

When rendering big PDF pages at high zoom levels, we currently fall back to CSS zoom to avoid rendering canvases with too many pixels. This causes zoomed in PDF to look blurry, and the text to be potentially unreadable.

This commit adds support for rendering part of a page (called PDFPageDetailView in the code), so that we can render portion of a page in a smaller canvas without hiting the maximun canvas size limit.

Specifically, we render an area of that page that is slightly larger than the area that is visible on the screen (100% larger in each direction, unless we have to limit it due to the maximum canvas size). As the user scrolls around the page, we re-render a new area centered around what is currently visible.

To play around with this patch, you need to use a large PDF (the one I'm using is https://www.gtt.to.it/cms/risorse/urbana/mappa/mapparete.pdf) and zoom in: in the current release the text becomes blurry, while with this patch it remains sharp. Also, if you set maxCanvasPixels to a smaller value (for example, 8M) you'll more clearly see the effects also at lower zoom levels. Note however that if maxCanvasPixels is set to less than 9 time the number of pixels visible in your window you'll get more frequent re-renders as you scroll.

The PDFPageDetailView class still process every single operation of the PDF, even if many of them will actually be outside of the canvas and thus not be actually rendered. This is acceptable because we are only rendering one "detail view" per page at a time, and we are not splitting the page in multiple tiles that might appear at the same time on the screen. In the future however #19043 could improve the performance of this patch, and it would enable (for example) tiling a page into multiple detail views when printing it.

There are some very minor TODOs that I left in the code, for some areas I need review feedback for. Also, I still need to find the best way to write tests.

Fixes #14147

Before:
image

After:
image

https://www.gtt.to.it/cms/risorse/urbana/mappa/mapparete.pdf at 400% zoom

Before:
image

After:
image

Fixes #14193

web/pdf_page_view.js Fixed Show fixed Hide fixed
web/pdf_page_view.js Outdated Show resolved Hide resolved
@nicolo-ribaudo
Copy link
Contributor Author

After discussing this patch with @calixteman, I updated it to prioritize rendering the full css-zoomed canvas rather than the high-resolution one. This means that, when zooming in, you'll first see the css-zoomed canvas, and then it will be replaced by the high-res one once it's ready. It's a slightly worse experience, however it guarantees that we do not regress in cases where the user starts moving around before that we are done rendering the background canvas (because if only the high-res canvas is there, they'll see just white until when the new high-res canvas is rendered).

I also changed the logic that decides when to re-render the high-res canvas to not only do it once the user scrolls past its edges, but when the user is close to scrolling past the edges.

web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_page_view.js Outdated Show resolved Hide resolved
web/app.js Outdated Show resolved Hide resolved
@nicolo-ribaudo nicolo-ribaudo force-pushed the draw-page-portion branch 2 times, most recently from 69a98fc to 3408061 Compare December 11, 2024 13:30
@nicolo-ribaudo
Copy link
Contributor Author

Unfortunately this approach has a problem right now: since the various SVG paths for drawings are inserted inside the .canvasWrapper, they are now behind the detail layer and thus not visible. Marking as draft until this is fixed.

@nicolo-ribaudo nicolo-ribaudo marked this pull request as draft December 11, 2024 14:20
web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_page_view.js Outdated Show resolved Hide resolved
@nicolo-ribaudo
Copy link
Contributor Author

Fixing the drawing problem required changing the new canvas to be in the existing .canvasWrapper, rather than in a separate layer. This actually ended up simplifying the code a bit, and the canvasWrapper management logic is now fully contained in PDFPageView.

@nicolo-ribaudo nicolo-ribaudo marked this pull request as ready for review December 11, 2024 16:18
Copy link
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.

Leaving a few more comments, mostly for the first patch since I've only begun looking at the Render high-res partial page views when falling back to CSS zoom commit (and that one requires careful checking).

Also, is this new functionality anything that we could "easily" test with e.g. integration-tests?

web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_viewer.js Outdated Show resolved Hide resolved
web/pdf_page_view.js Outdated Show resolved Hide resolved
web/pdf_rendering_queue.js Outdated Show resolved Hide resolved
web/pdf_viewer.js Outdated Show resolved Hide resolved
web/pdf_viewer.js Outdated Show resolved Hide resolved
@nicolo-ribaudo
Copy link
Contributor Author

I'm working on adding some meaningful tests.

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Linux m4)


Failed

Full output at http://54.241.84.105:8877/8ba1a6512246b8a/output.txt

Total script time: 29.65 mins

  • Unit tests: Passed
  • Integration Tests: FAILED
  • Regression tests: Passed

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Windows)


Failed

Full output at http://54.193.163.58:8877/3fbf252fd1d6f6e/output.txt

Total script time: 60.00 mins

@nicolo-ribaudo nicolo-ribaudo force-pushed the draw-page-portion branch 2 times, most recently from 2d54dd4 to 8e863cb Compare February 4, 2025 09:19
@Snuffleupagus
Copy link
Collaborator

/botio integrationtest

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Linux m4)


Received

Command cmd_integrationtest from @Snuffleupagus received. Current queue size: 0

Live output at: http://54.241.84.105:8877/abb80cfe2b1f79c/output.txt

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Windows)


Received

Command cmd_integrationtest from @Snuffleupagus received. Current queue size: 0

Live output at: http://54.193.163.58:8877/62ca8d25aae0fb7/output.txt

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Linux m4)


Failed

Full output at http://54.241.84.105:8877/abb80cfe2b1f79c/output.txt

Total script time: 13.26 mins

  • Integration Tests: FAILED

@nicolo-ribaudo
Copy link
Contributor Author

nicolo-ribaudo commented Feb 4, 2025

Whops sorry, the test now should be fixed for real. It was accidentally relying on the first expect() call happen as soon as the PDF viewer opens, before that it finishes rendering anything (which happens for me locally, but the bots are slower). We don't actually need that expect().

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Windows)


Failed

Full output at http://54.193.163.58:8877/62ca8d25aae0fb7/output.txt

Total script time: 27.42 mins

  • Integration Tests: FAILED

@nicolo-ribaudo
Copy link
Contributor Author

@Snuffleupagus please wait before re-running the tests — @calixteman messaged me a video where the detail canvas gets left behind even when it's not needed anymore, and I'm now investigating it.

@nicolo-ribaudo
Copy link
Contributor Author

Wow this was a difficult bug to track down, and to write tests for. I added two commits (that I will squash), fixing two different race conditions that causes the detail canvas to not be properly removed from the DOM when it's not needed anymore. Each commit includes a test.

The first commit fixes a race condition that can only be triggered via the API (since it requires two synchronous calls to .updateScale). The second commit fixes one that can also appear while manually zooming in/out quickly, and I believe it's the bug that @calixteman noticed.

I'm pasting here the message of the second fixup! commit, so that it doesn't get lost when squashing:

Clean up the PDFDetailView after cancellation synchronously.

This ensures that, when rendering is cancelled, we always restore tracking
the canvas that is still in the DOM (done in the `onCancel` callback of
`_drawCanvas`) before calling `.remove()` on whatever the tracked canvas
is.

Given this algorithm that starts with a 1000% zoom already rendered,
with a detail view canvas that we'll call #1:
- zoom 0.3x (!needs detail view)
- await 1 microtick (Promise.resolve())
- zoom 0.3x (!does not need detail view)

Before this patch, the order of operations would be:
- Create a new detail canvas #2 for the 300% zoom level
- Change detailView.canvas from #1 to #2
- await 1 microtick (between the two zoom changes)
- In .reset(), cancel the rendering
- In .reset(), call detailView.canvas.remove() (removing #2, even if the
  one in the dom is actually still #1)
- Set .detailView to `null`, because we don't need it anymore.
- await, because the `onCancel` logic was run asyncronously
- Change detailView.canvas from #2 to #1

After this patch, the order becomes:
- Create a new detail canvas #2 for the 300% zoom level
- Change detailView.canvas from #1 to #2
- await 1 microtick (between the two zoom changes)
- In .reset(), cancel the rendering
  - It runs `onCancel`, changing detailView.canvas from #2 to #1
- In .reset(), call detailView.canvas.remove() (which now removes #1)
- Set .detailView to `null`, because we don't need it anymore.

@nicolo-ribaudo nicolo-ribaudo force-pushed the draw-page-portion branch 2 times, most recently from 9c2f1fc to bc7e568 Compare February 5, 2025 14:08
@Snuffleupagus Snuffleupagus changed the title Render high-res partial page views when falling back to CSS zoom (bug 1492303) [api-minor] Render high-res partial page views when falling back to CSS zoom (bug 1492303) Feb 5, 2025
src/display/api.js Outdated Show resolved Hide resolved
src/display/api.js Outdated Show resolved Hide resolved
This base class contains the generic logic for:
- Creating a canvas and showing when appropriate
- Rendering in the canvas
- Keeping track of the rendering state
…SS zoom (bug 1492303)

When rendering big PDF pages at high zoom levels, we currently fall back
to CSS zoom to avoid rendering canvases with too many pixels. This
causes zoomed in PDF to look blurry, and the text to be potentially
unreadable.

This commit adds support for rendering _part_ of a page (called
`PDFPageDetailView` in the code), so that we can render portion of a
page in a smaller canvas without hiting the maximun canvas size limit.

Specifically, we render an area of that page that is slightly larger
than the area that is visible on the screen (100% larger in each
direction, unless we have to limit it due to the maximum canvas size).
As the user scrolls around the page, we re-render a new area centered
around what is currently visible.
When scrolling quickly, the constant re-rendering of the detail view
significantly affects rendering performance, causing Firefox to
not render even the _background canvas_, which is just a static canvas
not being re-drawn by JavaScript.

This commit changes the viewer to only render the detail view while
scrolling if its rendering hasn't just been cancelled. This means that:
- when the user is scrolling slowly, we have enough time to render the
  detail view before that we need to change its area, so the user always
  sees the full screen as high resolution.
- when the user is scrolling quickly, as soon as we have to cancel a
  rendering we just give up, and the user will see the lower resolution
  canvas. When then the user stops scrolling, we render the detail view
  for the new visible area.
@Snuffleupagus
Copy link
Collaborator

/botio integrationtest

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Linux m4)


Received

Command cmd_integrationtest from @Snuffleupagus received. Current queue size: 0

Live output at: http://54.241.84.105:8877/026343628fae9c8/output.txt

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Windows)


Received

Command cmd_integrationtest from @Snuffleupagus received. Current queue size: 0

Live output at: http://54.193.163.58:8877/7a79d253a24948d/output.txt

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Linux m4)


Failed

Full output at http://54.241.84.105:8877/026343628fae9c8/output.txt

Total script time: 11.39 mins

  • Integration Tests: FAILED

@moz-tools-bot
Copy link
Collaborator

From: Bot.io (Windows)


Failed

Full output at http://54.193.163.58:8877/7a79d253a24948d/output.txt

Total script time: 26.77 mins

  • Integration Tests: FAILED

@nicolo-ribaudo
Copy link
Contributor Author

The windows-only failure for "PDF viewer CSS-only zoom triggers when going bigger than maxCanvasPixels triggers CSS-only zoom above maxCanvasPixels" could be related, I'm taking a look

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
image-quality release-blocker Blocker for the upcoming release viewer
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PDF rendering problem. Barcode and text getting pixelated. PDF is blurry Huge canvases are not rendered
5 participants