#1391 region decoding fallback to jpeg export

This commit is contained in:
Thibault Deckers 2025-01-21 18:01:04 +01:00
parent 8e5d971a6f
commit 0575a6cce6
7 changed files with 92 additions and 92 deletions

View file

@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file.
### Fixed
- editing TIFF metadata increasing file size
- region decoding for some RAW files
## <a id="v1.12.2"></a>[v1.12.2] - 2025-01-13

View file

@ -6,14 +6,14 @@ import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect
import android.net.Uri
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MathUtils
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes
@ -30,12 +30,7 @@ class RegionFetcher internal constructor(
) {
private var lastDecoderRef: LastDecoderRef? = null
private val pageTempUris = HashMap<Pair<Uri, Int>, Uri>()
private val multiTrackGlideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
private val exportUris = HashMap<Pair<Uri, Int?>, Uri>()
suspend fun fetch(
uri: Uri,
@ -45,25 +40,27 @@ class RegionFetcher internal constructor(
regionRect: Rect,
imageWidth: Int,
imageHeight: Int,
requestKey: Pair<Uri, Int?> = Pair(uri, pageId),
result: MethodChannel.Result,
) {
if (pageId != null && MultiPageImage.isSupported(mimeType)) {
val id = Pair(uri, pageId)
// use JPEG export for requested page
fetch(
uri = pageTempUris.getOrPut(id) { createJpegForPage(uri, mimeType, pageId) },
uri = exportUris.getOrPut(requestKey) { createTemporaryJpegExport(uri, mimeType, pageId) },
mimeType = MimeTypes.JPEG,
pageId = null,
sampleSize = sampleSize,
regionRect = regionRect,
imageWidth = imageWidth,
imageHeight = imageHeight,
requestKey = requestKey,
result = result,
)
return
}
var currentDecoderRef = lastDecoderRef
if (currentDecoderRef != null && currentDecoderRef.uri != uri) {
if (currentDecoderRef != null && currentDecoderRef.requestKey != requestKey) {
currentDecoderRef = null
}
@ -76,7 +73,7 @@ class RegionFetcher internal constructor(
result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
return
}
currentDecoderRef = LastDecoderRef(uri, newDecoder)
currentDecoderRef = LastDecoderRef(requestKey, newDecoder)
}
val decoder = currentDecoderRef.decoder
lastDecoderRef = currentDecoderRef
@ -119,16 +116,35 @@ class RegionFetcher internal constructor(
result.error("fetch-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
}
} catch (e: Exception) {
if (mimeType != MimeTypes.JPEG) {
// retry with JPEG export on failure,
// as some formats are not fully supported by `BitmapRegionDecoder`
fetch(
uri = exportUris.getOrPut(requestKey) { createTemporaryJpegExport(uri, mimeType, pageId) },
mimeType = MimeTypes.JPEG,
pageId = null,
sampleSize = sampleSize,
regionRect = regionRect,
imageWidth = imageWidth,
imageHeight = imageHeight,
requestKey = requestKey,
result = result,
)
return
}
result.error("fetch-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message)
}
}
private fun createJpegForPage(sourceUri: Uri, mimeType: String, pageId: Int): Uri {
private fun createTemporaryJpegExport(uri: Uri, mimeType: String, pageId: Int?): Uri {
Log.d(LOG_TAG, "create JPEG export for uri=$uri mimeType=$mimeType pageId=$pageId")
val target = Glide.with(context)
.asBitmap()
.apply(multiTrackGlideOptions)
.load(MultiPageImage(context, sourceUri, mimeType, pageId))
.apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(AvesAppGlideModule.getModel(context, uri, mimeType, pageId))
.submit()
try {
val bitmap = target.get()
val tempFile = StorageUtils.createTempFile(context).apply {
@ -143,7 +159,11 @@ class RegionFetcher internal constructor(
}
private data class LastDecoderRef(
val uri: Uri,
val requestKey: Pair<Uri, Int?>,
val decoder: BitmapRegionDecoder,
)
companion object {
private val LOG_TAG = LogUtils.createTag<RegionFetcher>()
}
}

View file

@ -12,10 +12,8 @@ import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.decoder.SvgImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MimeTypes
@ -122,28 +120,16 @@ class ThumbnailFetcher internal constructor(
.format(if (quality == 100) DecodeFormat.PREFER_ARGB_8888 else DecodeFormat.PREFER_RGB_565)
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$pageId"))
.override(width, height)
val target = if (isVideo(mimeType)) {
if (isVideo(mimeType)) {
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
Glide.with(context)
.asBitmap()
.apply(options)
.load(VideoThumbnail(context, uri))
.submit(width, height)
} else {
val model: Any = when {
svgFetch -> SvgImage(context, uri)
tiffFetch -> TiffImage(context, uri, pageId)
multiPageFetch -> MultiPageImage(context, uri, mimeType, pageId)
else -> StorageUtils.getGlideSafeUri(context, uri, mimeType)
}
Glide.with(context)
.asBitmap()
.apply(options)
.load(model)
.submit(width, height)
}
val target = Glide.with(context)
.asBitmap()
.apply(options)
.load(AvesAppGlideModule.getModel(context, uri, mimeType, pageId))
.submit(width, height)
return try {
var bitmap = target.get()
if (needRotationAfterGlide(mimeType, pageId)) {

View file

@ -6,12 +6,7 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
@ -130,18 +125,10 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
rotationDegrees: Int,
isFlipped: Boolean,
) {
val model: Any = if (pageId != null && MultiPageImage.isSupported(mimeType)) {
MultiPageImage(context, uri, mimeType, pageId)
} else if (mimeType == MimeTypes.TIFF) {
TiffImage(context, uri, pageId)
} else {
StorageUtils.getGlideSafeUri(context, uri, mimeType, sizeBytes)
}
val target = Glide.with(context)
.asBitmap()
.apply(glideOptions)
.load(model)
.apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(AvesAppGlideModule.getModel(context, uri, mimeType, pageId, sizeBytes))
.submit()
try {
var bitmap = withContext(Dispatchers.IO) { target.get() }
@ -159,7 +146,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
error("streamImage-image-decode-null", "failed to get image for mimeType=$mimeType uri=$uri", null)
}
} catch (e: Exception) {
error("streamImage-image-decode-exception", "failed to get image for mimeType=$mimeType uri=$uri model=$model", toErrorDetails(e))
error("streamImage-image-decode-exception", "failed to get image for mimeType=$mimeType uri=$uri", toErrorDetails(e))
} finally {
Glide.with(context).clear(target)
}
@ -168,8 +155,8 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
private suspend fun streamVideoByGlide(uri: Uri, mimeType: String, sizeBytes: Long?) {
val target = Glide.with(context)
.asBitmap()
.apply(glideOptions)
.load(VideoThumbnail(context, uri))
.apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(AvesAppGlideModule.getModel(context, uri, mimeType, null, sizeBytes))
.submit()
try {
val bitmap = withContext(Dispatchers.IO) { target.get() }
@ -218,11 +205,5 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
const val CHANNEL = "deckers.thibault/aves/media_byte_stream"
private const val BUFFER_SIZE = 2 shl 17 // 256kB
// request a fresh image with the highest quality format
private val glideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
}
}

View file

@ -1,14 +1,21 @@
package deckers.thibault.aves.decoder
import android.content.Context
import android.net.Uri
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.compatRemoveIf
@GlideModule
@ -25,4 +32,26 @@ class AvesAppGlideModule : AppGlideModule() {
}
override fun isManifestParsingEnabled(): Boolean = false
companion object {
// request a fresh image with the highest quality format
val uncachedFullImageOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
fun getModel(context: Context, uri: Uri, mimeType: String, pageId: Int?, sizeBytes: Long? = null): Any {
return if (pageId != null && MultiPageImage.isSupported(mimeType)) {
MultiPageImage(context, uri, mimeType, pageId)
} else if (mimeType == MimeTypes.TIFF) {
TiffImage(context, uri, pageId)
} else if (mimeType == MimeTypes.SVG) {
SvgImage(context, uri)
} else if (isVideo(mimeType)) {
VideoThumbnail(context, uri)
} else {
StorageUtils.getGlideSafeUri(context, uri, mimeType, sizeBytes)
}
}
}
}

View file

@ -11,16 +11,10 @@ import android.net.Uri
import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.FutureTarget
import com.bumptech.glide.request.RequestOptions
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.decoder.SvgImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.metadata.ExifInterfaceHelper
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.Metadata.TYPE_EXIF
@ -68,6 +62,7 @@ import java.nio.channels.Channels
import java.util.Date
import java.util.TimeZone
import kotlin.math.absoluteValue
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
abstract class ImageProvider {
open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) {
@ -317,27 +312,12 @@ abstract class ImageProvider {
}
}
val model: Any = if (pageId != null && MultiPageImage.isSupported(sourceMimeType)) {
MultiPageImage(activity, sourceUri, sourceMimeType, pageId)
} else if (sourceMimeType == MimeTypes.TIFF) {
TiffImage(activity, sourceUri, pageId)
} else if (sourceMimeType == MimeTypes.SVG) {
SvgImage(activity, sourceUri)
} else {
StorageUtils.getGlideSafeUri(activity, sourceUri, sourceMimeType, sourceEntry.sizeBytes)
}
// request a fresh image with the highest quality format
val glideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
target = Glide.with(activity.applicationContext)
.asBitmap()
.apply(glideOptions)
.load(model)
.apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(AvesAppGlideModule.getModel(activity, sourceUri, sourceMimeType, pageId, sourceEntry.sizeBytes))
.submit(targetWidthPx, targetHeightPx)
var bitmap = withContext(Dispatchers.IO) { target.get() }
if (MimeTypes.needRotationAfterGlide(sourceMimeType, pageId)) {
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)

View file

@ -81,7 +81,8 @@ object StorageUtils {
return null
}
val trashDir = File(externalFilesDir, "trash")
if (!trashDir.exists() && !trashDir.mkdirs()) {
trashDir.mkdirs()
if (!trashDir.exists()) {
Log.e(LOG_TAG, "failed to create directories at path=$trashDir")
return null
}
@ -499,7 +500,8 @@ object StorageUtils {
parentFile
} else {
val directory = File(cleanDirPath)
if (!directory.exists() && !directory.mkdirs()) {
directory.mkdirs()
if (!directory.exists()) {
Log.e(LOG_TAG, "failed to create directories at path=$cleanDirPath")
return null
}
@ -712,7 +714,8 @@ object StorageUtils {
fun createTempFile(context: Context, extension: String? = null): File {
val directory = getTempDirectory(context)
if (!directory.exists() && !directory.mkdirs()) {
directory.mkdirs()
if (!directory.exists()) {
throw IOException("failed to create directories at path=$directory")
}
val tempFile = File.createTempFile("aves", extension, directory)