diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67e8aa252..a28c86bf6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
- Info: show matching dynamic albums
+### Fixed
+
+- crash when decoding some large thumbnails
+
## [v1.13.2] - 2025-06-02
### Changed
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 1bfb478de..ebd0245d9 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
@@ -5,8 +5,10 @@ import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
+import android.util.Log
import android.util.Size
import androidx.annotation.RequiresApi
+import androidx.core.graphics.scale
import androidx.core.net.toUri
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
@@ -17,6 +19,7 @@ import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
+import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.SVG
import deckers.thibault.aves.utils.MimeTypes.isVideo
@@ -25,6 +28,8 @@ import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId
import io.flutter.plugin.common.MethodChannel
+import kotlin.math.min
+import kotlin.math.roundToInt
class ThumbnailFetcher internal constructor(
private val context: Context,
@@ -77,6 +82,29 @@ class ThumbnailFetcher internal constructor(
}
}
+ if (bitmap != null) {
+ if (bitmap.width > width && bitmap.height > height) {
+ val scalingFactor: Double = min(bitmap.width.toDouble() / width, bitmap.height.toDouble() / height)
+ val dstWidth = (bitmap.width / scalingFactor).roundToInt()
+ val dstHeight = (bitmap.height / scalingFactor).roundToInt()
+ Log.d(
+ LOG_TAG, "rescale thumbnail for mimeType=$mimeType uri=$uri width=$width height=$height" +
+ ", with bitmap byteCount=${bitmap.byteCount} size=${bitmap.width}x${bitmap.height}" +
+ ", to target=${dstWidth}x${dstHeight}"
+ )
+ bitmap = bitmap.scale(dstWidth, dstHeight)
+ }
+
+ if (bitmap.byteCount > BITMAP_SIZE_DANGER_THRESHOLD) {
+ result.error(
+ "getThumbnail-large", "thumbnail bitmap dangerously large" +
+ " for mimeType=$mimeType uri=$uri pageId=$pageId width=$width height=$height" +
+ ", with bitmap byteCount=${bitmap.byteCount} size=${bitmap.width}x${bitmap.height} config=${bitmap.config?.name}", null
+ )
+ return
+ }
+ }
+
// do not recycle bitmaps fetched from `ContentResolver` or Glide as their lifecycle is unknown
val recycle = false
val bytes = BitmapUtils.getRawBytes(bitmap, recycle = recycle)
@@ -144,4 +172,9 @@ class ThumbnailFetcher internal constructor(
Glide.with(context).clear(target)
}
}
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag()
+ private const val BITMAP_SIZE_DANGER_THRESHOLD = 20 * (1 shl 20) // MB
+ }
}
\ No newline at end of file
diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart
index 057f544aa..3de23d30e 100644
--- a/lib/services/media/media_fetch_service.dart
+++ b/lib/services/media/media_fetch_service.dart
@@ -247,7 +247,7 @@ class PlatformMediaFetchService implements MediaFetchService {
return InteropDecoding.bytesToCodec(bytes);
}
} on PlatformException catch (e, stack) {
- if (_isUnknownVisual(mimeType)) {
+ if (_isUnknownVisual(mimeType) || e.code == 'getThumbnail-large') {
await reportService.recordError(e, stack);
}
}