libpdfviewer: update to PdfViewer 19

This commit is contained in:
Matéo Duparc 2024-06-06 13:43:13 +02:00
commit 7e01d2b224
Signed by: hardcoresushi
GPG Key ID: AFE384344A45E13A
23 changed files with 3514 additions and 959 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ keystore.properties
*.keystore
/.idea
/releases
/node_modules
/app/src/*/assets/viewer

View File

@ -1,4 +1,4 @@
Copyright © 2017-2023 GrapheneOS
Copyright © 2017-2024 GrapheneOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,3 +1,4 @@
import org.apache.tools.ant.taskdefs.condition.Os
import java.util.Properties
import java.io.FileInputStream
@ -38,14 +39,13 @@ android {
}
}
compileSdk = 33
buildToolsVersion = "33.0.2"
compileSdk = 34
buildToolsVersion = "34.0.0"
namespace = "app.grapheneos.pdfviewer"
defaultConfig {
minSdk = 21
targetSdk = 33
resourceConfigurations.add("en")
}
@ -70,18 +70,37 @@ android {
buildConfig = true
}
}
compileOptions {
sourceCompatibility(JavaVersion.VERSION_17)
targetCompatibility(JavaVersion.VERSION_17)
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
}
dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.core:core:1.13.1")
implementation("com.google.android.material:material:1.12.0")
}
fun getCommand(command: String, winExt: String = "cmd"): String {
return if (Os.isFamily(Os.FAMILY_WINDOWS)) "$command.$winExt" else command
}
val npmSetup = tasks.register("npmSetup", Exec::class) {
workingDir = projectDir
commandLine(getCommand("npm"), "ci", "--ignore-scripts")
}
val processStatic = tasks.register("processStatic", Exec::class) {
workingDir = projectDir.parentFile
dependsOn(npmSetup)
commandLine(getCommand("node", "exe"), "process_static.js")
}
val cleanStatic = tasks.register("cleanStatic", Delete::class) {
delete("src/main/assets/viewer", "src/debug/assets/viewer")
}
tasks.preBuild {
dependsOn(processStatic)
}
tasks.clean {
dependsOn(cleanStatic)
}

View File

@ -1 +0,0 @@
../../../pdfjs-dist/build/pdf.min.js

View File

@ -1 +0,0 @@
../../../pdfjs-dist/build/pdf.worker.min.js

View File

@ -268,7 +268,7 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
return new WebResourceResponse("application/pdf", null, mInputStream);
}
if ("/viewer.html".equals(path)) {
if ("/viewer/index.html".equals(path)) {
final WebResourceResponse response = fromAsset("text/html", path);
HashMap<String, String> headers = new HashMap<>();
headers.put("Content-Security-Policy", CONTENT_SECURITY_POLICY);
@ -278,11 +278,11 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
return response;
}
if ("/viewer.css".equals(path)) {
if ("/viewer/main.css".equals(path)) {
return fromAsset("text/css", path);
}
if ("/viewer.js".equals(path) || "/pdf.js".equals(path) || "/pdf.worker.js".equals(path)) {
if ("/viewer/js/index.js".equals(path) || "/viewer/js/worker.js".equals(path)) {
return fromAsset("application/javascript", path);
}
@ -428,7 +428,7 @@ public class PdfViewer implements LoaderManager.LoaderCallbacks<List<CharSequenc
this.fileSize = fileSize;
showSystemUi();
activity.invalidateOptionsMenu();
binding.webview.loadUrl("https://localhost/viewer.html");
binding.webview.loadUrl("https://localhost/viewer/index.html");
}
public void loadPdfWithPassword(final String password) {

View File

@ -155,13 +155,13 @@ public class Utils {
throw new ParseException("Invalid UTC offset minutes", position);
}
position += 2;
}
// Apostrophe shall succeed mm
if (date.charAt(position) != '\'') {
throw new ParseException("Expected apostrophe", position);
}
}
}
switch (utRel) {

View File

@ -1,6 +1,6 @@
plugins {
id("com.android.application") apply false
id("org.jetbrains.kotlin.android") apply false
id("com.android.application") version "8.4.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
}
allprojects {
@ -12,5 +12,5 @@ allprojects {
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
delete(rootProject.layout.buildDirectory)
}

33
eslint.config.js Normal file
View File

@ -0,0 +1,33 @@
import js from "@eslint/js";
import globals from "globals";
export default [
js.configs.recommended,
{
languageOptions: {
globals: {
channel: "readonly",
...globals.browser
},
ecmaVersion: 2022,
sourceType: "module"
},
rules: {
indent: ["error", 4],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
"no-unused-vars": ["error", {caughtErrors: "none"}],
"no-var": ["error"]
},
},
{
ignores: [
"app/build/",
"app/src/*/assets/viewer",
"build/",
"releases/"
]
}
];

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

17
gradlew vendored
View File

@ -83,7 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -201,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat vendored
View File

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

2115
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"type": "module",
"devDependencies": {
"esbuild": "^0.21.1",
"eslint": "^9.2.0",
"pdfjs-dist": "^4.2.67"
}
}

107
process_static.js Normal file
View File

@ -0,0 +1,107 @@
import esbuild from "esbuild";
import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
/**
* @typedef ProcessOptions
* @property {string} rootDir
* @property {string[]} entryPoints
* @property {string} outDir
* @property {boolean} production
*/
async function processStatic() {
const rootDir = "viewer";
const outDir = "app/src/main/assets/viewer";
const outDirDebug = "app/src/debug/assets/viewer";
await commandLine(getCommand("node_modules/.bin/eslint"), ".");
await processScripts({
rootDir,
entryPoints: ["js/index.js", "js/worker.js"],
outDir,
production: true,
});
await processScripts({
rootDir,
entryPoints: ["js/index.js", "js/worker.js"],
outDir: outDirDebug,
production: false,
});
await processStyles({
rootDir,
entryPoints: ["main.css"],
outDir,
production: true,
});
await processHtml({
rootDir,
entryPoints: ["index.html"],
outDir,
production: true,
});
}
/**
* @param {string} command
* @param {string} winExt
* @returns {string}
*/
function getCommand(command, winExt = "cmd") {
return path.resolve(globalThis.process.platform === "win32" ? `${command}.${winExt}` : command);
}
/**
* @param {string} command
* @param {...string} args
* @returns {Promise<void>}
*/
function commandLine(command, ...args) {
return new Promise((resolve, reject) => {
const subprocess = spawn(command, args, { shell: false, stdio: "inherit" });
subprocess.on("close", (code) => code === 0 ? resolve() : reject());
});
}
/**
* @param {ProcessOptions} options
*/
async function processScripts(options) {
const entryPoints = options.entryPoints.map((filepath) => path.join(options.rootDir, filepath));
await esbuild.build({
entryPoints,
bundle: true,
format: "esm",
platform: "browser",
target: "es2022",
outdir: path.join(options.outDir, "js"),
minify: options.production,
sourcemap: options.production ? false : "inline",
});
}
/**
* @param {ProcessOptions} options
*/
async function processStyles(options) {
const entryPoints = options.entryPoints.map((filepath) => path.join(options.rootDir, filepath));
await esbuild.build({
entryPoints,
bundle: true,
outdir: options.outDir,
minify: options.production,
});
}
/**
* @param {ProcessOptions} options
*/
async function processHtml(options) {
for (const entryPoint of options.entryPoints) {
await fs.copyFile(path.join(options.rootDir, entryPoint), path.join(options.outDir, entryPoint));
}
}
await processStatic();

56
viewer/css/pdf_viewer.css Normal file
View File

@ -0,0 +1,56 @@
html,
body {
height: 100%;
}
body,
canvas {
padding: 0;
margin: 0;
}
body {
background-color: #c0c0c0;
}
#container {
--scale-factor: 1;
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
#container canvas,
#container .textLayer {
/* overlay child elements on top of each other */
grid-row-start: 1;
grid-column-start: 1;
}
canvas {
display: inline-block;
position: relative;
}
[data-main-rotation="90"] {
transform: rotate(90deg);
}
[data-main-rotation="180"] {
transform: rotate(180deg);
}
[data-main-rotation="270"] {
transform: rotate(270deg);
}
.hiddenCanvasElement {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
display: none;
}

View File

