diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt index cc8c61dd8..6af5ad179 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt @@ -10,6 +10,7 @@ import deckers.thibault.aves.model.provider.FieldMap import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.model.provider.MediaStoreImageProvider +import deckers.thibault.aves.utils.MimeTypes import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -95,14 +96,24 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { return } - regionFetcher.fetch( - uri, - mimeType, - sampleSize, - Rect(x, y, x + width, y + height), - Size(imageWidth, imageHeight), - result, - ) + val regionRect = Rect(x, y, x + width, y + height) + when (mimeType) { + MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch( + uri, + sampleSize, + regionRect, + page = 0, + result, + ) + else -> regionFetcher.fetch( + uri, + mimeType, + sampleSize, + regionRect, + Size(imageWidth, imageHeight), + result, + ) + } } private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt new file mode 100644 index 000000000..1c88245bb --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/TiffRegionFetcher.kt @@ -0,0 +1,40 @@ +package deckers.thibault.aves.channel.calls + +import android.content.Context +import android.graphics.Rect +import android.net.Uri +import deckers.thibault.aves.utils.BitmapUtils.getBytes +import io.flutter.plugin.common.MethodChannel +import org.beyka.tiffbitmapfactory.DecodeArea +import org.beyka.tiffbitmapfactory.TiffBitmapFactory + +class TiffRegionFetcher internal constructor( + private val context: Context, +) { + fun fetch( + uri: Uri, + sampleSize: Int, + regionRect: Rect, + page: Int = 0, + result: MethodChannel.Result, + ) { + val resolver = context.contentResolver + try { + resolver.openFileDescriptor(uri, "r")?.use { descriptor -> + val options = TiffBitmapFactory.Options().apply { + inDirectoryNumber = page + inSampleSize = sampleSize + inDecodeArea = DecodeArea(regionRect.left, regionRect.top, regionRect.width(), regionRect.height()) + } + val bitmap = TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options) + if (bitmap != null) { + result.success(bitmap.getBytes(canHaveAlpha = true, recycle = true)) + } else { + result.error("getRegion-tiff-null", "failed to decode region for uri=$uri page=$page regionRect=$regionRect", null) + } + } + } catch (e: Exception) { + result.error("getRegion-tiff-read-exception", "failed to read from uri=$uri page=$page regionRect=$regionRect", e.message) + } + } +} 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 a5dba87d9..df6e5177e 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 @@ -95,7 +95,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen private fun streamImageByGlide(uri: Uri, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) { val target = Glide.with(activity) .asBitmap() - .apply(options) + .apply(glideOptions) .load(uri) .submit() try { @@ -118,7 +118,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen private fun streamVideoByGlide(uri: Uri) { val target = Glide.with(activity) .asBitmap() - .apply(options) + .apply(glideOptions) .load(VideoThumbnail(activity, uri)) .submit() try { @@ -135,7 +135,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } } - private fun streamTiffImage(uri: Uri) { + private fun streamTiffImage(uri: Uri, page: Int = 0) { val resolver = activity.contentResolver try { var dirCount = 0 @@ -148,18 +148,17 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen } // TODO TLAD handle multipage TIFF - if (dirCount > 0) { - val i = 0 + if (dirCount > page) { resolver.openFileDescriptor(uri, "r")?.use { descriptor -> val options = TiffBitmapFactory.Options().apply { inJustDecodeBounds = false - inDirectoryNumber = i + inDirectoryNumber = page } val bitmap = TiffBitmapFactory.decodeFileDescriptor(descriptor.fd, options) if (bitmap != null) { success(bitmap.getBytes(canHaveAlpha = true, recycle = true)) } else { - error("streamImage-tiff-null", "failed to get tiff image (dir=$i) from uri=$uri", null) + error("streamImage-tiff-null", "failed to get tiff image (dir=$page) from uri=$uri", null) } } } @@ -192,7 +191,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen const val bufferSize = 2 shl 17 // 256kB // request a fresh image with the highest quality format - val options = RequestOptions() + val glideOptions = RequestOptions() .format(DecodeFormat.PREFER_ARGB_8888) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index e253bc434..ed1df3392 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -178,7 +178,7 @@ class ImageEntry { // Android's `BitmapRegionDecoder` documentation states that "only the JPEG and PNG formats are supported" // but in practice (tested on API 25, 27, 29), it successfully decodes the formats listed below, // and it actually fails to decode GIF, DNG and animated WEBP. Other formats were not tested. - bool get canTile => + bool get _supportedByBitmapRegionDecoder => [ MimeTypes.heic, MimeTypes.heif, @@ -196,6 +196,8 @@ class ImageEntry { ].contains(mimeType) && !isAnimated; + bool get canTile => _supportedByBitmapRegionDecoder || mimeType == MimeTypes.tiff; + bool get isRaw => MimeTypes.rawImages.contains(mimeType); bool get isVideo => mimeType.startsWith('video');