Fix blurry PDF rendering when zoomed in Chrome PWA
All checks were successful
Build and push Docker image / build (push) Successful in 14s
Test / test (push) Successful in 17s

Zoom is now baked into the canvas render scale instead of being applied
via CSS zoom. CSS zoom only scales pixels already on the canvas, causing
blurriness at high DPR (e.g. PWA on mobile). Now the canvas is rendered
at the correct resolution for the chosen zoom level.

Also: stop nulling currentPdfDoc on re-render so PDF.js page cache is
preserved, making zoom re-renders significantly faster.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-04-05 16:14:24 +02:00
parent e5dc58d84f
commit f5c141626f
2 changed files with 7 additions and 17 deletions

View file

@ -1554,6 +1554,7 @@ body.dnd-mode .timer-display {
.reader-content {
flex: 1;
overflow-y: scroll;
overflow-x: auto;
padding: 24px 16px;
line-height: 1.8;
font-family: Georgia, 'Times New Roman', serif;

View file

@ -2996,7 +2996,6 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride) {
// Viewport wrapper: CSS zoom controls display scale without re-rendering
const pdfVp = document.createElement('div');
pdfVp.id = 'pdf-viewport';
if (scaleOverride == null) pdfVp.style.zoom = readerSettings.pdfZoom / 100;
contentEl.appendChild(pdfVp);
const containerWidth = readerSettings.pdfSpread
@ -3010,8 +3009,9 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride) {
const page = await pdf.getPage(pageNum);
const naturalVp = page.getViewport({scale: 1});
const pageWidth = readerSettings.pdfSpread ? (containerWidth - 8) / 2 : containerWidth;
// Zoom baked into canvas resolution — no CSS zoom, stays sharp at any DPR
const scale = scaleOverride != null ? scaleOverride
: Math.max(0.5, pageWidth / naturalVp.width);
: Math.max(0.5, pageWidth / naturalVp.width) * (readerSettings.pdfZoom / 100);
const viewport = page.getViewport({scale});
const wrapper = document.createElement('div');
@ -3654,7 +3654,7 @@ function toggleSettingsPanel() {
}
}
function applyPdfZoom(newZoom) {
async function applyPdfZoom(newZoom) {
const contentEl2 = $('reader-content');
const fraction = contentEl2
? contentEl2.scrollTop / (contentEl2.scrollHeight - contentEl2.clientHeight || 1)
@ -3668,12 +3668,9 @@ function applyPdfZoom(newZoom) {
if (readerSettings.pdfPaginated) {
pdfSmartZoomPage(pdfCurrentPage);
} else {
const vp = document.getElementById('pdf-viewport');
if (vp) vp.style.zoom = readerSettings.pdfZoom / 100;
await reRenderPdf();
if (contentEl2 && fraction > 0) {
requestAnimationFrame(() => {
contentEl2.scrollTop = fraction * (contentEl2.scrollHeight - contentEl2.clientHeight);
});
contentEl2.scrollTop = fraction * (contentEl2.scrollHeight - contentEl2.clientHeight);
}
}
}
@ -3682,7 +3679,6 @@ async function reRenderPdf() {
if (!currentPdfBuffer) return;
const contentEl = $('reader-content');
if (!contentEl) return;
currentPdfDoc = null; // force re-parse with same buffer
await renderPdf(currentPdfBuffer, contentEl);
if (readerSettings.pdfPaginated) enterPdfPaginatedMode();
}
@ -3716,14 +3712,7 @@ function exitPdfPaginatedMode() {
contentEl.classList.remove('pdf-paginated');
contentEl.style.overflow = '';
contentEl.removeEventListener('click', _pdfPaginatedClick);
const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper');
wrappers.forEach(w => {
w.style.display = '';
const canvas = w.querySelector('canvas');
if (canvas) canvas.style.transform = '';
});
const pdfVp = document.getElementById('pdf-viewport');
if (pdfVp) pdfVp.style.zoom = readerSettings.pdfZoom / 100;
reRenderPdf();
}
function _pdfPaginatedClick(e) {