@ -3,39 +3,6 @@
--text-layer-foreground: transparent;
}
html, body {
height: 100%;
}
body, canvas {
padding: 0;
margin: 0;
}
body {
background-color: #c0c0c0;
}
#container {
--scale-factor: 1;
width: 100%;
height: 100%;
display: grid;
place-items: center;
}
#container canvas, #container .textLayer {
/* overlay child elements on top of each other */
grid-row-start: 1;
grid-column-start: 1;
}
canvas {
display: inline-block;
position: relative;
}
.textLayer {
text-align: initial;
position: absolute;
@ -99,13 +66,3 @@ canvas {
.textLayer .endOfContent.active {
top: 0;
}
[data-main-rotation="90"] {
transform: rotate(90deg);
}
[data-main-rotation="180"] {
transform: rotate(180deg);
}
[data-main-rotation="270"] {
transform: rotate(270deg);
}

View File

@ -3,14 +3,13 @@
<head>
<meta name="viewport" content="width=device-width, user-scalable=no" />
<title>PDF</title>
<link rel="stylesheet" href="viewer.css" />
<script src="pdf.js"></script>
<link rel="stylesheet" href="/viewer/main.css">
<script type="module" src="/viewer/js/index.js"></script>
</head>
<body>
<div id="container">
<canvas id="content"></canvas>
<div id="text" class="textLayer"></div>
</div>
<script src="viewer.js"></script>
</body>
</html>

View File

@ -1,6 +1,11 @@
"use strict";
import {
GlobalWorkerOptions,
PasswordResponses,
getDocument,
renderTextLayer,
} from "pdfjs-dist";
pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdf.worker.js";
GlobalWorkerOptions.workerSrc = "/viewer/js/worker.js";
let pdfDoc = null;
let pageRendering = false;
@ -139,7 +144,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
}
const newCanvas = document.createElement("canvas");
const ratio = window.devicePixelRatio;
const ratio = globalThis.devicePixelRatio;
newCanvas.height = viewport.height * ratio;
newCanvas.width = viewport.width * ratio;
newCanvas.style.height = viewport.height + "px";
@ -166,7 +171,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
render();
const newTextLayerDiv = textLayerDiv.cloneNode();
task = pdfjsLib.renderTextLayer({
task = renderTextLayer({
textContentSource: page.streamTextContent(),
container: newTextLayerDiv,
viewport: viewport
@ -215,7 +220,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
});
}
function onRenderPage(zoom) {
globalThis.onRenderPage = function (zoom) {
if (pageRendering) {
if (newPageNumber === channel.getPage() && newZoomRatio === channel.getZoomRatio() &&
orientationDegrees === channel.getDocumentOrientationDegrees()) {
@ -232,13 +237,13 @@ function onRenderPage(zoom) {
} else {
renderPage(channel.getPage(), zoom, false);
}
}
};
function isTextSelected() {
return window.getSelection().toString() !== "";
}
globalThis.isTextSelected = function () {
return globalThis.getSelection().toString() !== "";
};
function toggleTextLayerVisibility() {
globalThis.toggleTextLayerVisibility = function () {
let textLayerForeground = "red";
let textLayerOpacity = 1;
if (isTextLayerVisible) {
@ -248,15 +253,15 @@ function toggleTextLayerVisibility() {
document.documentElement.style.setProperty("--text-layer-foreground", textLayerForeground);
document.documentElement.style.setProperty("--text-layer-opacity", textLayerOpacity.toString());
isTextLayerVisible = !isTextLayerVisible;
}
};
function loadDocument() {
globalThis.loadDocument = function () {
const pdfPassword = channel.getPassword();
const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
const loadingTask = getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
loadingTask.onPassword = (_, error) => {
if (error === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
if (error === PasswordResponses.NEED_PASSWORD) {
channel.showPasswordPrompt();
} else if (error === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
} else if (error === PasswordResponses.INCORRECT_PASSWORD) {
channel.invalidPassword();
}
};
@ -274,8 +279,8 @@ function loadDocument() {
}, function (reason) {
console.error(reason.name + ": " + reason.message);
});
}
};
window.onresize = () => {
globalThis.onresize = () => {
setLayerTransform(canvas.clientWidth, canvas.clientHeight, textLayerDiv);
};

1
viewer/js/worker.js Normal file
View File

@ -0,0 +1 @@
import "pdfjs-dist/build/pdf.worker.mjs";

2
viewer/main.css Normal file
View File

@ -0,0 +1,2 @@
@import url(css/pdf_viewer.css);
@import url(css/text_layer.css);