From da4a7ae38fe4e632a0eb5f092a34bf2457499c91 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 8 Jun 2024 18:49:39 +0200 Subject: [PATCH] fixed raw preview region decoding sample size --- CHANGELOG.md | 4 ++ .../channel/calls/fetchers/RegionFetcher.kt | 24 +++++++----- .../thibault/aves/metadata/Metadata.kt | 38 ++++++++----------- .../aves/metadata/metadataextractor/Helper.kt | 1 - .../deckers/thibault/aves/utils/MathUtils.kt | 9 +++++ .../aves/model/provider/ImageProviderTest.kt | 8 ++-- .../thibault/aves/utils/MathUtilsTest.kt | 17 +++++++++ 7 files changed, 64 insertions(+), 37 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/utils/MathUtils.kt create mode 100644 android/app/src/test/kotlin/deckers/thibault/aves/utils/MathUtilsTest.kt 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)) + } +}