decode TIFF thumbnails via Glide module
This commit is contained in:
parent
9e13fdeea7
commit
daa30b3e0c
3 changed files with 96 additions and 33 deletions
|
@ -13,6 +13,7 @@ import com.bumptech.glide.load.DecodeFormat
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import com.bumptech.glide.signature.ObjectKey
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
|
import deckers.thibault.aves.decoder.TiffThumbnail
|
||||||
import deckers.thibault.aves.decoder.VideoThumbnail
|
import deckers.thibault.aves.decoder.VideoThumbnail
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
|
@ -21,7 +22,6 @@ 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 io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
|
||||||
|
|
||||||
class ThumbnailFetcher internal constructor(
|
class ThumbnailFetcher internal constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
@ -45,9 +45,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (mimeType == MimeTypes.TIFF) {
|
if (mimeType != MimeTypes.TIFF && (width == defaultSize || height == defaultSize) && !isFlipped) {
|
||||||
bitmap = getTiff()
|
|
||||||
} else if ((width == defaultSize || height == defaultSize) && !isFlipped) {
|
|
||||||
// Fetch low quality thumbnails when size is not specified.
|
// Fetch low quality thumbnails when size is not specified.
|
||||||
// As of Android R, the Media Store content resolver may return a thumbnail
|
// As of Android R, the Media Store content resolver may return a thumbnail
|
||||||
// that is automatically rotated according to EXIF orientation, but not flipped,
|
// that is automatically rotated according to EXIF orientation, but not flipped,
|
||||||
|
@ -121,10 +119,11 @@ class ThumbnailFetcher internal constructor(
|
||||||
.load(VideoThumbnail(context, uri))
|
.load(VideoThumbnail(context, uri))
|
||||||
.submit(width, height)
|
.submit(width, height)
|
||||||
} else {
|
} else {
|
||||||
|
val model: Any = if (mimeType == MimeTypes.TIFF) TiffThumbnail(context, uri) else uri
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.load(uri)
|
.load(model)
|
||||||
.submit(width, height)
|
.submit(width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,31 +137,4 @@ class ThumbnailFetcher internal constructor(
|
||||||
Glide.with(context).clear(target)
|
Glide.with(context).clear(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTiff(): Bitmap? {
|
|
||||||
// determine sample size
|
|
||||||
var sampleSize = 1
|
|
||||||
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
|
|
||||||
return context.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
|
|
||||||
val options = TiffBitmapFactory.Options().apply {
|
|
||||||
inJustDecodeBounds = false
|
|
||||||
inSampleSize = sampleSize
|
|
||||||
}
|
|
||||||
return TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package deckers.thibault.aves.decoder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.Priority
|
||||||
|
import com.bumptech.glide.Registry
|
||||||
|
import com.bumptech.glide.annotation.GlideModule
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.Options
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher
|
||||||
|
import com.bumptech.glide.load.data.DataFetcher.DataCallback
|
||||||
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
|
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||||
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
|
import com.bumptech.glide.module.LibraryGlideModule
|
||||||
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
|
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@GlideModule
|
||||||
|
class TiffThumbnailGlideModule : LibraryGlideModule() {
|
||||||
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
|
registry.append(TiffThumbnail::class.java, InputStream::class.java, TiffThumbnailLoader.Factory())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TiffThumbnail(val context: Context, val uri: Uri)
|
||||||
|
|
||||||
|
internal class TiffThumbnailLoader : ModelLoader<TiffThumbnail, InputStream> {
|
||||||
|
override fun buildLoadData(model: TiffThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
||||||
|
return ModelLoader.LoadData(ObjectKey(model.uri), TiffThumbnailFetcher(model, width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handles(tiffThumbnail: TiffThumbnail): Boolean = true
|
||||||
|
|
||||||
|
internal class Factory : ModelLoaderFactory<TiffThumbnail, InputStream> {
|
||||||
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<TiffThumbnail, InputStream> = TiffThumbnailLoader()
|
||||||
|
|
||||||
|
override fun teardown() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TiffThumbnailFetcher(val model: TiffThumbnail, val width: Int, val height: Int) : DataFetcher<InputStream> {
|
||||||
|
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
||||||
|
val context = model.context
|
||||||
|
val uri = model.uri
|
||||||
|
|
||||||
|
// determine sample size
|
||||||
|
var sampleSize = 1
|
||||||
|
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
|
||||||
|
val bitmap = context.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
|
||||||
|
val options = TiffBitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = false
|
||||||
|
inSampleSize = sampleSize
|
||||||
|
}
|
||||||
|
TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmap == null) {
|
||||||
|
callback.onLoadFailed(Exception("null bitmap"))
|
||||||
|
} else {
|
||||||
|
callback.onDataReady(bitmap.getBytes().inputStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// already cleaned up in loadData and ByteArrayInputStream will be GC'd
|
||||||
|
override fun cleanup() {}
|
||||||
|
|
||||||
|
// cannot cancel
|
||||||
|
override fun cancel() {}
|
||||||
|
|
||||||
|
override fun getDataClass(): Class<InputStream> = InputStream::class.java
|
||||||
|
|
||||||
|
override fun getDataSource(): DataSource = DataSource.LOCAL
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ class VideoThumbnailGlideModule : LibraryGlideModule() {
|
||||||
class VideoThumbnail(val context: Context, val uri: Uri)
|
class VideoThumbnail(val context: Context, val uri: Uri)
|
||||||
|
|
||||||
internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
|
internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
|
||||||
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
|
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
||||||
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model))
|
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue