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 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/<commit>/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'
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue