diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4793a0e9a..295400e2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,10 @@ All notable changes to this project will be documented in this file.
- support for Android KitKat (API 19)
+### Fixed
+
+- crash when cataloguing large images
+
## [v1.11.1] - 2024-05-03
### Added
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 9d319e7db..b8f3c5740 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
@@ -14,10 +14,12 @@ 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.MathUtils
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel
+import kotlin.math.max
import kotlin.math.roundToInt
// As of Android 14 (API 34), `BitmapRegionDecoder` documentation states
@@ -60,10 +62,6 @@ class RegionFetcher internal constructor(
return
}
- val options = BitmapFactory.Options().apply {
- inSampleSize = sampleSize
- }
-
var currentDecoderRef = lastDecoderRef
if (currentDecoderRef != null && currentDecoderRef.uri != uri) {
currentDecoderRef = null
@@ -85,27 +83,35 @@ class RegionFetcher internal constructor(
// with raw images, the known image size may not match the decoded image size
// so we scale the requested region accordingly
- val effectiveRect = if (imageWidth != decoder.width || imageHeight != decoder.height) {
+ var effectiveRect = regionRect
+ var effectiveSampleSize = sampleSize
+
+ if (imageWidth != decoder.width || imageHeight != decoder.height) {
val xf = decoder.width.toDouble() / imageWidth
val yf = decoder.height.toDouble() / imageHeight
- Rect(
+ effectiveRect = Rect(
(regionRect.left * xf).roundToInt(),
(regionRect.top * yf).roundToInt(),
(regionRect.right * xf).roundToInt(),
(regionRect.bottom * yf).roundToInt(),
)
- } else {
- regionRect
+ val factor = MathUtils.highestPowerOf2((1 / max(xf, yf)).roundToInt())
+ if (factor > 1) {
+ effectiveSampleSize = max(1, effectiveSampleSize / factor)
+ }
}
// use `Long` as rect size could be unexpectedly large and go beyond `Int` max
- val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * effectiveRect.width() * effectiveRect.height() / sampleSize
+ val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * effectiveRect.width() * effectiveRect.height() / effectiveSampleSize
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
// decoding a region that large would yield an OOM when creating the bitmap
result.error("fetch-large-region", "Region too large for uri=$uri regionRect=$regionRect", null)
return
}
+ val options = BitmapFactory.Options().apply {
+ inSampleSize = effectiveSampleSize
+ }
val bitmap = decoder.decodeRegion(effectiveRect, options)
if (bitmap != null) {
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
index 4c578493b..04a21de0a 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt
@@ -134,32 +134,24 @@ object Metadata {
private val previewFiles = HashMap()
private fun getSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): Uri {
- return when (mimeType) {
- // formats known to yield OOM for large files
- MimeTypes.DNG,
- MimeTypes.DNG_ADOBE,
- MimeTypes.HEIC,
- MimeTypes.HEIF,
- MimeTypes.MP4,
- MimeTypes.PSD_VND,
- MimeTypes.PSD_X,
- MimeTypes.TIFF -> {
- if (isDangerouslyLarge(sizeBytes)) {
- // make a preview from the beginning of the file,
- // hoping the metadata is accessible in the copied chunk
- var previewFile = previewFiles[uri]
- if (previewFile == null) {
- previewFile = createPreviewFile(context, uri)
- previewFiles[uri] = previewFile
- }
- Uri.fromFile(previewFile)
- } else {
- // small enough to be safe as it is
- uri
+ // formats known to yield OOM for large files
+ return if ((MimeTypes.isImage(mimeType) || mimeType == MimeTypes.MP4)) {
+ if (isDangerouslyLarge(sizeBytes)) {
+ // make a preview from the beginning of the file,
+ // hoping the metadata is accessible in the copied chunk
+ var previewFile = previewFiles[uri]
+ if (previewFile == null) {
+ previewFile = createPreviewFile(context, uri)
+ previewFiles[uri] = previewFile
}
+ Uri.fromFile(previewFile)
+ } else {
+ // small enough to be safe as it is
+ uri
}
+ } else {
// *probably* safe
- else -> uri
+ uri
}
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
index 48bc3bdf5..36afac482 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt
@@ -122,7 +122,6 @@ object Helper {
val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, streamLength)
val metadata = com.drew.metadata.Metadata()
val handler = SafeExifTiffHandler(metadata, null, 0)
- Log.d(LOG_TAG, "safeReadTiff: availableHeapSize=${MemoryUtils.getAvailableHeapSize()}")
TiffReader().processTiff(reader, handler, 0)
return metadata
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt
new file mode 100644
index 000000000..7002c07f0
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt
@@ -0,0 +1,9 @@
+package deckers.thibault.aves.utils
+
+import kotlin.math.log2
+import kotlin.math.pow
+
+object MathUtils {
+ fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
+ fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
+}
\ No newline at end of file
diff --git a/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt b/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt
index 705503c62..14a126fef 100644
--- a/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt
+++ b/android/app/src/test/kotlin/deckers/thibault/aves/model/provider/ImageProviderTest.kt
@@ -10,9 +10,9 @@ class ImageProviderTest {
@Test
fun imageProvider_CorrectEmailSimple_ReturnsTrue() {
val date = LocalDate.of(1990, Month.FEBRUARY, 11).toEpochDay()
- assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Europe/Paris"), date), "+01:00")
- assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("UTC"), date), "+00:00")
- assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Asia/Kolkata"), date), "+05:30")
- assertEquals(ImageProvider.getTimeZoneString(TimeZone.getTimeZone("America/Chicago"), date), "-06:00")
+ assertEquals("+01:00", ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Europe/Paris"), date))
+ assertEquals("+00:00", ImageProvider.getTimeZoneString(TimeZone.getTimeZone("UTC"), date))
+ assertEquals("+05:30", ImageProvider.getTimeZoneString(TimeZone.getTimeZone("Asia/Kolkata"), date))
+ assertEquals("-06:00", ImageProvider.getTimeZoneString(TimeZone.getTimeZone("America/Chicago"), date))
}
}
diff --git a/android/app/src/test/kotlin/deckers/thibault/aves/utils/MathUtilsTest.kt b/android/app/src/test/kotlin/deckers/thibault/aves/utils/MathUtilsTest.kt
new file mode 100644
index 000000000..48382cca3
--- /dev/null
+++ b/android/app/src/test/kotlin/deckers/thibault/aves/utils/MathUtilsTest.kt
@@ -0,0 +1,17 @@
+package deckers.thibault.aves.utils
+
+import deckers.thibault.aves.utils.MathUtils.highestPowerOf2
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class MathUtilsTest {
+ @Test
+ fun mathUtils_highestPowerOf2() {
+ assertEquals(1024, highestPowerOf2(1024))
+ assertEquals(32, highestPowerOf2(42))
+ assertEquals(0, highestPowerOf2(0))
+ assertEquals(0, highestPowerOf2(-42))
+ assertEquals(0, highestPowerOf2(.5))
+ assertEquals(1, highestPowerOf2(1.5))
+ }
+}