access TIFF images via FileDescriptor instead of File
This commit is contained in:
parent
a2ce68c150
commit
e99e648753
4 changed files with 79 additions and 98 deletions
|
@ -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 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
|
// 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
|
// 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
|
// JitPack build result is available at https://jitpack.io/com/github/deckerst/Android-TiffBitmapFactory/<commit>/build.log
|
||||||
implementation 'com.github.deckerst:Android-TiffBitmapFactory:0f1f0aaab1'
|
implementation 'com.github.deckerst:Android-TiffBitmapFactory:7efb450636'
|
||||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||||
|
|
||||||
kapt 'androidx.annotation:annotation:1.1.0'
|
kapt 'androidx.annotation:annotation:1.1.0'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package deckers.thibault.aves.channel.calls
|
package deckers.thibault.aves.channel.calls
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
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.isVideo
|
||||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
|
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
|
||||||
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class ThumbnailFetcher internal constructor(
|
class ThumbnailFetcher internal constructor(
|
||||||
private val activity: Activity,
|
private val context: Context,
|
||||||
uri: String,
|
uri: String,
|
||||||
private val mimeType: String,
|
private val mimeType: String,
|
||||||
private val dateModifiedSecs: Long,
|
private val dateModifiedSecs: Long,
|
||||||
|
@ -47,18 +44,18 @@ class ThumbnailFetcher internal constructor(
|
||||||
var recycle = true
|
var recycle = true
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
|
|
||||||
if (mimeType == MimeTypes.TIFF) {
|
try {
|
||||||
bitmap = getTiff()
|
if (mimeType == MimeTypes.TIFF) {
|
||||||
} else if ((width == defaultSize || height == defaultSize) && !isFlipped) {
|
bitmap = getTiff()
|
||||||
// Fetch low quality thumbnails when size is not specified.
|
} else if ((width == defaultSize || height == defaultSize) && !isFlipped) {
|
||||||
// As of Android R, the Media Store content resolver may return a thumbnail
|
// Fetch low quality thumbnails when size is not specified.
|
||||||
// that is automatically rotated according to EXIF orientation, but not flipped,
|
// As of Android R, the Media Store content resolver may return a thumbnail
|
||||||
// so we skip this step for flipped entries.
|
// that is automatically rotated according to EXIF orientation, but not flipped,
|
||||||
try {
|
// so we skip this step for flipped entries.
|
||||||
bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) getByResolver() else getByMediaStore()
|
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
|
// 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)
|
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||||
private fun getByResolver(): Bitmap? {
|
private fun getByResolver(): Bitmap? {
|
||||||
val resolver = activity.contentResolver
|
val resolver = context.contentResolver
|
||||||
var bitmap: Bitmap? = resolver.loadThumbnail(uri, Size(width, height), null)
|
var bitmap: Bitmap? = resolver.loadThumbnail(uri, Size(width, height), null)
|
||||||
if (needRotationAfterContentResolverThumbnail(mimeType)) {
|
if (needRotationAfterContentResolverThumbnail(mimeType)) {
|
||||||
bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped)
|
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
|
||||||
}
|
}
|
||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getByMediaStore(): Bitmap? {
|
private fun getByMediaStore(): Bitmap? {
|
||||||
val contentId = ContentUris.parseId(uri)
|
val contentId = ContentUris.parseId(uri)
|
||||||
val resolver = activity.contentResolver
|
val resolver = context.contentResolver
|
||||||
return if (isVideo(mimeType)) {
|
return if (isVideo(mimeType)) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
MediaStore.Video.Thumbnails.getThumbnail(resolver, contentId, MediaStore.Video.Thumbnails.MINI_KIND, null)
|
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)
|
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
|
// from Android Q, returned thumbnail is already rotated according to EXIF orientation
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && bitmap != null) {
|
||||||
bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped)
|
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
|
||||||
}
|
}
|
||||||
bitmap
|
bitmap
|
||||||
}
|
}
|
||||||
|
@ -118,13 +115,13 @@ class ThumbnailFetcher internal constructor(
|
||||||
|
|
||||||
val target = if (isVideo(mimeType)) {
|
val target = if (isVideo(mimeType)) {
|
||||||
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
Glide.with(activity)
|
Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.load(VideoThumbnail(activity, uri))
|
.load(VideoThumbnail(context, uri))
|
||||||
.submit(width, height)
|
.submit(width, height)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(activity)
|
Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.load(uri)
|
.load(uri)
|
||||||
|
@ -134,51 +131,38 @@ class ThumbnailFetcher internal constructor(
|
||||||
return try {
|
return try {
|
||||||
var bitmap = target.get()
|
var bitmap = target.get()
|
||||||
if (needRotationAfterGlide(mimeType)) {
|
if (needRotationAfterGlide(mimeType)) {
|
||||||
bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped)
|
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
|
||||||
}
|
}
|
||||||
bitmap
|
bitmap
|
||||||
} finally {
|
} finally {
|
||||||
Glide.with(activity).clear(target)
|
Glide.with(context).clear(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTiff(): Bitmap? {
|
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
|
// determine sample size
|
||||||
TiffBitmapFactory.decodeFile(file, options)
|
|
||||||
val imageWidth = options.outWidth
|
|
||||||
val imageHeight = options.outHeight
|
|
||||||
var sampleSize = 1
|
var sampleSize = 1
|
||||||
if (imageHeight > height || imageWidth > width) {
|
context.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
|
||||||
while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) {
|
val options = TiffBitmapFactory.Options().apply {
|
||||||
sampleSize *= 2
|
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
|
// decode
|
||||||
with(options) {
|
return context.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
|
||||||
inJustDecodeBounds = false
|
val options = TiffBitmapFactory.Options().apply {
|
||||||
inSampleSize = sampleSize
|
inJustDecodeBounds = false
|
||||||
|
inSampleSize = sampleSize
|
||||||
|
}
|
||||||
|
return TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options)
|
||||||
}
|
}
|
||||||
return TiffBitmapFactory.decodeFile(file, options)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -21,7 +21,6 @@ import io.flutter.plugin.common.EventChannel.EventSink
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
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)
|
error("streamImage-image-decode-null", "failed to get image from uri=$uri", null)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
var errorDetails = e.message
|
error("streamImage-image-decode-exception", "failed to get image from uri=$uri", toErrorDetails(e))
|
||||||
if (errorDetails?.isNotEmpty() == true) {
|
|
||||||
errorDetails = errorDetails.split("\n".toRegex(), 2).first()
|
|
||||||
}
|
|
||||||
error("streamImage-image-decode-exception", "failed to get image from uri=$uri", errorDetails)
|
|
||||||
} finally {
|
} finally {
|
||||||
Glide.with(activity).clear(target)
|
Glide.with(activity).clear(target)
|
||||||
}
|
}
|
||||||
|
@ -141,34 +136,44 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun streamTiffImage(uri: Uri) {
|
private fun streamTiffImage(uri: Uri) {
|
||||||
// copy source stream to a temp file
|
val resolver = activity.contentResolver
|
||||||
val file: File
|
|
||||||
try {
|
try {
|
||||||
file = File.createTempFile("aves", ".tiff")
|
var dirCount = 0
|
||||||
StorageUtils.openInputStream(activity, uri)?.use { input ->
|
resolver.openFileDescriptor(uri, "r")?.use { descriptor ->
|
||||||
StorageUtils.copyInputStreamToFile(input, file)
|
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()
|
// TODO TLAD handle multipage TIFF
|
||||||
options.inJustDecodeBounds = true
|
if (dirCount > 0) {
|
||||||
TiffBitmapFactory.decodeFile(file, options)
|
val i = 0
|
||||||
val dirCount: Int = options.outDirectoryCount
|
resolver.openFileDescriptor(uri, "r")?.use { descriptor ->
|
||||||
// TODO TLAD handle multipage TIFF
|
val options = TiffBitmapFactory.Options().apply {
|
||||||
if (dirCount > 0) {
|
inJustDecodeBounds = false
|
||||||
val i = 0
|
inDirectoryNumber = i
|
||||||
options.inDirectoryNumber = i
|
}
|
||||||
options.inJustDecodeBounds = false
|
val bitmap = TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options)
|
||||||
val bitmap = TiffBitmapFactory.decodeFile(file, options)
|
if (bitmap != null) {
|
||||||
if (bitmap != null) {
|
success(bitmap.getBytes(canHaveAlpha = true, recycle = true))
|
||||||
success(bitmap.getBytes(canHaveAlpha = true, recycle = true))
|
} else {
|
||||||
} else {
|
error("streamImage-tiff-null", "failed to get tiff image (dir=$i) from uri=$uri", null)
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,10 @@ import android.webkit.MimeTypeMap
|
||||||
import com.commonsware.cwac.document.DocumentFileCompat
|
import com.commonsware.cwac.document.DocumentFileCompat
|
||||||
import deckers.thibault.aves.utils.LogUtils.createTag
|
import deckers.thibault.aves.utils.LogUtils.createTag
|
||||||
import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath
|
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.*
|
||||||
import java.util.regex.Pattern
|
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
|
// convenience methods
|
||||||
|
|
||||||
fun ensureTrailingSeparator(dirPath: String): String {
|
fun ensureTrailingSeparator(dirPath: String): String {
|
||||||
|
|
Loading…
Reference in a new issue