From db8db42358ea88b6e784fe750d9cbecedc142d25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:15:43 +0000 Subject: [PATCH 01/10] Bump pdfjs-dist from 5.4.149 to 5.4.296 Bumps [pdfjs-dist](https://github.com/mozilla/pdf.js) from 5.4.149 to 5.4.296. - [Release notes](https://github.com/mozilla/pdf.js/releases) - [Commits](https://github.com/mozilla/pdf.js/compare/v5.4.149...v5.4.296) --- updated-dependencies: - dependency-name: pdfjs-dist dependency-version: 5.4.296 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 96 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index e92e6651..d929fbd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "@stylistic/eslint-plugin": "^5.5.0", "esbuild": "^0.25.11", "eslint": "^9.38.0", - "pdfjs-dist": "^5.4.149" + "pdfjs-dist": "^5.4.296" } }, "node_modules/@esbuild/aix-ppc64": { @@ -664,9 +664,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.78.tgz", - "integrity": "sha512-YaBHJvT+T1DoP16puvWM6w46Lq3VhwKIJ8th5m1iEJyGh7mibk5dT7flBvMQ1EH1LYmMzXJ+OUhu+8wQ9I6u7g==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", "dev": true, "license": "MIT", "optional": true, @@ -677,22 +677,22 @@ "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.78", - "@napi-rs/canvas-darwin-arm64": "0.1.78", - "@napi-rs/canvas-darwin-x64": "0.1.78", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.78", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.78", - "@napi-rs/canvas-linux-arm64-musl": "0.1.78", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.78", - "@napi-rs/canvas-linux-x64-gnu": "0.1.78", - "@napi-rs/canvas-linux-x64-musl": "0.1.78", - "@napi-rs/canvas-win32-x64-msvc": "0.1.78" + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.78.tgz", - "integrity": "sha512-N1ikxztjrRmh8xxlG5kYm1RuNr8ZW1EINEDQsLhhuy7t0pWI/e7SH91uFVLZKCMDyjel1tyWV93b5fdCAi7ggw==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", "cpu": [ "arm64" ], @@ -707,9 +707,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.78.tgz", - "integrity": "sha512-FA3aCU3G5yGc74BSmnLJTObnZRV+HW+JBTrsU+0WVVaNyVKlb5nMvYAQuieQlRVemsAA2ek2c6nYtHh6u6bwFw==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", "cpu": [ "arm64" ], @@ -724,9 +724,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.78.tgz", - "integrity": "sha512-xVij69o9t/frixCDEoyWoVDKgE3ksLGdmE2nvBWVGmoLu94MWUlv2y4Qzf5oozBmydG5Dcm4pRHFBM7YWa1i6g==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", "cpu": [ "x64" ], @@ -741,9 +741,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.78.tgz", - "integrity": "sha512-aSEXrLcIpBtXpOSnLhTg4jPsjJEnK7Je9KqUdAWjc7T8O4iYlxWxrXFIF8rV8J79h5jNdScgZpAUWYnEcutR3g==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", "cpu": [ "arm" ], @@ -758,9 +758,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.78.tgz", - "integrity": "sha512-dlEPRX1hLGKaY3UtGa1dtkA1uGgFITn2mDnfI6YsLlYyLJQNqHx87D1YTACI4zFCUuLr/EzQDzuX+vnp9YveVg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", "cpu": [ "arm64" ], @@ -775,9 +775,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.78.tgz", - "integrity": "sha512-TsCfjOPZtm5Q/NO1EZHR5pwDPSPjPEttvnv44GL32Zn1uvudssjTLbvaG1jHq81Qxm16GTXEiYLmx4jOLZQYlg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", "cpu": [ "arm64" ], @@ -792,9 +792,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.78.tgz", - "integrity": "sha512-+cpTTb0GDshEow/5Fy8TpNyzaPsYb3clQIjgWRmzRcuteLU+CHEU/vpYvAcSo7JxHYPJd8fjSr+qqh+nI5AtmA==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", "cpu": [ "riscv64" ], @@ -809,9 +809,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.78.tgz", - "integrity": "sha512-wxRcvKfvYBgtrO0Uy8OmwvjlnTcHpY45LLwkwVNIWHPqHAsyoTyG/JBSfJ0p5tWRzMOPDCDqdhpIO4LOgXjeyg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", "cpu": [ "x64" ], @@ -826,9 +826,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.78.tgz", - "integrity": "sha512-vQFOGwC9QDP0kXlhb2LU1QRw/humXgcbVp8mXlyBqzc/a0eijlLF9wzyarHC1EywpymtS63TAj8PHZnhTYN6hg==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", "cpu": [ "x64" ], @@ -843,9 +843,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.78", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.78.tgz", - "integrity": "sha512-/eKlTZBtGUgpRKalzOzRr6h7KVSuziESWXgBcBnXggZmimwIJWPJlEcbrx5Tcwj8rPuZiANXQOG9pPgy9Q4LTQ==", + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", "cpu": [ "x64" ], @@ -1659,16 +1659,16 @@ } }, "node_modules/pdfjs-dist": { - "version": "5.4.149", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.149.tgz", - "integrity": "sha512-Xe8/1FMJEQPUVSti25AlDpwpUm2QAVmNOpFP0SIahaPIOKBKICaefbzogLdwey3XGGoaP4Lb9wqiw2e9Jqp0LA==", + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=20.16.0 || >=22.3.0" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.77" + "@napi-rs/canvas": "^0.1.80" } }, "node_modules/picomatch": { diff --git a/package.json b/package.json index dfd92ce9..a5d5d12b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,6 @@ "@stylistic/eslint-plugin": "^5.5.0", "esbuild": "^0.25.11", "eslint": "^9.38.0", - "pdfjs-dist": "^5.4.149" + "pdfjs-dist": "^5.4.296" } } From 371611d61f965acde18742f05b2022484705c168 Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 Sep 2025 15:34:20 +0000 Subject: [PATCH 02/10] bump targetSdk to 36 --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 223011cf..1ffe7811 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,7 +48,7 @@ android { defaultConfig { applicationId = "app.grapheneos.pdfviewer" minSdk = 26 - targetSdk = 35 + targetSdk = 36 versionCode = 32 versionName = versionCode.toString() } From 7889f868df9d90448c50eaa75e7ba7e9ab602fab Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 Sep 2025 17:49:58 +0000 Subject: [PATCH 03/10] use new WindowCompat#enableEdgeToEdge API --- app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index 936f7085..c8f6d374 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -279,7 +279,7 @@ private void showWebViewCrashed() { @SuppressLint({"SetJavaScriptEnabled"}) protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + WindowCompat.enableEdgeToEdge(getWindow()); binding = PdfviewerBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); From 884216a974565ab6f04e6dff213977632de167bc Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 Sep 2025 12:45:25 +0000 Subject: [PATCH 04/10] add getViewport function that adjusts rotation based on page.rotate --- viewer/js/index.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/viewer/js/index.js b/viewer/js/index.js index 3eed8fee..c2d65a01 100644 --- a/viewer/js/index.js +++ b/viewer/js/index.js @@ -79,13 +79,17 @@ function setLayerTransform(pageWidth, pageHeight, layerDiv) { } function getDefaultZoomRatio(page, orientationDegrees) { - const totalRotation = (orientationDegrees + page.rotate) % 360; - const viewport = page.getViewport({scale: 1, rotation: totalRotation}); + const viewport = getViewport(page, 1, orientationDegrees); const widthZoomRatio = document.body.clientWidth / viewport.width; const heightZoomRatio = document.body.clientHeight / viewport.height; return Math.max(Math.min(widthZoomRatio, heightZoomRatio, channel.getMaxZoomRatio()), channel.getMinZoomRatio()); } +function getViewport(page, scale, orientationDegrees) { + const rotation = (page.rotate + orientationDegrees) % 360; + return page.getViewport({scale, rotation}); +} + /** * Does BFS traversal of all of the nodes in the outline tree to convert the tree so that the * nodes are of a simpler form. The simple outline nodes have the following structure: @@ -221,8 +225,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { channel.setZoomRatio(defaultZoomRatio); } - const totalRotation = (orientationDegrees + page.rotate) % 360; - const viewport = page.getViewport({scale: newZoomRatio, rotation: totalRotation}); + const viewport = getViewport(page, newZoomRatio, orientationDegrees); const scaleFactor = newZoomRatio / zoomRatio; const ratio = globalThis.devicePixelRatio; @@ -260,10 +263,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { if (renderPixels > maxRenderPixels) { console.log(`resolution ${renderPixels} exceeds maximum allowed ${maxRenderPixels}`); const adjustedScale = Math.sqrt(maxRenderPixels / renderPixels); - newViewport = page.getViewport({ - scale: newZoomRatio * adjustedScale, - rotation: totalRotation - }); + newViewport = getViewport(page, newZoomRatio * adjustedScale, orientationDegrees); } const newCanvas = document.createElement("canvas"); From 9f4d6ca8fd88800ef6e29852d28fdd1594d89320 Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 Sep 2025 12:52:26 +0000 Subject: [PATCH 05/10] only consider width to calculate initial zoom ratio --- viewer/js/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/viewer/js/index.js b/viewer/js/index.js index c2d65a01..676c3d87 100644 --- a/viewer/js/index.js +++ b/viewer/js/index.js @@ -81,8 +81,7 @@ function setLayerTransform(pageWidth, pageHeight, layerDiv) { function getDefaultZoomRatio(page, orientationDegrees) { const viewport = getViewport(page, 1, orientationDegrees); const widthZoomRatio = document.body.clientWidth / viewport.width; - const heightZoomRatio = document.body.clientHeight / viewport.height; - return Math.max(Math.min(widthZoomRatio, heightZoomRatio, channel.getMaxZoomRatio()), channel.getMinZoomRatio()); + return Math.max(Math.min(widthZoomRatio, channel.getMaxZoomRatio()), channel.getMinZoomRatio()); } function getViewport(page, scale, orientationDegrees) { From f82a7a597fdb2065e4e71e400d5914544a467dea Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 Sep 2025 15:02:40 +0000 Subject: [PATCH 06/10] introduce page element The page element is a container for the canvas and text layer and helps to keep the layers aligned. The following changes were also needed: - Only center PDF pages horizontally. Vertical centering was often the cause of text layer misalignment and additional code was often needed to fix the alignment. Moreover, vertical centering doesn't make sense when there are multiple PDF pages within the same DOM. So, drop it completely. - Drop old text layer alignment method. --- viewer/css/pdf_viewer.css | 46 ++++++++++++++++++------------------ viewer/css/text_layer.css | 4 +++- viewer/index.html | 6 +++-- viewer/js/index.js | 49 +++++++++++++-------------------------- 4 files changed, 47 insertions(+), 58 deletions(-) diff --git a/viewer/css/pdf_viewer.css b/viewer/css/pdf_viewer.css index d4b7e477..49b6aebf 100644 --- a/viewer/css/pdf_viewer.css +++ b/viewer/css/pdf_viewer.css @@ -1,53 +1,55 @@ html, body { + width: 100%; height: 100%; } -body, -canvas { - padding: 0; - margin: 0; -} - body { + margin: 0; + padding: 0; background-color: #c0c0c0; } #container { --scale-factor: 1; + + position: fixed; + overflow: auto; + width: 100%; + height: 100%; +} + +#container .page { --user-unit: 1; --total-scale-factor: calc(var(--scale-factor) * var(--user-unit)); --scale-round-x: 1px; --scale-round-y: 1px; - width: 100%; - height: 100%; - display: grid; - place-items: center; -} + --page-width: 0; + --page-height: 0; -#container canvas, -#container .textLayer { - /* overlay child elements on top of each other */ - grid-row-start: 1; - grid-column-start: 1; + position: relative; + width: round(down, var(--total-scale-factor) * var(--page-width), var(--scale-round-x)); + height: round(down, var(--total-scale-factor) * var(--page-height), var(--scale-round-y)); + margin: 0 auto; } -canvas { - display: inline-block; - position: relative; +.page canvas { + position: absolute; + width: 100%; + height: 100%; } [data-main-rotation="90"] { - transform: rotate(90deg); + transform: rotate(90deg) translateY(-100%); } [data-main-rotation="180"] { - transform: rotate(180deg); + transform: rotate(180deg) translate(-100%, -100%); } [data-main-rotation="270"] { - transform: rotate(270deg); + transform: rotate(270deg) translateX(-100%); } .hiddenCanvasElement { diff --git a/viewer/css/text_layer.css b/viewer/css/text_layer.css index b636b99e..9d7c3e07 100644 --- a/viewer/css/text_layer.css +++ b/viewer/css/text_layer.css @@ -3,13 +3,15 @@ } .textLayer { - text-align: initial; position: absolute; + text-align: initial; + inset: 0; overflow: clip; opacity: 1; line-height: 1; text-size-adjust: none; forced-color-adjust: none; + transform-origin: 0 0; caret-color: CanvasText; z-index: 0; } diff --git a/viewer/index.html b/viewer/index.html index 65eefa76..4a975615 100644 --- a/viewer/index.html +++ b/viewer/index.html @@ -8,8 +8,10 @@
- -
+
+ +
+
diff --git a/viewer/js/index.js b/viewer/js/index.js index 676c3d87..2a10eb23 100644 --- a/viewer/js/index.js +++ b/viewer/js/index.js @@ -14,6 +14,7 @@ let renderPending = false; let renderPendingZoom = 0; const canvas = document.getElementById("content"); const container = document.getElementById("container"); +const singlePageView = document.getElementById("single-page-view"); let orientationDegrees = 0; let zoomRatio = 1; let textLayerDiv = document.getElementById("text"); @@ -59,25 +60,19 @@ function doPrerender(pageNumber, prerenderTrigger) { } } -function display(newCanvas, zoom) { +function display(page, newCanvas, zoom, orientationDegrees) { + const viewport = getViewport(page, 1, orientationDegrees); + singlePageView.style.setProperty("--page-width", `${viewport.width}px`); + singlePageView.style.setProperty("--page-height", `${viewport.height}px`); + canvas.height = newCanvas.height; canvas.width = newCanvas.width; - canvas.style.height = newCanvas.style.height; - canvas.style.width = newCanvas.style.width; canvas.getContext("2d", { alpha: false }).drawImage(newCanvas, 0, 0); if (!zoom) { - scrollTo(0, 0); + container.scrollTo(0, 0); } } -function setLayerTransform(pageWidth, pageHeight, layerDiv) { - const translate = { - X: Math.max(0, pageWidth - document.body.clientWidth) / 2, - Y: Math.max(0, pageHeight - document.body.clientHeight) / 2 - }; - layerDiv.style.translate = `${translate.X}px ${translate.Y}px`; -} - function getDefaultZoomRatio(page, orientationDegrees) { const viewport = getViewport(page, 1, orientationDegrees); const widthZoomRatio = document.body.clientWidth / viewport.width; @@ -190,17 +185,16 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { ", orientationDegrees: " + orientationDegrees + ", prerender: " + prerender); for (let i = 0; i < cache.length; i++) { const cached = cache[i]; - if (cached.pageNumber === pageNumber && cached.zoomRatio === newZoomRatio && + if (cached.page.pageNumber === pageNumber && cached.zoomRatio === newZoomRatio && cached.orientationDegrees === orientationDegrees) { if (useRender) { cache.splice(i, 1); cache.push(cached); - display(cached.canvas, zoom); + display(cached.page, cached.canvas, zoom, orientationDegrees); textLayerDiv.replaceWith(cached.textLayerDiv); textLayerDiv = cached.textLayerDiv; - setLayerTransform(cached.pageWidth, cached.pageHeight, textLayerDiv); container.style.setProperty("--scale-factor", newZoomRatio.toString()); textLayerDiv.hidden = false; } @@ -231,8 +225,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { if (useRender) { if (newZoomRatio !== zoomRatio) { - canvas.style.height = viewport.height + "px"; - canvas.style.width = viewport.width + "px"; + container.style.setProperty("--scale-factor", newZoomRatio); } zoomRatio = newZoomRatio; } @@ -242,13 +235,13 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { pageRendering = false; // zoom focus relative to page origin, rather than screen origin - const globalFocusX = channel.getZoomFocusX() / ratio + globalThis.scrollX; - const globalFocusY = channel.getZoomFocusY() / ratio + globalThis.scrollY; + const globalFocusX = channel.getZoomFocusX() / ratio + container.scrollLeft; + const globalFocusY = channel.getZoomFocusY() / ratio + container.scrollTop; const translationFactor = scaleFactor - 1; const scrollX = globalFocusX * translationFactor; const scrollY = globalFocusY * translationFactor; - scrollBy(scrollX, scrollY); + container.scrollBy(scrollX, scrollY); return; } @@ -268,9 +261,6 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { const newCanvas = document.createElement("canvas"); newCanvas.height = newViewport.height * ratio; newCanvas.width = newViewport.width * ratio; - // use original viewport height for CSS zoom - newCanvas.style.height = viewport.height + "px"; - newCanvas.style.width = viewport.width + "px"; const newContext = newCanvas.getContext("2d", { alpha: false }); newContext.scale(ratio, ratio); @@ -287,7 +277,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { if (!useRender || rendered) { return; } - display(newCanvas, zoom); + display(page, newCanvas, zoom, orientationDegrees); rendered = true; } render(); @@ -307,7 +297,6 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { render(); - setLayerTransform(viewport.width, viewport.height, newTextLayerDiv); if (useRender) { textLayerDiv.replaceWith(newTextLayerDiv); textLayerDiv = newTextLayerDiv; @@ -319,13 +308,11 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { cache.shift(); } cache.push({ - pageNumber: pageNumber, + page: page, zoomRatio: newZoomRatio, orientationDegrees: orientationDegrees, canvas: newCanvas, - textLayerDiv: newTextLayerDiv, - pageWidth: viewport.width, - pageHeight: viewport.height + textLayerDiv: newTextLayerDiv }); pageRendering = false; @@ -434,7 +421,3 @@ globalThis.loadDocument = function () { console.error(reason.name + ": " + reason.message); }); }; - -globalThis.onresize = () => { - setLayerTransform(canvas.clientWidth, canvas.clientHeight, textLayerDiv); -}; From 7af5d7223655f6cd194a8b0964e1ab9ce09de76e Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 Sep 2025 16:53:57 +0000 Subject: [PATCH 07/10] place webview behind app toolbar and system bars The WebView now takes all screen space regardless of whether app toolbar is hidden or not. So, we add padding in our javascript viewer to account for system insets and app toolbar size. --- .../app/grapheneos/pdfviewer/PdfViewer.java | 30 +++++++++++++++++++ app/src/main/res/layout/pdfviewer.xml | 11 ++++--- viewer/css/pdf_viewer.css | 12 ++++++-- viewer/js/index.js | 18 ++++++++++- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java index c8f6d374..e91a9094 100644 --- a/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java +++ b/app/src/main/java/app/grapheneos/pdfviewer/PdfViewer.java @@ -29,7 +29,10 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; @@ -38,6 +41,8 @@ import com.google.android.material.snackbar.Snackbar; +import org.json.JSONObject; + import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -265,6 +270,23 @@ public void onLoaded() { public String getPassword() { return mEncryptedDocumentPassword != null ? mEncryptedDocumentPassword : ""; } + + @JavascriptInterface + public String getInsets() { + WindowInsetsCompat wic = ViewCompat.getRootWindowInsets(binding.getRoot()); + int types = WindowInsetsCompat.Type.statusBars() + | WindowInsetsCompat.Type.navigationBars() + | WindowInsetsCompat.Type.displayCutout(); + Insets insets = (wic != null) ? wic.getInsetsIgnoringVisibility(types) : Insets.NONE; + + HashMap insetMap = new HashMap<>(4); + insetMap.put("top", insets.top + binding.toolbar.getHeight()); + insetMap.put("right", insets.right); + insetMap.put("bottom", insets.bottom); + insetMap.put("left", insets.left); + + return new JSONObject(insetMap).toString(); + } } private void showWebViewCrashed() { @@ -284,6 +306,14 @@ protected void onCreate(Bundle savedInstanceState) { binding = PdfviewerBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); + + ViewCompat.setOnApplyWindowInsetsListener(binding.getRoot(), (v, insets) -> { + if (mUri != null) { + binding.webview.evaluateJavascript("updateInsets()", null); + } + return insets; + }); + viewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication())).get(PdfViewModel.class); viewModel.getOutline().observe(this, requested -> { diff --git a/app/src/main/res/layout/pdfviewer.xml b/app/src/main/res/layout/pdfviewer.xml index dc73b98a..7796b6ec 100644 --- a/app/src/main/res/layout/pdfviewer.xml +++ b/app/src/main/res/layout/pdfviewer.xml @@ -4,6 +4,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + - - `${(Number(v) || 0) / ratio}px`; + + container.style.setProperty("--inset-top", toCssPx(insets.top)); + container.style.setProperty("--inset-right", toCssPx(insets.right)); + container.style.setProperty("--inset-bottom", toCssPx(insets.bottom)); + container.style.setProperty("--inset-left", toCssPx(insets.left)); +}; + +addEventListener("DOMContentLoaded", globalThis.updateInsets); From e492668d7638b37d9eb3d772749b9ac0ac220fdd Mon Sep 17 00:00:00 2001 From: octocorvus Date: Fri, 3 Oct 2025 00:18:25 +0000 Subject: [PATCH 08/10] take container padding into account when calculating zoom focus --- viewer/js/index.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/viewer/js/index.js b/viewer/js/index.js index 96632d46..ad34c575 100644 --- a/viewer/js/index.js +++ b/viewer/js/index.js @@ -73,11 +73,21 @@ function display(page, newCanvas, zoom, orientationDegrees) { } } +function getContainerPadding() { + const containerStyle = getComputedStyle(container); + return { + top: parseFloat(containerStyle.paddingTop), + right: parseFloat(containerStyle.paddingRight), + bottom: parseFloat(containerStyle.paddingBottom), + left: parseFloat(containerStyle.paddingLeft) + }; +} + function getDefaultZoomRatio(page, orientationDegrees) { const viewport = getViewport(page, 1, orientationDegrees); - const containerStyle = getComputedStyle(container); - const containerPadding = parseFloat(containerStyle.paddingLeft) + parseFloat(containerStyle.paddingRight); - const containerInnerWidth = container.clientWidth - containerPadding; + const containerPadding = getContainerPadding(); + const containerHorizontalPadding = containerPadding.left + containerPadding.right; + const containerInnerWidth = container.clientWidth - containerHorizontalPadding; const widthZoomRatio = containerInnerWidth / viewport.width; return Math.max(Math.min(widthZoomRatio, channel.getMaxZoomRatio()), channel.getMinZoomRatio()); } @@ -237,9 +247,10 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { textLayerDiv.hidden = true; pageRendering = false; + const containerPadding = getContainerPadding(); // zoom focus relative to page origin, rather than screen origin - const globalFocusX = channel.getZoomFocusX() / ratio + container.scrollLeft; - const globalFocusY = channel.getZoomFocusY() / ratio + container.scrollTop; + const globalFocusX = channel.getZoomFocusX() / ratio + container.scrollLeft - containerPadding.left; + const globalFocusY = channel.getZoomFocusY() / ratio + container.scrollTop - containerPadding.top; const translationFactor = scaleFactor - 1; const scrollX = globalFocusX * translationFactor; From 6ecc0e212c68af5905150c9b30a44b8ec1a9048e Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 Sep 2025 17:07:47 +0000 Subject: [PATCH 09/10] add 8px padding to container edges --- viewer/css/pdf_viewer.css | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/viewer/css/pdf_viewer.css b/viewer/css/pdf_viewer.css index 90c707ac..50666f23 100644 --- a/viewer/css/pdf_viewer.css +++ b/viewer/css/pdf_viewer.css @@ -18,13 +18,15 @@ body { --inset-bottom: 0; --inset-left: 0; + --padding: 8px; + position: fixed; overflow: auto; inset: 0; - padding-top: var(--inset-top); - padding-right: var(--inset-right); - padding-bottom: var(--inset-bottom); - padding-left: var(--inset-left); + padding-top: calc(var(--padding) + var(--inset-top)); + padding-right: calc(var(--padding) + var(--inset-right)); + padding-bottom: calc(var(--padding) + var(--inset-bottom)); + padding-left: calc(var(--padding) + var(--inset-left)); } #container .page { From 0c59776216767c7073a00d730a7415d051c00d4d Mon Sep 17 00:00:00 2001 From: octocorvus Date: Thu, 4 Sep 2025 19:22:22 +0000 Subject: [PATCH 10/10] set --scale-factor property on container before call to display() Prevents jank which happens due to --scale-factor not being set to correct value before canvas is displayed. --- viewer/js/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/viewer/js/index.js b/viewer/js/index.js index ad34c575..d502681d 100644 --- a/viewer/js/index.js +++ b/viewer/js/index.js @@ -204,11 +204,11 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { cache.splice(i, 1); cache.push(cached); + container.style.setProperty("--scale-factor", newZoomRatio.toString()); display(cached.page, cached.canvas, zoom, orientationDegrees); textLayerDiv.replaceWith(cached.textLayerDiv); textLayerDiv = cached.textLayerDiv; - container.style.setProperty("--scale-factor", newZoomRatio.toString()); textLayerDiv.hidden = false; } @@ -291,6 +291,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { if (!useRender || rendered) { return; } + container.style.setProperty("--scale-factor", newZoomRatio.toString()); display(page, newCanvas, zoom, orientationDegrees); rendered = true; } @@ -314,7 +315,6 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger = 0) { if (useRender) { textLayerDiv.replaceWith(newTextLayerDiv); textLayerDiv = newTextLayerDiv; - container.style.setProperty("--scale-factor", newZoomRatio.toString()); textLayerDiv.hidden = false; }