fixed raw preview region decoding sample size
This commit is contained in:
parent
1346e8867b
commit
da4a7ae38f
7 changed files with 64 additions and 37 deletions
|
@ -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
|
||||
|
||||
## <a id="v1.11.1"></a>[v1.11.1] - 2024-05-03
|
||||
|
||||
### Added
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -134,16 +134,8 @@ object Metadata {
|
|||
private val previewFiles = HashMap<Uri, File>()
|
||||
|
||||
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 -> {
|
||||
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
|
||||
|
@ -157,9 +149,9 @@ object Metadata {
|
|||
// small enough to be safe as it is
|
||||
uri
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// *probably* safe
|
||||
else -> uri
|
||||
uri
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue