From 1d5a7e116c7b2d326f0f15cbf42e5d98eea390c8 Mon Sep 17 00:00:00 2001 From: octocorvus Date: Sun, 5 Oct 2025 17:12:03 +0000 Subject: [PATCH 01/10] update pdf.js to to 5.4.296 --- package-lock.json | 98 ++++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index a29d1a88..fd46ab92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "@stylistic/eslint-plugin": "^5.4.0", "esbuild": "^0.25.10", "eslint": "^9.37.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" ], @@ -914,6 +914,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1144,6 +1145,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1660,16 +1662,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 c6c4292e..173db666 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,6 @@ "@stylistic/eslint-plugin": "^5.4.0", "esbuild": "^0.25.10", "eslint": "^9.37.0", - "pdfjs-dist": "^5.4.149" + "pdfjs-dist": "^5.4.296" } } From 777bca4e4b0eaba14e4c01f61b46dd9c6b98bbbf 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 de4acf4c3f8ef412ef6b2f0162432aa9fab3bdcb 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 6e79da9b2dff995ab4e2adf5e3513f9028c1eef5 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 1f1b964b1829199082a25b2d89dac52c5ffe0f93 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 9dfb04651a29353d0c6f49bf4d35f342962b8213 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 f4e3ce161f70abc8641e5f147ae70800aed10fb9 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 92732ecff90479cd797a3f31d38831a9b5860294 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 dd5033b2ebd850f57dd4e65ab4fa0d55e90090bf 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 4006e0c983ed6466f810df026020c6c0b79b8ac3 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; }