Compare commits

...

22 Commits

Author SHA1 Message Date
Daniel Micay 29a004091e minor whitespace fixes 2023-04-22 09:33:16 -04:00
Pratyush 195bba7891 replace custom file size parser with AOSP implementation 2023-04-20 11:18:14 -04:00
Pratyush 17c7c84296 rewrite DocumentPropertiesLoader in kotlin 2023-04-20 11:18:14 -04:00
Pratyush 61607858ef convert remaining fragments to kotlin 2023-04-20 11:18:11 -04:00
octocorvus fb59568765 update Android Gradle Plugin to 8.0.0
fix: update JDK to 17 for AGP 8.0
fix: enable buildConfig build feature
refactor: remove unnecessary android.enableR8.fullMode
2023-04-14 08:37:42 -04:00
Patryk Mis 4d1807718e Update Gradle to 8.1 2023-04-12 12:57:17 -04:00
Daniel Micay a6b4144a08 drop legacy GitHub Actions submodule checkout 2023-04-10 01:33:57 -04:00
Daniel Micay 5c8c4d7d83 update GitHub Actions JDK to 20 2023-04-10 01:33:27 -04:00
dependabot[bot] e161b71d22 Bump pdfjs-dist from 3.4.120 to 3.5.141
Bumps [pdfjs-dist](https://github.com/mozilla/pdfjs-dist) from 3.4.120 to 3.5.141.
- [Release notes](https://github.com/mozilla/pdfjs-dist/releases)
- [Commits](https://github.com/mozilla/pdfjs-dist/commits)

---
updated-dependencies:
- dependency-name: pdfjs-dist
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-09 10:40:16 -04:00
Daniel Micay 759417f4da update Kotlin to 1.8.20 2023-04-09 09:34:51 -04:00
octocorvus bb14ba1a25 bugfix: make text layer position absolute and use CSS transform to al...
...ign it to canvas

Fixes a bug where there was empty space above pdf page, when page
rotation is 90 or 270 degrees.

Testing: open pdf and try all rotations (0, 90, 180 and 270 degrees)
with various zoom levels (especially, max and min zoom levels), and make
sure that text layer is aligned properly to the canvas and there aren't
any unwanted blank spaces. Finally, repeat the same testing procedure
for landscape mode.

To check if text layer is aligned properly on debug builds, toggle text
layer visibility from menu or use chrome dev tools.
2023-04-09 09:32:58 -04:00
Daniel Micay a59e72d9e0 add setup script 2023-03-24 19:46:52 -04:00
Daniel Micay f2b0162630 remove empty .gitmodules 2023-03-24 19:46:11 -04:00
octocorvus ae1c0874ce add option to toggle text layer visibility on debug builds
This is to ease debugging text layer on debug builds.
2023-03-24 19:45:49 -04:00
octocorvus d445c48f3c code style changes
- js: use double quotes consistently
- js: use strict equality operator everywhere
- js: end all statements with semi colon
- css: remove redundant unit of measures
2023-03-24 19:45:49 -04:00
octocorvus 69696ae2a9 use CSS transform to rotate text layer
Newer pdf.js versions don't automatically rotate text layer based on
viewport's rotation, instead they now set the value of the attribute
"data-main-rotation" to value of rotation in degrees. So, we now apply
CSS transformation to the text layer depending on the value of that
attribute.
2023-03-24 19:45:49 -04:00
octocorvus 2dea11799c use --scale-factor variable to set text layer font size
Newer pdf.js versions requires this variable to be set equal to the
actual scale (zoom ratio), to properly set font size in text layer div.
2023-03-24 19:45:49 -04:00
octocorvus f5a1452a2d migrate away from deprecated APIs
- renderTextLayer: use textContentSource parameter instead of textContentStream
- renderTextLayer: pass a div for the container argument
2023-03-24 19:45:49 -04:00
octocorvus f87941ea22 update pdf.js to v3.4.120 2023-03-24 19:45:49 -04:00
octocorvus 371c9509f6 update GitHub workflow to install npm dependencies 2023-03-24 19:45:49 -04:00
octocorvus d01131d4c6 use dependabot for npm dependencies 2023-03-24 19:45:49 -04:00
octocorvus 2935bd4b27 migrate to pdfjs-dist npm package 2023-03-24 19:45:49 -04:00
33 changed files with 2353 additions and 1249 deletions

View File

@ -10,3 +10,8 @@ updates:
schedule:
interval: daily
target-branch: main
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
target-branch: main

View File

@ -8,13 +8,16 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
submodules: true
- name: Set up JDK 19
cache: npm
- name: Set up JDK 20
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 19
java-version: 20
cache: gradle
- run: npm ci --ignore-scripts
- name: Build with Gradle
run: ./gradlew build --no-daemon

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ keystore.properties
*.keystore
/.idea
/releases
/node_modules

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "pdfjs-dist"]
path = third_party/pdfjs-dist
url = https://github.com/mozilla/pdfjs-dist.git

View File

@ -15,7 +15,7 @@ plugins {
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
languageVersion.set(JavaLanguageVersion.of(17))
}
}
@ -76,16 +76,17 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
}
}
compileOptions {
sourceCompatibility(JavaVersion.VERSION_11)
targetCompatibility(JavaVersion.VERSION_11)
sourceCompatibility(JavaVersion.VERSION_17)
targetCompatibility(JavaVersion.VERSION_17)
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
jvmTarget = JavaVersion.VERSION_17.toString()
}
}

