file:///scard/picture.jpg
18 | * file:///android_asset/picture.png
20 | * android.resource://com.example.app/drawable/picture
22 | *
23 | * @param context Application context
24 | * @param uri URI of the image
25 | * @return the decoded bitmap
26 | * @throws Exception if decoding fails.
27 | */
28 | @NonNull Bitmap decode(Context context, @NonNull Uri uri) throws Exception;
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/pdfview-android/src/main/java/com/pdfview/subsamplincscaleimageview/ImageViewState.java:
--------------------------------------------------------------------------------
1 | package com.pdfview.subsamplincscaleimageview;
2 |
3 | import android.graphics.PointF;
4 | import androidx.annotation.NonNull;
5 |
6 | import java.io.Serializable;
7 |
8 | /**
9 | * Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
10 | */
11 | @SuppressWarnings("WeakerAccess")
12 | public class ImageViewState implements Serializable {
13 |
14 | private final float scale;
15 |
16 | private final float centerX;
17 |
18 | private final float centerY;
19 |
20 | private final int orientation;
21 |
22 | public ImageViewState(float scale, @NonNull PointF center, int orientation) {
23 | this.scale = scale;
24 | this.centerX = center.x;
25 | this.centerY = center.y;
26 | this.orientation = orientation;
27 | }
28 |
29 | public float getScale() {
30 | return scale;
31 | }
32 |
33 | @NonNull public PointF getCenter() {
34 | return new PointF(centerX, centerY);
35 | }
36 |
37 | public int getOrientation() {
38 | return orientation;
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/sample-local/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion 32
6 | defaultConfig {
7 | applicationId "com.pdfview_sample.sample"
8 | minSdkVersion 21
9 | targetSdkVersion 32
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
13 | }
14 |
15 | compileOptions {
16 | sourceCompatibility JavaVersion.VERSION_11
17 | targetCompatibility JavaVersion.VERSION_11
18 | }
19 |
20 | kotlinOptions {
21 | jvmTarget = "11"
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 | }
31 |
32 | dependencies {
33 | implementation 'com.dmitryborodin:pdfview-android:1.1.0'
34 | // implementation project(':pdfview-android')
35 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
36 | implementation 'androidx.appcompat:appcompat:1.4.1'
37 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
38 | testImplementation 'junit:junit:4.13.2'
39 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
41 | }
42 |
--------------------------------------------------------------------------------
/pdfview-android/src/main/java/com/pdfview/PDFView.kt:
--------------------------------------------------------------------------------
1 | package com.pdfview
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import com.pdfview.subsamplincscaleimageview.ImageSource
6 | import com.pdfview.subsamplincscaleimageview.SubsamplingScaleImageView
7 | import java.io.File
8 |
9 | class PDFView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : SubsamplingScaleImageView(context, attrs) {
10 |
11 | private var mfile: File? = null
12 | private var mScale: Float = 8f
13 |
14 | init {
15 | setMinimumTileDpi(120)
16 | setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_START)
17 | }
18 |
19 | fun fromAsset(assetFileName: String): PDFView {
20 | mfile = FileUtils.fileFromAsset(context, assetFileName)
21 | return this
22 | }
23 |
24 | fun fromFile(file: File): PDFView {
25 | mfile = file
26 | return this
27 | }
28 |
29 | fun fromFile(filePath: String): PDFView {
30 | mfile = File(filePath)
31 | return this
32 | }
33 |
34 | fun scale(scale: Float): PDFView {
35 | mScale = scale
36 | return this
37 | }
38 |
39 | fun show() {
40 | val source = ImageSource.uri(mfile!!.path)
41 | setRegionDecoderFactory { PDFRegionDecoder(view = this, file = mfile!!, scale = mScale) }
42 | setImage(source)
43 | }
44 |
45 | override fun onDetachedFromWindow() {
46 | super.onDetachedFromWindow()
47 | this.recycle()
48 | }
49 | }
--------------------------------------------------------------------------------
/sample-network/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion 32
6 | defaultConfig {
7 | applicationId "com.pdfview_sample.sample"
8 | minSdkVersion 21
9 | targetSdkVersion 32
10 | versionCode 1
11 | versionName "1.0"
12 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
13 | }
14 |
15 | compileOptions {
16 | sourceCompatibility JavaVersion.VERSION_11
17 | targetCompatibility JavaVersion.VERSION_11
18 | }
19 |
20 | kotlinOptions {
21 | jvmTarget = "11"
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 | }
31 |
32 | dependencies {
33 | // implementation 'com.pdfview:pdfview-android:1.0.0'
34 | implementation project(':pdfview-android')
35 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
36 | implementation 'androidx.appcompat:appcompat:1.4.1'
37 | //this provides toUri(), by viewModel and other extensions
38 | implementation "androidx.fragment:fragment-ktx:1.4.1"
39 |
40 | implementation 'com.squareup.okhttp3:okhttp:4.7.2'
41 |
42 | testImplementation 'junit:junit:4.13.2'
43 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
45 | }
46 |
--------------------------------------------------------------------------------
/maven-scripts/info.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 | libraryVersion = '1.1.0'
3 |
4 | libraryName = 'pdfview-android'
5 | publishedGroupId = 'com.dmitryborodin'
6 | artifactId = 'pdfview-android'
7 |
8 | libraryDescription = 'Small library to show PDF files in your native android application'
9 |
10 | siteUrl = 'https://github.com/Dmitry-Borodin/pdfview'
11 | gitUrl = 'https://github.com/Dmitry-Borodin/pdfview'
12 | gitConnection = 'scm:git:github.com/Dmitry-Borodin/pdfview.git'
13 | gitDeveloperConnection = 'scm:git:ssh://github.com/Dmitry-Borodin/pdfview.git'
14 |
15 | developerId = 'Dmitry-Borodin'
16 | developerName = 'Dmitry Borodin'
17 | developerEmail = 'pdfview@DmitryBorodin.com'
18 |
19 | licenseName = 'Apache-2.0'
20 | licenseUrl = 'https://www.apache.org/licenses/LICENSE-2.0.html'
21 | allLicenses = ["Apache-2.0"]
22 |
23 | File secretPropsFile = project.rootProject.file('local.properties')
24 | if (secretPropsFile.exists()) {
25 | // Read local.properties file first if it exists
26 | Properties p = new Properties()
27 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
28 | p.each { name, value -> ext[name] = value }
29 | } else {
30 | // Use system environment variables
31 | OSSRH_USERNAME = System.getenv('OSSRH_USERNAME')
32 | OSSRH_PASSWORD = System.getenv('OSSRH_PASSWORD')
33 | SONATYPE_STAGING_PROFILE_ID = System.getenv('SONATYPE_STAGING_PROFILE_ID')
34 | SIGNING_KEY_ID = System.getenv('SIGNING_KEY_ID')
35 | SIGNING_PASSWORD = System.getenv('SIGNING_PASSWORD')
36 | SIGNING_KEY = System.getenv('SIGNING_KEY')
37 | }
38 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Android ###
2 | # from Stackoverflow
3 | # Built application files
4 | *.apk
5 | *.ap_
6 |
7 | # Files for the Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 |
17 | # Gradle files
18 | .gradle/
19 | build/
20 |
21 | # Local configuration file (sdk path, etc)
22 | local.properties
23 |
24 | # Proguard folder generated by Eclipse
25 | proguard/
26 |
27 | # Log Files
28 | *.log
29 |
30 | # Android Studio Navigation editor temp files
31 | .navigation/
32 |
33 | ### Android Patch ###
34 | gen-external-apklibs
35 |
36 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
37 | hs_err_pid*
38 |
39 | ### Intellij ###
40 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
41 |
42 | *.iml
43 |
44 | ## Directory-based project format:
45 | .idea/
46 | # if you remove the above rule, at least ignore the following:
47 |
48 | # User-specific stuff:
49 | # .idea/workspace.xml
50 | # .idea/tasks.xml
51 | # .idea/dictionaries
52 |
53 | # Sensitive or high-churn files:
54 | # .idea/dataSources.ids
55 | # .idea/dataSources.xml
56 | # .idea/sqlDataSources.xml
57 | # .idea/dynamic.xml
58 | # .idea/uiDesigner.xml
59 |
60 | # Gradle:
61 | # .idea/gradle.xml
62 | # .idea/libraries
63 |
64 | # Mongo Explorer plugin:
65 | # .idea/mongoSettings.xml
66 |
67 | ## File-based project format:
68 | *.ipr
69 | *.iws
70 |
71 | ## Plugin-specific files:
72 |
73 | # IntelliJ
74 | /out/
75 | /captures
76 |
77 | # mpeltonen/sbt-idea plugin
78 | .idea_modules/
79 |
80 | # JIRA plugin
81 | atlassian-ide-plugin.xml
82 |
83 | # Crashlytics plugin (for Android Studio and IntelliJ)
84 | com_crashlytics_export_strings.xml
85 | crashlytics.properties
86 | crashlytics-build.properties
--------------------------------------------------------------------------------
/pdfview-android/src/main/java/com/pdfview/subsamplincscaleimageview/decoder/CompatDecoderFactory.java:
--------------------------------------------------------------------------------
1 | package com.pdfview.subsamplincscaleimageview.decoder;
2 |
3 | import android.graphics.Bitmap;
4 | import androidx.annotation.NonNull;
5 |
6 | import java.lang.reflect.Constructor;
7 | import java.lang.reflect.InvocationTargetException;
8 |
9 | /**
10 | * Compatibility factory to instantiate decoders with empty public constructors.
11 | * @param file:///scard/picture.jpg
21 | * file:///android_asset/picture.png
23 | * android.resource://com.example.app/drawable/picture
25 | * @param context Application context. A reference may be held, but must be cleared on recycle.
26 | * @param uri URI of the image.
27 | * @return Dimensions of the image.
28 | * @throws Exception if initialisation fails.
29 | */
30 | @NonNull Point init(Context context, @NonNull Uri uri) throws Exception;
31 |
32 | /**
33 | *
34 | * Decode a region of the image with the given sample size. This method is called off the UI
35 | * thread so it can safely load the image on the current thread. It is called from
36 | * {@link android.os.AsyncTask}s running in an executor that may have multiple threads, so
37 | * implementations must be thread safe. Adding synchronized to the method signature
38 | * is the simplest way to achieve this, but bear in mind the {@link #recycle()} method can be
39 | * called concurrently.
40 | *
41 | * See {@link SkiaImageRegionDecoder} and {@link SkiaPooledImageRegionDecoder} for examples of 42 | * internal locking and synchronization. 43 | *
44 | * @param sRect Source image rectangle to decode. 45 | * @param sampleSize Sample size. 46 | * @return The decoded region. It is safe to return null if decoding fails. 47 | */ 48 | @NonNull Bitmap decodeRegion(@NonNull Rect sRect, int sampleSize); 49 | 50 | /** 51 | * Status check. Should return false before initialisation and after recycle. 52 | * @return true if the decoder is ready to be used. 53 | */ 54 | boolean isReady(); 55 | 56 | /** 57 | * This method will be called when the decoder is no longer required. It should clean up any resources still in use. 58 | */ 59 | void recycle(); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /pdfview-android/src/main/java/com/pdfview/PDFRegionDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.pdfview 2 | 3 | import android.content.Context 4 | import android.graphics.* 5 | import android.graphics.pdf.PdfRenderer 6 | import android.net.Uri 7 | import android.os.ParcelFileDescriptor 8 | import androidx.annotation.ColorInt 9 | import com.pdfview.subsamplincscaleimageview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE 10 | import com.pdfview.subsamplincscaleimageview.decoder.ImageRegionDecoder 11 | import java.io.File 12 | 13 | internal class PDFRegionDecoder(private val view: PDFView, 14 | private val file: File, 15 | private val scale: Float, 16 | @param:ColorInt private val backgroundColorPdf: Int = Color.WHITE) : ImageRegionDecoder { 17 | 18 | private lateinit var descriptor: ParcelFileDescriptor 19 | private lateinit var renderer: PdfRenderer 20 | private var pageWidth = 0 21 | private var pageHeight = 0 22 | 23 | @Throws(Exception::class) 24 | override fun init(context: Context, uri: Uri): Point { 25 | descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) 26 | renderer = PdfRenderer(descriptor) 27 | val page = renderer.openPage(0) 28 | pageWidth = (page.width * scale).toInt() 29 | pageHeight = (page.height * scale).toInt() 30 | if (renderer.pageCount > 15) { 31 | view.setHasBaseLayerTiles(false) 32 | } else if (renderer.pageCount == 1) { 33 | view.setMinimumScaleType(SCALE_TYPE_CENTER_INSIDE) 34 | } 35 | page.close() 36 | return Point(pageWidth,pageHeight * renderer.pageCount) 37 | } 38 | 39 | override fun decodeRegion(rect: Rect, sampleSize: Int): Bitmap { 40 | val numPageAtStart = Math.floor(rect.top.toDouble() / pageHeight).toInt() 41 | val numPageAtEnd = Math.ceil(rect.bottom.toDouble() / pageHeight).toInt() - 1 42 | val bitmap = Bitmap.createBitmap(rect.width() / sampleSize, rect.height() / sampleSize, Bitmap.Config.ARGB_8888) 43 | val canvas = Canvas(bitmap) 44 | canvas.drawColor(backgroundColorPdf) 45 | canvas.drawBitmap(bitmap, 0f, 0f, null) 46 | for ((iteration, pageIndex) in (numPageAtStart..numPageAtEnd).withIndex()) { 47 | synchronized(renderer) { 48 | val page = renderer.openPage(pageIndex) 49 | val matrix = Matrix() 50 | matrix.setScale(scale / sampleSize, scale / sampleSize) 51 | matrix.postTranslate( 52 | (-rect.left / sampleSize).toFloat(), -((rect.top - pageHeight * numPageAtStart) / sampleSize).toFloat() + (pageHeight.toFloat() / sampleSize) * iteration) 53 | page.render(bitmap,null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) 54 | page.close() 55 | } 56 | } 57 | return bitmap 58 | } 59 | 60 | override fun isReady(): Boolean { 61 | return pageWidth > 0 && pageHeight > 0 62 | } 63 | 64 | override fun recycle() { 65 | renderer.close() 66 | descriptor.close() 67 | pageWidth = 0 68 | pageHeight = 0 69 | } 70 | } -------------------------------------------------------------------------------- /sample-network/src/main/java/com/pdfview_network_sample/pdfview/PdfViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.pdfview_network_sample.pdfview 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import android.os.Handler 6 | import android.os.Looper 7 | import androidx.annotation.MainThread 8 | import androidx.core.net.toUri 9 | import androidx.lifecycle.LiveData 10 | import androidx.lifecycle.MutableLiveData 11 | import androidx.lifecycle.ViewModel 12 | import androidx.lifecycle.ViewModelProvider 13 | import okhttp3.Call 14 | import okhttp3.Callback 15 | import okhttp3.OkHttpClient 16 | import okhttp3.Request 17 | import okhttp3.Response 18 | import java.io.BufferedInputStream 19 | import java.io.File 20 | import java.io.FileOutputStream 21 | import java.io.IOException 22 | import java.io.OutputStream 23 | 24 | /** 25 | * Provides path to a file when it's downloading and surviving view lifecycle 26 | * 27 | * @author Dmitry Borodin on 7/17/20. 28 | */ 29 | 30 | const val REMOTE_PDF_URL = "https://github.com/Dmitry-Borodin/pdfview-android/raw/dev/sample-local/src/main/assets/great-expectations.pdf" 31 | const val PDF_CACHED_FILE_NAME = "mypdf.pdf" 32 | 33 | class PdfViewModel(private val cacheDir: File) : ViewModel() { 34 | 35 | /** 36 | * Used to avoid starting another download while first one is in progress. It shouldn't happen anyway, 37 | * but I would like to be sure, even after modification. 38 | * 39 | * If downloading with coroutines we can just keep reference to Job and check if it's active, then we won't need to set it to false. 40 | */ 41 | private var inProgress = false //should be used from main thread only 42 | 43 | private val pdfPath: MutableLiveData43 | * An implementation of {@link ImageRegionDecoder} using a pool of {@link BitmapRegionDecoder}s, 44 | * to provide true parallel loading of tiles. This is only effective if parallel loading has been 45 | * enabled in the view by calling {@link SubsamplingScaleImageView#setExecutor(Executor)} 46 | * with a multi-threaded {@link Executor} instance. 47 | *
48 | * One decoder is initialised when the class is initialised. This is enough to decode base layer tiles. 49 | * Additional decoders are initialised when a subregion of the image is first requested, which indicates 50 | * interaction with the view. Creation of additional encoders stops when {@link #allowAdditionalDecoder(int, long)} 51 | * returns false. The default implementation takes into account the file size, number of CPU cores, 52 | * low memory status and a hard limit of 4. Extend this class to customise this. 53 | *
54 | * WARNING: This class is highly experimental and not proven to be stable on a wide range of 55 | * devices. You are advised to test it thoroughly on all available devices, and code your app to use 56 | * {@link SkiaImageRegionDecoder} on old or low powered devices you could not test. 57 | *
58 | */ 59 | public class SkiaPooledImageRegionDecoder implements ImageRegionDecoder { 60 | 61 | private static final String TAG = SkiaPooledImageRegionDecoder.class.getSimpleName(); 62 | 63 | private static boolean debug = false; 64 | 65 | private DecoderPool decoderPool = new DecoderPool(); 66 | private final ReadWriteLock decoderLock = new ReentrantReadWriteLock(true); 67 | 68 | private static final String FILE_PREFIX = "file://"; 69 | private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/"; 70 | private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; 71 | 72 | private final Bitmap.Config bitmapConfig; 73 | 74 | private Context context; 75 | private Uri uri; 76 | 77 | private long fileLength = Long.MAX_VALUE; 78 | private final Point imageDimensions = new Point(0, 0); 79 | private final AtomicBoolean lazyInited = new AtomicBoolean(false); 80 | 81 | @Keep 82 | @SuppressWarnings("unused") 83 | public SkiaPooledImageRegionDecoder() { 84 | this(null); 85 | } 86 | 87 | @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) 88 | public SkiaPooledImageRegionDecoder(@Nullable Bitmap.Config bitmapConfig) { 89 | Bitmap.Config globalBitmapConfig = SubsamplingScaleImageView.getPreferredBitmapConfig(); 90 | if (bitmapConfig != null) { 91 | this.bitmapConfig = bitmapConfig; 92 | } else if (globalBitmapConfig != null) { 93 | this.bitmapConfig = globalBitmapConfig; 94 | } else { 95 | this.bitmapConfig = Bitmap.Config.RGB_565; 96 | } 97 | } 98 | 99 | /** 100 | * Controls logging of debug messages. All instances are affected. 101 | * @param debug true to enable debug logging, false to disable. 102 | */ 103 | @Keep 104 | @SuppressWarnings("unused") 105 | public static void setDebug(boolean debug) { 106 | SkiaPooledImageRegionDecoder.debug = debug; 107 | } 108 | 109 | /** 110 | * Initialises the decoder pool. This method creates one decoder on the current thread and uses 111 | * it to decode the bounds, then spawns an independent thread to populate the pool with an 112 | * additional three decoders. The thread will abort if {@link #recycle()} is called. 113 | */ 114 | @Override 115 | @NonNull 116 | public Point init(final Context context, @NonNull final Uri uri) throws Exception { 117 | this.context = context; 118 | this.uri = uri; 119 | initialiseDecoder(); 120 | return this.imageDimensions; 121 | } 122 | 123 | /** 124 | * Initialises extra decoders for as long as {@link #allowAdditionalDecoder(int, long)} returns 125 | * true and the pool has not been recycled. 126 | */ 127 | private void lazyInit() { 128 | if (lazyInited.compareAndSet(false, true) && fileLength < Long.MAX_VALUE) { 129 | debug("Starting lazy init of additional decoders"); 130 | Thread thread = new Thread() { 131 | @Override 132 | public void run() { 133 | while (decoderPool != null && allowAdditionalDecoder(decoderPool.size(), fileLength)) { 134 | // New decoders can be created while reading tiles but this read lock prevents 135 | // them being initialised while the pool is being recycled. 136 | try { 137 | if (decoderPool != null) { 138 | long start = System.currentTimeMillis(); 139 | debug("Starting decoder"); 140 | initialiseDecoder(); 141 | long end = System.currentTimeMillis(); 142 | debug("Started decoder, took " + (end - start) + "ms"); 143 | } 144 | } catch (Exception e) { 145 | // A decoder has already been successfully created so we can ignore this 146 | debug("Failed to start decoder: " + e.getMessage()); 147 | } 148 | } 149 | } 150 | }; 151 | thread.start(); 152 | } 153 | } 154 | 155 | /** 156 | * Initialises a new {@link BitmapRegionDecoder} and adds it to the pool, unless the pool has 157 | * been recycled while it was created. 158 | */ 159 | private void initialiseDecoder() throws Exception { 160 | String uriString = uri.toString(); 161 | BitmapRegionDecoder decoder; 162 | long fileLength = Long.MAX_VALUE; 163 | if (uriString.startsWith(RESOURCE_PREFIX)) { 164 | Resources res; 165 | String packageName = uri.getAuthority(); 166 | if (context.getPackageName().equals(packageName)) { 167 | res = context.getResources(); 168 | } else { 169 | PackageManager pm = context.getPackageManager(); 170 | res = pm.getResourcesForApplication(packageName); 171 | } 172 | 173 | int id = 0; 174 | List