diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca7cfca80..12b860b1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
## [v1.12.2] - 2025-01-13
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
index b8f3c5740..c6c9d0220 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/RegionFetcher.kt
@@ -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, Uri>()
-
- private val multiTrackGlideOptions = RequestOptions()
- .format(DecodeFormat.PREFER_ARGB_8888)
- .diskCacheStrategy(DiskCacheStrategy.NONE)
- .skipMemoryCache(true)
+ private val exportUris = HashMap, Uri>()
suspend fun fetch(
uri: Uri,
@@ -45,25 +40,27 @@ class RegionFetcher internal constructor(
regionRect: Rect,
imageWidth: Int,
imageHeight: Int,
+ requestKey: Pair = 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,
val decoder: BitmapRegionDecoder,
)
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag()
+ }
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
index 7b5cdc2d1..3d57f38fc 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt
@@ -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)) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
index 2cb61ad99..a0b033c3c 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt
@@ -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)
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt
index 0b699c80a..52336f25c 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/AvesAppGlideModule.kt
@@ -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)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index 9734cd497..8b2d530e5 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -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)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
index 93f64ccef..136d3827d 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt
@@ -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)