View File

@ -1 +1 @@
../../../../third_party/pdfjs-dist/build/pdf.min.js
../../../../node_modules/pdfjs-dist/build/pdf.min.js

View File

@ -1 +1 @@
../../../../third_party/pdfjs-dist/build/pdf.worker.min.js
../../../../node_modules/pdfjs-dist/build/pdf.worker.min.js

View File

@ -1,3 +1,8 @@
:root {
--text-layer-opacity: 0.2;
--text-layer-foreground: transparent;
}
html, body {
height: 100%;
}
@ -12,6 +17,8 @@ body {
}
#container {
--scale-factor: 1;
width: 100%;
height: 100%;
display: grid;
@ -24,25 +31,26 @@ body {
grid-column-start: 1;
}
canvas, .textLayer {
canvas {
display: inline-block;
position: relative;
}
.textLayer {
text-align: initial;
position: absolute;
overflow: hidden;
opacity: 0.2;
opacity: var(--text-layer-opacity);
line-height: 1;
}
.textLayer span,
.textLayer br {
color: transparent;
color: var(--text-layer-foreground);
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
transform-origin: 0 0;
}
.textLayer .highlight {
@ -91,3 +99,13 @@ canvas, .textLayer {
.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

@ -6,7 +6,8 @@ let pdfDoc = null;
let pageRendering = false;
let renderPending = false;
let renderPendingZoom = 0;
const canvas = document.getElementById('content');
const canvas = document.getElementById("content");
const container = document.getElementById("container");
let orientationDegrees = 0;
let zoomRatio = 1;
let textLayerDiv = document.getElementById("text");
@ -19,6 +20,8 @@ let useRender;
const cache = [];
const maxCached = 6;
let isTextLayerVisible = false;
function maybeRenderNextPage() {
if (renderPending) {
pageRendering = false;
@ -61,6 +64,14 @@ function display(newCanvas, zoom) {
}
}
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 renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
pageRendering = true;
useRender = !prerender;
@ -82,6 +93,8 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
textLayerDiv.replaceWith(cached.textLayerDiv);
textLayerDiv = cached.textLayerDiv;
setLayerTransform(cached.pageWidth, cached.pageHeight, textLayerDiv);
container.style.setProperty("--scale-factor", newZoomRatio.toString());
}
pageRendering = false;
@ -95,7 +108,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
return;
}
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees})
const viewport = page.getViewport({scale: newZoomRatio, rotation: orientationDegrees});
if (useRender) {
if (newZoomRatio !== zoomRatio) {
@ -105,7 +118,7 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
zoomRatio = newZoomRatio;
}
if (zoom == 2) {
if (zoom === 2) {
pageRendering = false;
return;
}
@ -137,10 +150,10 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
}
render();
const textLayerFrag = document.createDocumentFragment();
const newTextLayerDiv = textLayerDiv.cloneNode();
task = pdfjsLib.renderTextLayer({
textContentStream: page.streamTextContent(),
container: textLayerFrag,
textContentSource: page.streamTextContent(),
container: newTextLayerDiv,
viewport: viewport
});
task.promise.then(function() {
@ -148,14 +161,24 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
render();
const newTextLayerDiv = textLayerDiv.cloneNode();
newTextLayerDiv.style.height = newCanvas.style.height;
newTextLayerDiv.style.width = newCanvas.style.width;
// We use CSS transform to rotate a text layer div of zero
// degrees rotation. So, when the rotation is 90 or 270
// degrees, set width and height of the text layer div to the
// height and width of the canvas, respectively, to prevent
// text layer misalignment.
if (orientationDegrees % 180 === 0) {
newTextLayerDiv.style.height = newCanvas.style.height;
newTextLayerDiv.style.width = newCanvas.style.width;
} else {
newTextLayerDiv.style.height = newCanvas.style.width;
newTextLayerDiv.style.width = newCanvas.style.height;
}
setLayerTransform(viewport.width, viewport.height, newTextLayerDiv);
if (useRender) {
textLayerDiv.replaceWith(newTextLayerDiv);
textLayerDiv = newTextLayerDiv;
container.style.setProperty("--scale-factor", newZoomRatio.toString());
}
newTextLayerDiv.appendChild(textLayerFrag);
if (cache.length === maxCached) {
cache.shift()
@ -165,7 +188,9 @@ function renderPage(pageNumber, zoom, prerender, prerenderTrigger=0) {
zoomRatio: newZoomRatio,
orientationDegrees: orientationDegrees,
canvas: newCanvas,
textLayerDiv: newTextLayerDiv
textLayerDiv: newTextLayerDiv,
pageWidth: viewport.width,
pageHeight: viewport.height
});
pageRendering = false;
@ -198,6 +223,18 @@ function isTextSelected() {
return window.getSelection().toString() !== "";
}
function toggleTextLayerVisibility() {
let textLayerForeground = "red";
let textLayerOpacity = 1;
if (isTextLayerVisible) {
textLayerForeground = "transparent";
textLayerOpacity = 0.2;
}
document.documentElement.style.setProperty("--text-layer-foreground", textLayerForeground);
document.documentElement.style.setProperty("--text-layer-opacity", textLayerOpacity.toString());
isTextLayerVisible = !isTextLayerVisible;
}
function loadDocument() {
const pdfPassword = channel.getPassword();
const loadingTask = pdfjsLib.getDocument({ url: "https://localhost/placeholder.pdf", password: pdfPassword });
@ -223,3 +260,7 @@ function loadDocument() {
console.error(reason.name + ": " + reason.message);
});
}
window.onresize = () => {
setLayerTransform(canvas.clientWidth, canvas.clientHeight, textLayerDiv);
}

View File

@ -45,12 +45,14 @@ import app.grapheneos.pdfviewer.fragment.DocumentPropertiesFragment;
import app.grapheneos.pdfviewer.fragment.PasswordPromptFragment;
import app.grapheneos.pdfviewer.fragment.JumpToPageFragment;
import app.grapheneos.pdfviewer.ktx.ViewKt;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesLoader;
import app.grapheneos.pdfviewer.loader.DocumentPropertiesAsyncTaskLoader;
import app.grapheneos.pdfviewer.viewModel.PasswordStatus;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@ -189,7 +191,7 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
final Bundle args = new Bundle();
args.putString(KEY_PROPERTIES, properties);
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesLoader.ID, args, PdfViewer.this));
runOnUiThread(() -> LoaderManager.getInstance(PdfViewer.this).restartLoader(DocumentPropertiesAsyncTaskLoader.ID, args, PdfViewer.this));
}
@JavascriptInterface
@ -474,14 +476,14 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
@NonNull
@Override
public Loader<List<CharSequence>> onCreateLoader(int id, Bundle args) {
return new DocumentPropertiesLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
return new DocumentPropertiesAsyncTaskLoader(this, args.getString(KEY_PROPERTIES), mNumPages, mUri);
}
@Override
public void onLoadFinished(@NonNull Loader<List<CharSequence>> loader, List<CharSequence> data) {
mDocumentProperties = data;
setToolbarTitleWithDocumentName();
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesLoader.ID);
LoaderManager.getInstance(this).destroyLoader(DocumentPropertiesAsyncTaskLoader.ID);
}
@Override
@ -618,15 +620,21 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.pdf_viewer, menu);
if (BuildConfig.DEBUG) {
inflater.inflate(R.menu.pdf_viewer_debug, menu);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(@NonNull Menu menu) {
final int[] ids = {R.id.action_jump_to_page, R.id.action_next, R.id.action_previous,
R.id.action_first, R.id.action_last, R.id.action_rotate_clockwise,
R.id.action_rotate_counterclockwise, R.id.action_view_document_properties,
R.id.action_share, R.id.action_save_as};
final ArrayList<Integer> ids = new ArrayList<>(Arrays.asList(R.id.action_jump_to_page,
R.id.action_next, R.id.action_previous, R.id.action_first, R.id.action_last,
R.id.action_rotate_clockwise, R.id.action_rotate_counterclockwise,
R.id.action_view_document_properties, R.id.action_share, R.id.action_save_as));
if (BuildConfig.DEBUG) {
ids.add(R.id.debug_action_toggle_text_layer_visibility);
}
if (mDocumentState < STATE_LOADED) {
for (final int id : ids) {
final MenuItem item = menu.findItem(id);
@ -691,6 +699,9 @@ public class PdfViewer extends AppCompatActivity implements LoaderManager.Loader
return true;
} else if (itemId == R.id.action_save_as) {
saveDocument();
} else if (itemId == R.id.debug_action_toggle_text_layer_visibility) {
binding.webview.evaluateJavascript("toggleTextLayerVisibility()", null);
return true;
}
return super.onOptionsItemSelected(item);

View File

@ -9,21 +9,6 @@ import java.text.ParseException;
import java.util.Calendar;
public class Utils {
public static String parseFileSize(long fileSize) {
final double kb = fileSize / 1000d;
if (kb == 0d) {
return fileSize + " Bytes";
}
final DecimalFormat format = new DecimalFormat("#.##");
format.setRoundingMode(RoundingMode.CEILING);
if (kb < 1000) {
return format.format(kb) + " kB (" + fileSize + " Bytes)";
}
return format.format(kb / 1000) + " MB (" + fileSize + " Bytes)";
}
private static int parseIntSafely(String field) throws ParseException {
try {

View File

@ -1,60 +0,0 @@
package app.grapheneos.pdfviewer.fragment;
import android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import app.grapheneos.pdfviewer.R;
import java.util.ArrayList;
import java.util.List;
public class DocumentPropertiesFragment extends DialogFragment {
public static final String TAG = "DocumentPropertiesFragment";
private static final String KEY_DOCUMENT_PROPERTIES = "document_properties";
private List<String> mDocumentProperties;
public static DocumentPropertiesFragment newInstance(final List<CharSequence> metaData) {
final DocumentPropertiesFragment fragment = new DocumentPropertiesFragment();
final Bundle args = new Bundle();
args.putCharSequenceArrayList(KEY_DOCUMENT_PROPERTIES, (ArrayList<CharSequence>) metaData);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mDocumentProperties = getArguments().getStringArrayList(KEY_DOCUMENT_PROPERTIES);
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity activity = requireActivity();
final MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(activity)
.setPositiveButton(android.R.string.ok, null);
if (mDocumentProperties != null) {
dialog.setTitle(getString(R.string.action_view_document_properties));
dialog.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1,
mDocumentProperties), null);
} else {
dialog.setTitle(R.string.document_properties_retrieval_failed);
}
return dialog.create();
}
}

View File

@ -0,0 +1,53 @@
package app.grapheneos.pdfviewer.fragment
import android.app.Dialog
import android.os.Bundle
import android.widget.ArrayAdapter
import androidx.fragment.app.DialogFragment
import app.grapheneos.pdfviewer.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class DocumentPropertiesFragment : DialogFragment() {
// TODO replace with nav args once the `PdfViewer` activity is converted to kotlin
private val mDocumentProperties: List<String> by lazy {
requireArguments().getStringArrayList(KEY_DOCUMENT_PROPERTIES)?.toList() ?: emptyList()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireActivity())
.setPositiveButton(android.R.string.ok, null).apply {
if (mDocumentProperties.isNotEmpty()) {
setTitle(getString(R.string.action_view_document_properties))
setAdapter(
ArrayAdapter(
requireActivity(),
android.R.layout.simple_list_item_1,
mDocumentProperties
), null
)
} else {
setTitle(R.string.document_properties_retrieval_failed)
}
}
.create()
}
companion object {
const val TAG = "DocumentPropertiesFragment"
private const val KEY_DOCUMENT_PROPERTIES = "document_properties"
@JvmStatic
fun newInstance(metaData: List<CharSequence>): DocumentPropertiesFragment {
val fragment = DocumentPropertiesFragment()
val args = Bundle()
args.putCharSequenceArrayList(
KEY_DOCUMENT_PROPERTIES,
metaData as ArrayList<CharSequence>
)
fragment.arguments = args
return fragment
}
}
}

View File

@ -1,61 +0,0 @@
package app.grapheneos.pdfviewer.fragment;
import android.app.Dialog;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import app.grapheneos.pdfviewer.PdfViewer;
public class JumpToPageFragment extends DialogFragment {
public static final String TAG = "JumpToPageFragment";
private final static String STATE_PICKER_CUR = "picker_cur";
private final static String STATE_PICKER_MIN = "picker_min";
private final static String STATE_PICKER_MAX = "picker_max";
private NumberPicker mPicker;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mPicker = new NumberPicker(getActivity());
if (savedInstanceState != null) {
mPicker.setMinValue(savedInstanceState.getInt(STATE_PICKER_MIN));
mPicker.setMaxValue(savedInstanceState.getInt(STATE_PICKER_MAX));
mPicker.setValue(savedInstanceState.getInt(STATE_PICKER_CUR));
} else {
mPicker.setMinValue(1);
mPicker.setMaxValue(((PdfViewer)requireActivity()).mNumPages);
mPicker.setValue(((PdfViewer)requireActivity()).mPage);
}
final FrameLayout layout = new FrameLayout(getActivity());
layout.addView(mPicker, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER));
return new MaterialAlertDialogBuilder(requireActivity())
.setView(layout)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
mPicker.clearFocus();
((PdfViewer)requireActivity()).onJumpToPageInDocument(mPicker.getValue());
})
.setNegativeButton(android.R.string.cancel, null)
.create();
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt(STATE_PICKER_MIN, mPicker.getMinValue());
outState.putInt(STATE_PICKER_MAX, mPicker.getMaxValue());
outState.putInt(STATE_PICKER_CUR, mPicker.getValue());
}
}

View File

@ -0,0 +1,60 @@
package app.grapheneos.pdfviewer.fragment
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.Gravity
import android.widget.FrameLayout
import android.widget.NumberPicker
import androidx.fragment.app.DialogFragment
import app.grapheneos.pdfviewer.PdfViewer
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class JumpToPageFragment : DialogFragment() {
companion object {
const val TAG = "JumpToPageFragment"
private const val STATE_PICKER_CUR = "picker_cur"
private const val STATE_PICKER_MIN = "picker_min"
private const val STATE_PICKER_MAX = "picker_max"
}
private val mPicker: NumberPicker by lazy { NumberPicker(requireActivity()) }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val viewerActivity: PdfViewer = (requireActivity() as PdfViewer)
if (savedInstanceState != null) {
mPicker.minValue = savedInstanceState.getInt(STATE_PICKER_MIN)
mPicker.maxValue = savedInstanceState.getInt(STATE_PICKER_MAX)
mPicker.value = savedInstanceState.getInt(STATE_PICKER_CUR)
} else {
mPicker.minValue = 1
mPicker.maxValue = viewerActivity.mNumPages
mPicker.value = viewerActivity.mPage
}
val layout = FrameLayout(requireActivity())
layout.addView(
mPicker, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.CENTER
)
)
return MaterialAlertDialogBuilder(requireActivity())
.setView(layout)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
mPicker.clearFocus()
viewerActivity.onJumpToPageInDocument(mPicker.value)
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(STATE_PICKER_MIN, mPicker.minValue)
outState.putInt(STATE_PICKER_MAX, mPicker.maxValue)
outState.putInt(STATE_PICKER_CUR, mPicker.value)
}
}

View File

@ -0,0 +1,48 @@
package app.grapheneos.pdfviewer.loader;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.loader.content.AsyncTaskLoader;
import java.util.List;
public class DocumentPropertiesAsyncTaskLoader extends AsyncTaskLoader<List<CharSequence>> {
public static final String TAG = "DocumentPropertiesLoader";
public static final int ID = 1;
private final String mProperties;
private final int mNumPages;
private final Uri mUri;
public DocumentPropertiesAsyncTaskLoader(Context context, String properties, int numPages, Uri uri) {
super(context);
mProperties = properties;
mNumPages = numPages;
mUri = uri;
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Nullable
@Override
public List<CharSequence> loadInBackground() {
DocumentPropertiesLoader loader = new DocumentPropertiesLoader(
getContext(),
mProperties,
mNumPages,
mUri
);
return loader.loadAsList();
}
}

View File

@ -1,149 +0,0 @@
package app.grapheneos.pdfviewer.loader;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Typeface;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.StyleSpan;
import android.util.Log;
import androidx.loader.content.AsyncTaskLoader;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import app.grapheneos.pdfviewer.R;
import app.grapheneos.pdfviewer.Utils;
public class DocumentPropertiesLoader extends AsyncTaskLoader<List<CharSequence>> {
public static final String TAG = "DocumentPropertiesLoader";
public static final int ID = 1;
private final String mProperties;
private final int mNumPages;
private final Uri mUri;
private Cursor mCursor;
public DocumentPropertiesLoader(Context context, String properties, int numPages, Uri uri) {
super(context);
mProperties = properties;
mNumPages = numPages;
mUri = uri;
}
@Override
public List<CharSequence> loadInBackground() {
final Context context = getContext();
final String[] names = context.getResources().getStringArray(R.array.property_names);
final List<CharSequence> properties = new ArrayList<>(names.length);
mCursor = context.getContentResolver().query(mUri, null, null, null, null);
if (mCursor != null) {
mCursor.moveToFirst();
final int indexName = mCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (indexName >= 0) {
properties.add(getProperty(null, names[0], mCursor.getString(indexName)));
}
final int indexSize = mCursor.getColumnIndex(OpenableColumns.SIZE);
if (indexSize >= 0) {
final long fileSize = Long.parseLong(mCursor.getString(indexSize));
properties.add(getProperty(null, names[1], Utils.parseFileSize(fileSize)));
}
mCursor.close();
}
try {
final JSONObject json = new JSONObject(mProperties);
properties.add(getProperty(json, names[2], "Title"));
properties.add(getProperty(json, names[3], "Author"));
properties.add(getProperty(json, names[4], "Subject"));
properties.add(getProperty(json, names[5], "Keywords"));
properties.add(getProperty(json, names[6], "CreationDate"));
properties.add(getProperty(json, names[7], "ModDate"));
properties.add(getProperty(json, names[8], "Producer"));
properties.add(getProperty(json, names[9], "Creator"));
properties.add(getProperty(json, names[10], "PDFFormatVersion"));
properties.add(getProperty(null, names[11], String.valueOf(mNumPages)));
return properties;
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
@Override
public void deliverResult(List<CharSequence> properties) {
if (isReset()) {
onReleaseResources();
} else if (isStarted()) {
super.deliverResult(properties);
}
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
protected void onStopLoading() {
cancelLoad();
}
@Override
public void onCanceled(List<CharSequence> properties) {
super.onCanceled(properties);
onReleaseResources();
}
@Override
protected void onReset() {
super.onReset();
onStopLoading();
onReleaseResources();
}
private void onReleaseResources() {
if (mCursor != null) {
mCursor.close();
mCursor = null;
}
}
private CharSequence getProperty(final JSONObject json, String name, String specName) {
final SpannableStringBuilder property = new SpannableStringBuilder(name).append(":\n");
final String value = json != null ? json.optString(specName, "-") : specName;
if (specName != null && specName.endsWith("Date")) {
final Context context = getContext();
try {
property.append(value.equals("-") ? value : Utils.parseDate(value));
} catch (ParseException e) {
Log.w(TAG, e.getMessage() + " for " + value + " at offset: " + e.getErrorOffset());
property.append(context.getString(R.string.document_properties_invalid_date));
}
} else {
property.append(value);
}
property.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return property;
}
}

View File

@ -0,0 +1,111 @@
package app.grapheneos.pdfviewer.loader
import android.content.Context
import android.graphics.Typeface
import android.net.Uri
import android.provider.OpenableColumns
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.format.Formatter
import android.text.style.StyleSpan
import android.util.Log
import app.grapheneos.pdfviewer.R
import org.json.JSONException
class DocumentPropertiesLoader(
private val context: Context,
private val properties: String,
private val numPages: Int,
private val mUri: Uri
) {
fun loadAsList(): List<CharSequence> {
return load().map { item ->
val name = context.getString(item.key.nameResource)
val value = item.value
SpannableStringBuilder()
.append(name)
.append(":\n")
.append(value)
.apply {
setSpan(
StyleSpan(Typeface.BOLD),
0,
name.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
fun load(): Map<DocumentProperty, String> {
val result = mutableMapOf<DocumentProperty, String>()
result.addFileProperties()
result.addPageSizeProperty()
result.addPDFJsProperties()
return result
}
private fun MutableMap<DocumentProperty, String>.addPageSizeProperty() {
this[DocumentProperty.Pages] = java.lang.String.valueOf(numPages)
}
private fun MutableMap<DocumentProperty, String>.addFileProperties() {
putAll(getFileProperties())
}
private fun MutableMap<DocumentProperty, String>.addPDFJsProperties() {
putAll(getPDFJsProperties())
}
private fun getPDFJsProperties(): Map<DocumentProperty, String> {
return try {
PDFJsPropertiesToDocumentPropertyConverter(
properties,
context.getString(R.string.document_properties_invalid_date),
parseExceptionListener = { parseException, value ->
Log.w(
DocumentPropertiesAsyncTaskLoader.TAG,
"${parseException.message} for $value at offset: ${parseException.errorOffset}"
)
}
).convert()
} catch (e: JSONException) {
Log.w(
DocumentPropertiesAsyncTaskLoader.TAG,
"invalid properties"
)
emptyMap()
}
}
private fun getFileProperties(): Map<DocumentProperty, String> {
val collections = mutableMapOf<DocumentProperty, String>()
val proj = arrayOf(
OpenableColumns.DISPLAY_NAME,
OpenableColumns.SIZE
)
context.contentResolver.query(
mUri,
proj,
null,
null
)?.use { cursor ->
cursor.moveToFirst()
val indexName: Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (indexName >= 0) {
collections[DocumentProperty.FileName] = cursor.getString(indexName)
}
val indexSize: Int = cursor.getColumnIndex(OpenableColumns.SIZE)
if (indexSize >= 0) {
val fileSize: Long = cursor.getString(indexSize).toLong()
collections[DocumentProperty.FileSize] = Formatter.formatShortFileSize(context, fileSize)
}
}
return collections
}
}

View File

@ -0,0 +1,35 @@
package app.grapheneos.pdfviewer.loader
import androidx.annotation.StringRes
import app.grapheneos.pdfviewer.R
private const val TITLE_KEY = "Title"
private const val AUTHOR_KEY = "Author"
private const val SUBJECT_KEY = "Subject"
private const val KEYWORDS_KEY = "Keywords"
private const val CREATION_DATE_KEY = "CreationDate"
private const val MODIFY_DATE_KEY = "ModDate"
private const val PRODUCER_KEY = "Producer"
private const val CREATOR_KEY = "Creator"
private const val PDF_VERSION_KEY = "PDFFormatVersion"
const val DEFAULT_VALUE = "-"
enum class DocumentProperty(
val key: String = "",
@StringRes val nameResource: Int,
val isDate: Boolean = false
) {
FileName(key = "", nameResource = R.string.file_name),
FileSize(key = "", nameResource = R.string.file_size),
Pages(key = "", nameResource = R.string.pages),
Title(key = TITLE_KEY, nameResource = R.string.title),
Author(key = AUTHOR_KEY, nameResource = R.string.author),
Subject(key = SUBJECT_KEY, nameResource = R.string.subject),
Keywords(key = KEYWORDS_KEY, nameResource = R.string.keywords),
CreationDate(key = CREATION_DATE_KEY, nameResource = R.string.creation_date, isDate = true),
ModifyDate(key = MODIFY_DATE_KEY, nameResource = R.string.modify_date, isDate = true),
Producer(key = PRODUCER_KEY, nameResource = R.string.producer),
Creator(key = CREATOR_KEY, nameResource = R.string.creator),
PDFVersion(key = PDF_VERSION_KEY, nameResource = R.string.pdf_version);
}

View File

@ -0,0 +1,47 @@
package app.grapheneos.pdfviewer.loader
import app.grapheneos.pdfviewer.Utils
import org.json.JSONException
import org.json.JSONObject
import java.text.ParseException
import kotlin.jvm.Throws
class PDFJsPropertiesToDocumentPropertyConverter(
private val properties: String,
private val propertyInvalidDate: String,
private val parseExceptionListener: (e: ParseException, value: String) -> Unit
) {
@Throws(JSONException::class)
fun convert(): Map<DocumentProperty, String> {
val result = mutableMapOf<DocumentProperty, String>()
val json = JSONObject(properties)
addJsonProperties(json, result)
return result
}
private fun addJsonProperties(
json: JSONObject,
collections: MutableMap<DocumentProperty, String>
) {
for (documentProperty in DocumentProperty.values()) {
val key = documentProperty.key
if (key.isEmpty()) continue
val value = json.optString(key, DEFAULT_VALUE)
collections[documentProperty] = prettify(documentProperty, value)
}
}
private fun prettify(property: DocumentProperty, value: String): String {
if (value != DEFAULT_VALUE && property.isDate) {
return try {
Utils.parseDate(value)
} catch (parseException: ParseException) {
parseExceptionListener.invoke(parseException, value)
propertyInvalidDate
}
}
return value
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/debug_action_toggle_text_layer_visibility"
android:title="@string/debug_action_toggle_text_layer_visibility"
app:showAsAction="never" />
</menu>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="property_names">
<item>File name</item>
<item>File size</item>
<item>Title</item>
<item>Author</item>
<item>Subject</item>
<item>Keywords</item>
<item>Creation date</item>
<item>Modify date</item>
<item>Producer</item>
<item>Creator</item>
<item>PDF version</item>
<item>Pages</item>
</string-array>
</resources>

View File

@ -14,6 +14,8 @@
<string name="action_save_as">Save as</string>
<string name="action_view_document_properties">Properties</string>
<string name="debug_action_toggle_text_layer_visibility">Toggle text layer visibility</string>
<string name="document_properties_invalid_date">Invalid date</string>
<string name="document_properties_retrieval_failed">Failed to obtain document metadata</string>
@ -29,4 +31,17 @@
<string name="password_prompt_description">Enter the password to decrypt this PDF file</string>
<string name="open">Open</string>
<string name="cancel">Cancel</string>
<string name="file_name">File name</string>
<string name="file_size">File size</string>
<string name="title">Title</string>
<string name="author">Author</string>
<string name="subject">Subject</string>
<string name="keywords">Keywords</string>
<string name="creation_date">Creation date</string>
<string name="modify_date">Modify date</string>
<string name="producer">Producer</string>
<string name="creator">Creator</string>
<string name="pdf_version">PDF version</string>
<string name="pages">Pages</string>
</resources>

View File

@ -4,8 +4,8 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.4.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
classpath("com.android.tools.build:gradle:8.0.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
}
}

View File

@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableR8.fullMode=true
kotlin.code.style=official

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=ff7bf6a86f09b9b2c40bb8f48b25fc19cf2b2664fd1d220cd7ab833ec758d0d7
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
distributionSha256Sum=a62c5f99585dd9e1f95dab7b9415a0e698fa9dd1e6c38537faa81ac078f4d23e
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

7
gradlew vendored
View File

@ -85,9 +85,6 @@ done
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -197,6 +194,10 @@ if "$cygwin" || "$msys" ; then
done
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

1285
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"pdfjs-dist": "3.5.141"
}
}

3
setup Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
npm ci --ignore-scripts

@ -1 +0,0 @@
Subproject commit eb245b8de89c8d631d175ae937136de54ea3ed51