From e99e648753f0e207cc8e356608a76ae3992874d2 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 13 Nov 2020 18:39:05 +0900 Subject: [PATCH] access TIFF images via FileDescriptor instead of File --- android/app/build.gradle | 4 +- .../aves/channel/calls/ThumbnailFetcher.kt | 92 ++++++++----------- .../channel/streams/ImageByteStreamHandler.kt | 65 +++++++------ .../thibault/aves/utils/StorageUtils.kt | 16 +--- 4 files changed, 79 insertions(+), 98 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f1187bfc4..75bac6650 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -101,8 +101,8 @@ dependencies { // as of v0.9.8.7, `Android-TiffBitmapFactory` master branch is set up to release and distribute via Bintray // as of 20201113, its `q_support` branch allows decoding TIFF without a `File`, but is not released // we forked it to bypass official releases, upgrading its Android/Gradle structure to make it compatible with JitPack - // JitPack build result is available at https://jitpack.io/com/github/deckerst/Android-TiffBitmapFactory/q_support-SNAPSHOT/build.log - implementation 'com.github.deckerst:Android-TiffBitmapFactory:0f1f0aaab1' + // JitPack build result is available at https://jitpack.io/com/github/deckerst/Android-TiffBitmapFactory//build.log + implementation 'com.github.deckerst:Android-TiffBitmapFactory:7efb450636' implementation 'com.github.bumptech.glide:glide:4.11.0' kapt 'androidx.annotation:annotation:1.1.0' diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt index e236cd091..a69bd76a7 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt @@ -1,7 +1,7 @@ package deckers.thibault.aves.channel.calls -import android.app.Activity import android.content.ContentUris +import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.os.Build @@ -20,14 +20,11 @@ import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide -import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.MethodChannel import org.beyka.tiffbitmapfactory.TiffBitmapFactory -import java.io.File -import java.io.IOException class ThumbnailFetcher internal constructor( - private val activity: Activity, + private val context: Context, uri: String, private val mimeType: String, private val dateModifiedSecs: Long, @@ -47,18 +44,18 @@ class ThumbnailFetcher internal constructor( var recycle = true var exception: Exception? = null - if (mimeType == MimeTypes.TIFF) { - bitmap = getTiff() - } else if ((width == defaultSize || height == defaultSize) && !isFlipped) { - // Fetch low quality thumbnails when size is not specified. - // As of Android R, the Media Store content resolver may return a thumbnail - // that is automatically rotated according to EXIF orientation, but not flipped, - // so we skip this step for flipped entries. - try { + try { + if (mimeType == MimeTypes.TIFF) { + bitmap = getTiff() + } else if ((width == defaultSize || height == defaultSize) && !isFlipped) { + // Fetch low quality thumbnails when size is not specified. + // As of Android R, the Media Store content resolver may return a thumbnail + // that is automatically rotated according to EXIF orientation, but not flipped, + // so we skip this step for flipped entries. bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) getByResolver() else getByMediaStore() - } catch (e: Exception) { - exception = e } + } catch (e: Exception) { + exception = e } // fallback if the native methods failed or for higher quality thumbnails @@ -84,17 +81,17 @@ class ThumbnailFetcher internal constructor( @RequiresApi(api = Build.VERSION_CODES.Q) private fun getByResolver(): Bitmap? { - val resolver = activity.contentResolver + val resolver = context.contentResolver var bitmap: Bitmap? = resolver.loadThumbnail(uri, Size(width, height), null) if (needRotationAfterContentResolverThumbnail(mimeType)) { - bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped) + bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) } return bitmap } private fun getByMediaStore(): Bitmap? { val contentId = ContentUris.parseId(uri) - val resolver = activity.contentResolver + val resolver = context.contentResolver return if (isVideo(mimeType)) { @Suppress("DEPRECATION") MediaStore.Video.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Video.Thumbnails.MINI_KIND, null) @@ -103,7 +100,7 @@ class ThumbnailFetcher internal constructor( var bitmap = MediaStore.Images.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Images.Thumbnails.MINI_KIND, null) // from Android Q, returned thumbnail is already rotated according to EXIF orientation if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) { - bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped) + bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) } bitmap } @@ -118,13 +115,13 @@ class ThumbnailFetcher internal constructor( val target = if (isVideo(mimeType)) { options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE) - Glide.with(activity) + Glide.with(context) .asBitmap() .apply(options) - .load(VideoThumbnail(activity, uri)) + .load(VideoThumbnail(context, uri)) .submit(width, height) } else { - Glide.with(activity) + Glide.with(context) .asBitmap() .apply(options) .load(uri) @@ -134,51 +131,38 @@ class ThumbnailFetcher internal constructor( return try { var bitmap = target.get() if (needRotationAfterGlide(mimeType)) { - bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped) + bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) } bitmap } finally { - Glide.with(activity).clear(target) + Glide.with(context).clear(target) } } private fun getTiff(): Bitmap? { - // copy source stream to a temp file - val file: File - try { - file = File.createTempFile("aves", ".tiff") - StorageUtils.openInputStream(activity, uri)?.use { input -> - StorageUtils.copyInputStreamToFile(input, file) - } - file.deleteOnExit() - } catch (e: IOException) { - return null - } - - // check directory count - val options = TiffBitmapFactory.Options().apply { - inJustDecodeBounds = true - } - TiffBitmapFactory.decodeFile(file, options) - if (options.outDirectoryCount == 0) return null - options.inDirectoryNumber = 0 - // determine sample size - TiffBitmapFactory.decodeFile(file, options) - val imageWidth = options.outWidth - val imageHeight = options.outHeight var sampleSize = 1 - if (imageHeight > height || imageWidth > width) { - while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) { - sampleSize *= 2 + context.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor -> + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = true + } + TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options) + val imageWidth = options.outWidth + val imageHeight = options.outHeight + if (imageHeight > height || imageWidth > width) { + while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) { + sampleSize *= 2 + } } } // decode - with(options) { - inJustDecodeBounds = false - inSampleSize = sampleSize + return context.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor -> + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = false + inSampleSize = sampleSize + } + return TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options) } - return TiffBitmapFactory.decodeFile(file, options) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt index 0cd0607af..a5dba87d9 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -21,7 +21,6 @@ import io.flutter.plugin.common.EventChannel.EventSink import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.beyka.tiffbitmapfactory.TiffBitmapFactory -import java.io.File import java.io.IOException import java.io.InputStream @@ -110,11 +109,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen error("streamImage-image-decode-null", "failed to get image from uri=$uri", null) } } catch (e: Exception) { - var errorDetails = e.message - if (errorDetails?.isNotEmpty() == true) { - errorDetails = errorDetails.split("\n".toRegex(), 2).first() - } - error("streamImage-image-decode-exception", "failed to get image from uri=$uri", errorDetails) + error("streamImage-image-decode-exception", "failed to get image from uri=$uri", toErrorDetails(e)) } finally { Glide.with(activity).clear(target) } @@ -141,34 +136,44 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } private fun streamTiffImage(uri: Uri) { - // copy source stream to a temp file - val file: File + val resolver = activity.contentResolver try { - file = File.createTempFile("aves", ".tiff") - StorageUtils.openInputStream(activity, uri)?.use { input -> - StorageUtils.copyInputStreamToFile(input, file) + var dirCount = 0 + resolver.openFileDescriptor(uri, "r")?.use { descriptor -> + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = true + } + TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options) + dirCount = options.outDirectoryCount } - file.deleteOnExit() - } catch (e: IOException) { - error("streamImage-tiff-copy", "failed to copy file from uri=$uri", null) - return - } - val options = TiffBitmapFactory.Options() - options.inJustDecodeBounds = true - TiffBitmapFactory.decodeFile(file, options) - val dirCount: Int = options.outDirectoryCount - // TODO TLAD handle multipage TIFF - if (dirCount > 0) { - val i = 0 - options.inDirectoryNumber = i - options.inJustDecodeBounds = false - val bitmap = TiffBitmapFactory.decodeFile(file, options) - if (bitmap != null) { - success(bitmap.getBytes(canHaveAlpha = true, recycle = true)) - } else { - error("streamImage-tiff-null", "failed to get tiff image (dir=$i) from uri=$uri", null) + // TODO TLAD handle multipage TIFF + if (dirCount > 0) { + val i = 0 + resolver.openFileDescriptor(uri, "r")?.use { descriptor -> + val options = TiffBitmapFactory.Options().apply { + inJustDecodeBounds = false + inDirectoryNumber = i + } + val bitmap = TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options) + if (bitmap != null) { + success(bitmap.getBytes(canHaveAlpha = true, recycle = true)) + } else { + error("streamImage-tiff-null", "failed to get tiff image (dir=$i) from uri=$uri", null) + } + } } + } catch (e: Exception) { + error("streamImage-tiff-exception", "failed to get image from uri=$uri", toErrorDetails(e)) + } + } + + private fun toErrorDetails(e: Exception): String? { + val errorDetails = e.message + return if (errorDetails?.isNotEmpty() == true) { + errorDetails.split("\n".toRegex(), 2).first() + } else { + errorDetails } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index 7ae3e3740..ab26c9932 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -15,7 +15,10 @@ import android.webkit.MimeTypeMap import com.commonsware.cwac.document.DocumentFileCompat import deckers.thibault.aves.utils.LogUtils.createTag import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath -import java.io.* +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream import java.util.* import java.util.regex.Pattern @@ -405,17 +408,6 @@ object StorageUtils { } } - @Throws(IOException::class) - fun copyInputStreamToFile(inputStream: InputStream, file: File) { - FileOutputStream(file).use { outputStream -> - var read: Int - val bytes = ByteArray(1024) - while (inputStream.read(bytes).also { read = it } != -1) { - outputStream.write(bytes, 0, read) - } - } - } - // convenience methods fun ensureTrailingSeparator(dirPath: String): String {