temp: do not tile multi-track images
This commit is contained in:
parent
bd8bc19fa1
commit
a37c10a969
9 changed files with 138 additions and 132 deletions
|
@ -98,7 +98,7 @@ repositories {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
|
||||||
implementation 'androidx.core:core-ktx:1.5.0-alpha05' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
|
implementation 'androidx.core:core-ktx:1.5.0-beta01' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
||||||
implementation 'com.commonsware.cwac:document:0.4.1'
|
implementation 'com.commonsware.cwac:document:0.4.1'
|
||||||
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
|
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
|
||||||
|
|
|
@ -13,7 +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.MultiTrackThumbnail
|
import deckers.thibault.aves.decoder.MultiTrackImage
|
||||||
import deckers.thibault.aves.decoder.TiffThumbnail
|
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
|
||||||
|
@ -128,7 +128,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
val model: Any = if (tiffFetch) {
|
val model: Any = if (tiffFetch) {
|
||||||
TiffThumbnail(context, uri, page ?: 0)
|
TiffThumbnail(context, uri, page ?: 0)
|
||||||
} else if (multiTrackFetch) {
|
} else if (multiTrackFetch) {
|
||||||
MultiTrackThumbnail(context, uri, page ?: 0)
|
MultiTrackImage(context, uri, page ?: 0)
|
||||||
} else uri
|
} else uri
|
||||||
Glide.with(context)
|
Glide.with(context)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
|
|
|
@ -9,7 +9,7 @@ import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
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 deckers.thibault.aves.decoder.MultiTrackThumbnail
|
import deckers.thibault.aves.decoder.MultiTrackImage
|
||||||
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
|
||||||
|
@ -118,7 +118,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
|
|
||||||
private fun streamImageByGlide(uri: Uri, page: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
|
private fun streamImageByGlide(uri: Uri, page: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
|
||||||
val model: Any = if (isHeifLike(mimeType) && page != null) {
|
val model: Any = if (isHeifLike(mimeType) && page != null) {
|
||||||
MultiTrackThumbnail(activity, uri, page)
|
MultiTrackImage(activity, uri, page)
|
||||||
} else {
|
} else {
|
||||||
uri
|
uri
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
package deckers.thibault.aves.decoder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
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.metadata.MultiTrackMedia
|
||||||
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
|
||||||
|
@GlideModule
|
||||||
|
class MultiTrackImageGlideModule : LibraryGlideModule() {
|
||||||
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
|
registry.append(MultiTrackImage::class.java, InputStream::class.java, MultiTrackThumbnailLoader.Factory())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int)
|
||||||
|
|
||||||
|
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, InputStream> {
|
||||||
|
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
||||||
|
return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackImageFetcher(model, width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handles(model: MultiTrackImage): Boolean = true
|
||||||
|
|
||||||
|
internal class Factory : ModelLoaderFactory<MultiTrackImage, InputStream> {
|
||||||
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MultiTrackImage, InputStream> = MultiTrackThumbnailLoader()
|
||||||
|
|
||||||
|
override fun teardown() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int, val height: Int) : DataFetcher<InputStream> {
|
||||||
|
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||||
|
callback.onLoadFailed(Exception("unsupported Android version"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = model.context
|
||||||
|
val uri = model.uri
|
||||||
|
val trackIndex = model.trackIndex
|
||||||
|
|
||||||
|
val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,124 +0,0 @@
|
||||||
package deckers.thibault.aves.decoder
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.media.MediaExtractor
|
|
||||||
import android.media.MediaFormat
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
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 deckers.thibault.aves.utils.LogUtils
|
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
|
|
||||||
@GlideModule
|
|
||||||
class MultiTrackThumbnailGlideModule : LibraryGlideModule() {
|
|
||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
|
||||||
registry.append(MultiTrackThumbnail::class.java, InputStream::class.java, MultiTrackThumbnailLoader.Factory())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MultiTrackThumbnail(val context: Context, val uri: Uri, val trackIndex: Int)
|
|
||||||
|
|
||||||
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackThumbnail, InputStream> {
|
|
||||||
override fun buildLoadData(model: MultiTrackThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
|
||||||
return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackThumbnailFetcher(model, width, height))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handles(MultiTrackThumbnail: MultiTrackThumbnail): Boolean = true
|
|
||||||
|
|
||||||
internal class Factory : ModelLoaderFactory<MultiTrackThumbnail, InputStream> {
|
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MultiTrackThumbnail, InputStream> = MultiTrackThumbnailLoader()
|
|
||||||
|
|
||||||
override fun teardown() {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MultiTrackThumbnailFetcher(val model: MultiTrackThumbnail, val width: Int, val height: Int) : DataFetcher<InputStream> {
|
|
||||||
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
||||||
callback.onLoadFailed(Exception("unsupported Android version"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = model.context
|
|
||||||
val uri = model.uri
|
|
||||||
val trackIndex = model.trackIndex
|
|
||||||
|
|
||||||
val imageIndex = trackIndexToImageIndex(context, uri, trackIndex)
|
|
||||||
if (imageIndex == null) {
|
|
||||||
callback.onLoadFailed(Exception("no image index"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val bitmap: Bitmap?
|
|
||||||
|
|
||||||
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return
|
|
||||||
try {
|
|
||||||
bitmap = retriever.getImageAtIndex(imageIndex)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
callback.onLoadFailed(e)
|
|
||||||
return
|
|
||||||
} finally {
|
|
||||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
|
||||||
retriever.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bitmap == null) {
|
|
||||||
callback.onLoadFailed(Exception("null bitmap"))
|
|
||||||
} else {
|
|
||||||
callback.onDataReady(bitmap.getBytes()?.inputStream())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? {
|
|
||||||
val extractor = MediaExtractor()
|
|
||||||
try {
|
|
||||||
extractor.setDataSource(context, uri, null)
|
|
||||||
val trackCount = extractor.trackCount
|
|
||||||
if (trackIndex < trackCount) {
|
|
||||||
var imageIndex = 0
|
|
||||||
for (i in 0 until trackIndex) {
|
|
||||||
val mimeType = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME)
|
|
||||||
if (MimeTypes.isImage(mimeType)) imageIndex++
|
|
||||||
}
|
|
||||||
return imageIndex
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e)
|
|
||||||
} finally {
|
|
||||||
extractor.release()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val LOG_TAG = LogUtils.createTag(MultiTrackThumbnailFetcher::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,7 +33,7 @@ internal class TiffThumbnailLoader : ModelLoader<TiffThumbnail, InputStream> {
|
||||||
return ModelLoader.LoadData(ObjectKey(model.uri), TiffThumbnailFetcher(model, width, height))
|
return ModelLoader.LoadData(ObjectKey(model.uri), TiffThumbnailFetcher(model, width, height))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handles(tiffThumbnail: TiffThumbnail): Boolean = true
|
override fun handles(model: TiffThumbnail): Boolean = true
|
||||||
|
|
||||||
internal class Factory : ModelLoaderFactory<TiffThumbnail, InputStream> {
|
internal class Factory : ModelLoaderFactory<TiffThumbnail, InputStream> {
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<TiffThumbnail, InputStream> = TiffThumbnailLoader()
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<TiffThumbnail, InputStream> = TiffThumbnailLoader()
|
||||||
|
|
|
@ -34,7 +34,7 @@ internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
|
||||||
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model))
|
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handles(videoThumbnail: VideoThumbnail): Boolean = true
|
override fun handles(model: VideoThumbnail): Boolean = true
|
||||||
|
|
||||||
internal class Factory : ModelLoaderFactory<VideoThumbnail, InputStream> {
|
internal class Factory : ModelLoaderFactory<VideoThumbnail, InputStream> {
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<VideoThumbnail, InputStream> = VideoThumbnailLoader()
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<VideoThumbnail, InputStream> = VideoThumbnailLoader()
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package deckers.thibault.aves.metadata
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.media.MediaExtractor
|
||||||
|
import android.media.MediaFormat
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
|
|
||||||
|
object MultiTrackMedia {
|
||||||
|
private val LOG_TAG = LogUtils.createTag(MultiTrackMedia::class.java)
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
|
fun getImage(context: Context, uri: Uri, trackIndex: Int): Bitmap? {
|
||||||
|
val imageIndex = trackIndexToImageIndex(context, uri, trackIndex) ?: return null
|
||||||
|
|
||||||
|
val retriever = StorageUtils.openMetadataRetriever(context, uri) ?: return null
|
||||||
|
try {
|
||||||
|
return retriever.getImageAtIndex(imageIndex)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to extract image from uri=$uri trackIndex=$trackIndex imageIndex=$imageIndex", e)
|
||||||
|
} finally {
|
||||||
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
|
retriever.release()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trackIndexToImageIndex(context: Context, uri: Uri, trackIndex: Int): Int? {
|
||||||
|
val extractor = MediaExtractor()
|
||||||
|
try {
|
||||||
|
extractor.setDataSource(context, uri, null)
|
||||||
|
val trackCount = extractor.trackCount
|
||||||
|
if (trackIndex < trackCount) {
|
||||||
|
var imageIndex = 0
|
||||||
|
for (i in 0 until trackIndex) {
|
||||||
|
val mimeType = extractor.getTrackFormat(i).getString(MediaFormat.KEY_MIME)
|
||||||
|
if (MimeTypes.isImage(mimeType)) imageIndex++
|
||||||
|
}
|
||||||
|
return imageIndex
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to get image index for uri=$uri, trackIndex=$trackIndex", e)
|
||||||
|
} finally {
|
||||||
|
extractor.release()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
|
@ -224,7 +224,8 @@ class ImageEntry {
|
||||||
MimeTypes.rw2,
|
MimeTypes.rw2,
|
||||||
MimeTypes.srw,
|
MimeTypes.srw,
|
||||||
].contains(mimeType) &&
|
].contains(mimeType) &&
|
||||||
!isAnimated;
|
!isAnimated &&
|
||||||
|
page == null;
|
||||||
|
|
||||||
bool get canTile => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
bool get canTile => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue