tiling: handle raw images with different decoding size
fixed fetching dimensions of raw images
This commit is contained in:
parent
318fa0577a
commit
b42201dec0
12 changed files with 197 additions and 64 deletions
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Size
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import deckers.thibault.aves.model.ExifOrientationOp
|
import deckers.thibault.aves.model.ExifOrientationOp
|
||||||
import deckers.thibault.aves.model.provider.FieldMap
|
import deckers.thibault.aves.model.provider.FieldMap
|
||||||
|
@ -82,12 +83,14 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val sampleSize = call.argument<Int>("sampleSize")
|
val sampleSize = call.argument<Int>("sampleSize")
|
||||||
val x = call.argument<Int>("x")
|
val x = call.argument<Int>("regionX")
|
||||||
val y = call.argument<Int>("y")
|
val y = call.argument<Int>("regionY")
|
||||||
val width = call.argument<Int>("width")
|
val width = call.argument<Int>("regionWidth")
|
||||||
val height = call.argument<Int>("height")
|
val height = call.argument<Int>("regionHeight")
|
||||||
|
val imageWidth = call.argument<Int>("imageWidth")
|
||||||
|
val imageHeight = call.argument<Int>("imageHeight")
|
||||||
|
|
||||||
if (uri == null || mimeType == null || sampleSize == null || x == null || y == null || width == null || height == null) {
|
if (uri == null || mimeType == null || sampleSize == null || x == null || y == null || width == null || height == null || imageWidth == null || imageHeight == null) {
|
||||||
result.error("getRegion-args", "failed because of missing arguments", null)
|
result.error("getRegion-args", "failed because of missing arguments", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -97,6 +100,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
mimeType,
|
mimeType,
|
||||||
sampleSize,
|
sampleSize,
|
||||||
Rect(x, y, x + width, y + height),
|
Rect(x, y, x + width, y + height),
|
||||||
|
Size(imageWidth, imageHeight),
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -58,6 +59,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
@ -70,6 +72,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
"getContentResolverMetadata" -> GlobalScope.launch { getContentResolverMetadata(call, Coresult(result)) }
|
"getContentResolverMetadata" -> GlobalScope.launch { getContentResolverMetadata(call, Coresult(result)) }
|
||||||
"getExifInterfaceMetadata" -> GlobalScope.launch { getExifInterfaceMetadata(call, Coresult(result)) }
|
"getExifInterfaceMetadata" -> GlobalScope.launch { getExifInterfaceMetadata(call, Coresult(result)) }
|
||||||
"getMediaMetadataRetrieverMetadata" -> GlobalScope.launch { getMediaMetadataRetrieverMetadata(call, Coresult(result)) }
|
"getMediaMetadataRetrieverMetadata" -> GlobalScope.launch { getMediaMetadataRetrieverMetadata(call, Coresult(result)) }
|
||||||
|
"getBitmapFactoryInfo" -> GlobalScope.launch { getBitmapFactoryInfo(call, Coresult(result)) }
|
||||||
"getEmbeddedPictures" -> GlobalScope.launch { getEmbeddedPictures(call, Coresult(result)) }
|
"getEmbeddedPictures" -> GlobalScope.launch { getEmbeddedPictures(call, Coresult(result)) }
|
||||||
"getExifThumbnails" -> GlobalScope.launch { getExifThumbnails(call, Coresult(result)) }
|
"getExifThumbnails" -> GlobalScope.launch { getExifThumbnails(call, Coresult(result)) }
|
||||||
"getXmpThumbnails" -> GlobalScope.launch { getXmpThumbnails(call, Coresult(result)) }
|
"getXmpThumbnails" -> GlobalScope.launch { getXmpThumbnails(call, Coresult(result)) }
|
||||||
|
@ -483,6 +486,34 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
if (uri == null) {
|
||||||
|
result.error("getBitmapDecoderInfo-args", "failed because of missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val metadataMap = HashMap<String, String>()
|
||||||
|
try {
|
||||||
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
|
val options = BitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
}
|
||||||
|
BitmapFactory.decodeStream(input, null, options)
|
||||||
|
options.outMimeType?.let { metadataMap["MimeType"] = it }
|
||||||
|
options.outWidth.let { metadataMap["Width"] = it.toString() }
|
||||||
|
options.outHeight.let { metadataMap["Height"] = it.toString() }
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
options.outColorSpace?.let { metadataMap["ColorSpace"] = it.toString() }
|
||||||
|
options.outConfig?.let { metadataMap["Config"] = it.toString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
result.success(metadataMap)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
|
private fun getEmbeddedPictures(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
|
|
|
@ -6,22 +6,24 @@ import android.graphics.BitmapFactory
|
||||||
import android.graphics.BitmapRegionDecoder
|
import android.graphics.BitmapRegionDecoder
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Size
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class RegionFetcher internal constructor(
|
class RegionFetcher internal constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
) {
|
) {
|
||||||
private var lastDecoderRef: Pair<Uri, BitmapRegionDecoder>? = null
|
private var lastDecoderRef: LastDecoderRef? = null
|
||||||
|
|
||||||
fun fetch(
|
fun fetch(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
sampleSize: Int,
|
sampleSize: Int,
|
||||||
rect: Rect,
|
regionRect: Rect,
|
||||||
|
imageSize: Size,
|
||||||
result: MethodChannel.Result,
|
result: MethodChannel.Result,
|
||||||
) {
|
) {
|
||||||
val options = BitmapFactory.Options().apply {
|
val options = BitmapFactory.Options().apply {
|
||||||
|
@ -29,8 +31,8 @@ class RegionFetcher internal constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentDecoderRef = lastDecoderRef
|
var currentDecoderRef = lastDecoderRef
|
||||||
if (currentDecoderRef != null && currentDecoderRef.first != uri) {
|
if (currentDecoderRef != null && currentDecoderRef.uri != uri) {
|
||||||
currentDecoderRef.second.recycle()
|
currentDecoderRef.decoder.recycle()
|
||||||
currentDecoderRef = null
|
currentDecoderRef = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,12 +41,27 @@ class RegionFetcher internal constructor(
|
||||||
val newDecoder = StorageUtils.openInputStream(context, uri).use { input ->
|
val newDecoder = StorageUtils.openInputStream(context, uri).use { input ->
|
||||||
BitmapRegionDecoder.newInstance(input, false)
|
BitmapRegionDecoder.newInstance(input, false)
|
||||||
}
|
}
|
||||||
currentDecoderRef = Pair(uri, newDecoder)
|
currentDecoderRef = LastDecoderRef(uri, newDecoder)
|
||||||
}
|
}
|
||||||
val decoder = currentDecoderRef.second
|
val decoder = currentDecoderRef.decoder
|
||||||
lastDecoderRef = currentDecoderRef
|
lastDecoderRef = currentDecoderRef
|
||||||
|
|
||||||
val data = decoder.decodeRegion(rect, options)?.let {
|
// with raw images, the known image size may not match the decoded image size
|
||||||
|
// so we scale the requested region accordingly
|
||||||
|
val effectiveRect = if (imageSize.width != decoder.width || imageSize.height != decoder.height) {
|
||||||
|
val xf = decoder.width.toDouble() / imageSize.width
|
||||||
|
val yf = decoder.height.toDouble() / imageSize.height
|
||||||
|
Rect(
|
||||||
|
(regionRect.left * xf).roundToInt(),
|
||||||
|
(regionRect.top * yf).roundToInt(),
|
||||||
|
(regionRect.right * xf).roundToInt(),
|
||||||
|
(regionRect.bottom * yf).roundToInt(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
regionRect
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = decoder.decodeRegion(effectiveRect, options)?.let {
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
|
||||||
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
|
// Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency
|
||||||
|
@ -58,10 +75,15 @@ class RegionFetcher internal constructor(
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
result.success(data)
|
result.success(data)
|
||||||
} else {
|
} else {
|
||||||
result.error("getRegion-null", "failed to decode region for uri=$uri rect=$rect", null)
|
result.error("getRegion-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
result.error("getRegion-read-exception", "failed to initialize region decoder for uri=$uri", e.message)
|
result.error("getRegion-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class LastDecoderRef(
|
||||||
|
val uri: Uri,
|
||||||
|
val decoder: BitmapRegionDecoder,
|
||||||
|
)
|
||||||
|
|
|
@ -123,7 +123,8 @@ class SourceImageEntry {
|
||||||
fillVideoByMediaMetadataRetriever(context)
|
fillVideoByMediaMetadataRetriever(context)
|
||||||
if (isSized && hasDuration) return this
|
if (isSized && hasDuration) return this
|
||||||
}
|
}
|
||||||
if (MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) {
|
// skip metadata-extractor for raw images because it reports the decoded dimensions instead of the raw dimensions
|
||||||
|
if (!MimeTypes.isRaw(sourceMimeType) && MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) {
|
||||||
fillByMetadataExtractor(context)
|
fillByMetadataExtractor(context)
|
||||||
if (isSized && foundExif) return this
|
if (isSized && foundExif) return this
|
||||||
}
|
}
|
||||||
|
@ -224,8 +225,9 @@ class SourceImageEntry {
|
||||||
private fun fillByBitmapDecode(context: Context) {
|
private fun fillByBitmapDecode(context: Context) {
|
||||||
try {
|
try {
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
val options = BitmapFactory.Options()
|
val options = BitmapFactory.Options().apply {
|
||||||
options.inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
|
}
|
||||||
BitmapFactory.decodeStream(input, null, options)
|
BitmapFactory.decodeStream(input, null, options)
|
||||||
width = options.outWidth
|
width = options.outWidth
|
||||||
height = options.outHeight
|
height = options.outHeight
|
||||||
|
|
|
@ -144,11 +144,14 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
"contentId" to contentId,
|
"contentId" to contentId,
|
||||||
)
|
)
|
||||||
|
|
||||||
if ((width <= 0 || height <= 0) && needSize(mimeType)
|
if (MimeTypes.isRaw(mimeType)
|
||||||
|
|| (width <= 0 || height <= 0) && needSize(mimeType)
|
||||||
|| durationMillis == 0L && needDuration
|
|| durationMillis == 0L && needDuration
|
||||||
) {
|
) {
|
||||||
// some images are incorrectly registered in the Media Store,
|
// Some images are incorrectly registered in the Media Store,
|
||||||
// they are valid but miss some attributes, such as width, height, orientation
|
// missing some attributes such as width, height, orientation.
|
||||||
|
// Also, the reported size of raw images is inconsistent across devices
|
||||||
|
// and Android versions (sometimes the raw size, sometimes the decoded size).
|
||||||
val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context)
|
val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context)
|
||||||
entryMap = entry.toMap()
|
entryMap = entry.toMap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,16 @@ object MimeTypes {
|
||||||
const val WEBP = "image/webp"
|
const val WEBP = "image/webp"
|
||||||
|
|
||||||
// raw raster
|
// raw raster
|
||||||
|
private const val ARW = "image/x-sony-arw"
|
||||||
|
private const val CR2 = "image/x-canon-cr2"
|
||||||
private const val DNG = "image/x-adobe-dng"
|
private const val DNG = "image/x-adobe-dng"
|
||||||
|
private const val NEF = "image/x-nikon-nef"
|
||||||
|
private const val NRW = "image/x-nikon-nrw"
|
||||||
|
private const val ORF = "image/x-olympus-orf"
|
||||||
|
private const val PEF = "image/x-pentax-pef"
|
||||||
|
private const val RAF = "image/x-fuji-raf"
|
||||||
|
private const val RW2 = "image/x-panasonic-rw2"
|
||||||
|
private const val SRW = "image/x-samsung-srw"
|
||||||
|
|
||||||
// vector
|
// vector
|
||||||
const val SVG = "image/svg+xml"
|
const val SVG = "image/svg+xml"
|
||||||
|
@ -35,6 +44,13 @@ object MimeTypes {
|
||||||
else -> isVideo(mimeType)
|
else -> isVideo(mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isRaw(mimeType: String?): Boolean {
|
||||||
|
return when (mimeType) {
|
||||||
|
ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// returns whether the specified MIME type represents
|
// returns whether the specified MIME type represents
|
||||||
// a raster image format that allows an alpha channel
|
// a raster image format that allows an alpha channel
|
||||||
fun canHaveAlpha(mimeType: String?) = when (mimeType) {
|
fun canHaveAlpha(mimeType: String?) = when (mimeType) {
|
||||||
|
|
|
@ -169,7 +169,26 @@ class ImageEntry {
|
||||||
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
|
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
|
||||||
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg].contains(mimeType) || isRaw;
|
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg].contains(mimeType) || isRaw;
|
||||||
|
|
||||||
bool get canTile => !isVideo && !isAnimated && ![MimeTypes.gif].contains(mimeType);
|
// 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 =>
|
||||||
|
[
|
||||||
|
MimeTypes.heic,
|
||||||
|
MimeTypes.heif,
|
||||||
|
MimeTypes.jpeg,
|
||||||
|
MimeTypes.webp,
|
||||||
|
MimeTypes.arw,
|
||||||
|
MimeTypes.cr2,
|
||||||
|
MimeTypes.nef,
|
||||||
|
MimeTypes.nrw,
|
||||||
|
MimeTypes.orf,
|
||||||
|
MimeTypes.pef,
|
||||||
|
MimeTypes.raf,
|
||||||
|
MimeTypes.rw2,
|
||||||
|
MimeTypes.srw,
|
||||||
|
].contains(mimeType) &&
|
||||||
|
!isAnimated;
|
||||||
|
|
||||||
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
|
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
@ -113,13 +114,15 @@ class ImageFileService {
|
||||||
return Future.sync(() => null);
|
return Future.sync(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `rect`: region to decode, with coordinates in reference to `imageSize`
|
||||||
static Future<Uint8List> getRegion(
|
static Future<Uint8List> getRegion(
|
||||||
String uri,
|
String uri,
|
||||||
String mimeType,
|
String mimeType,
|
||||||
int rotationDegrees,
|
int rotationDegrees,
|
||||||
bool isFlipped,
|
bool isFlipped,
|
||||||
int sampleSize,
|
int sampleSize,
|
||||||
Rect rect, {
|
Rectangle<int> regionRect,
|
||||||
|
Size imageSize, {
|
||||||
Object taskKey,
|
Object taskKey,
|
||||||
int priority,
|
int priority,
|
||||||
}) {
|
}) {
|
||||||
|
@ -130,10 +133,12 @@ class ImageFileService {
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
'sampleSize': sampleSize,
|
'sampleSize': sampleSize,
|
||||||
'x': rect.left.toInt(),
|
'regionX': regionRect.left,
|
||||||
'y': rect.top.toInt(),
|
'regionY': regionRect.top,
|
||||||
'width': rect.width.toInt(),
|
'regionWidth': regionRect.width,
|
||||||
'height': rect.height.toInt(),
|
'regionHeight': regionRect.height,
|
||||||
|
'imageWidth': imageSize.width.toInt(),
|
||||||
|
'imageHeight': imageSize.height.toInt(),
|
||||||
});
|
});
|
||||||
return result as Uint8List;
|
return result as Uint8List;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
|
|
|
@ -76,6 +76,19 @@ class MetadataService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<Map> getBitmapFactoryInfo(ImageEntry entry) async {
|
||||||
|
try {
|
||||||
|
// return map with all data available when decoding image bounds with `BitmapFactory`
|
||||||
|
final result = await platform.invokeMethod('getBitmapFactoryInfo', <String, dynamic>{
|
||||||
|
'uri': entry.uri,
|
||||||
|
}) as Map;
|
||||||
|
return result;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
debugPrint('getBitmapFactoryInfo failed with code=${e.code}, exception=${e.message}, details=${e.details}');
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Map> getContentResolverMetadata(ImageEntry entry) async {
|
static Future<Map> getContentResolverMetadata(ImageEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with all data available from the content resolver
|
// return map with all data available from the content resolver
|
||||||
|
@ -92,7 +105,7 @@ class MetadataService {
|
||||||
|
|
||||||
static Future<Map> getExifInterfaceMetadata(ImageEntry entry) async {
|
static Future<Map> getExifInterfaceMetadata(ImageEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with all data available from the ExifInterface library
|
// return map with all data available from the `ExifInterface` library
|
||||||
final result = await platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getExifInterfaceMetadata', <String, dynamic>{
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
}) as Map;
|
}) as Map;
|
||||||
|
@ -105,7 +118,7 @@ class MetadataService {
|
||||||
|
|
||||||
static Future<Map> getMediaMetadataRetrieverMetadata(ImageEntry entry) async {
|
static Future<Map> getMediaMetadataRetrieverMetadata(ImageEntry entry) async {
|
||||||
try {
|
try {
|
||||||
// return map with all data available from the MediaMetadataRetriever
|
// return map with all data available from `MediaMetadataRetriever`
|
||||||
final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getMediaMetadataRetrieverMetadata', <String, dynamic>{
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
}) as Map;
|
}) as Map;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:ui' as ui show Codec;
|
import 'dart:ui' as ui show Codec;
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
@ -22,7 +23,7 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
scale: key.scale,
|
scale: key.scale,
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription('uri=${key.uri}, rect=${key.rect}');
|
yield ErrorDescription('uri=${key.uri}, regionRect=${key.regionRect}');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -37,7 +38,8 @@ class RegionProvider extends ImageProvider<RegionProviderKey> {
|
||||||
key.rotationDegrees,
|
key.rotationDegrees,
|
||||||
key.isFlipped,
|
key.isFlipped,
|
||||||
key.sampleSize,
|
key.sampleSize,
|
||||||
key.rect,
|
key.regionRect,
|
||||||
|
key.imageSize,
|
||||||
taskKey: key,
|
taskKey: key,
|
||||||
);
|
);
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
|
@ -63,7 +65,8 @@ class RegionProviderKey {
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
final int rotationDegrees, sampleSize;
|
final int rotationDegrees, sampleSize;
|
||||||
final bool isFlipped;
|
final bool isFlipped;
|
||||||
final Rect rect;
|
final Rectangle<int> regionRect;
|
||||||
|
final Size imageSize;
|
||||||
final double scale;
|
final double scale;
|
||||||
|
|
||||||
const RegionProviderKey({
|
const RegionProviderKey({
|
||||||
|
@ -72,14 +75,16 @@ class RegionProviderKey {
|
||||||
@required this.rotationDegrees,
|
@required this.rotationDegrees,
|
||||||
@required this.isFlipped,
|
@required this.isFlipped,
|
||||||
@required this.sampleSize,
|
@required this.sampleSize,
|
||||||
@required this.rect,
|
@required this.regionRect,
|
||||||
|
@required this.imageSize,
|
||||||
this.scale = 1.0,
|
this.scale = 1.0,
|
||||||
}) : assert(uri != null),
|
}) : assert(uri != null),
|
||||||
assert(mimeType != null),
|
assert(mimeType != null),
|
||||||
assert(rotationDegrees != null),
|
assert(rotationDegrees != null),
|
||||||
assert(isFlipped != null),
|
assert(isFlipped != null),
|
||||||
assert(sampleSize != null),
|
assert(sampleSize != null),
|
||||||
assert(rect != null),
|
assert(regionRect != null),
|
||||||
|
assert(imageSize != null),
|
||||||
assert(scale != null);
|
assert(scale != null);
|
||||||
|
|
||||||
// do not store the entry as it is, because the key should be constant
|
// do not store the entry as it is, because the key should be constant
|
||||||
|
@ -87,7 +92,7 @@ class RegionProviderKey {
|
||||||
factory RegionProviderKey.fromEntry(
|
factory RegionProviderKey.fromEntry(
|
||||||
ImageEntry entry, {
|
ImageEntry entry, {
|
||||||
@required int sampleSize,
|
@required int sampleSize,
|
||||||
@required Rect rect,
|
@required Rectangle<int> rect,
|
||||||
}) {
|
}) {
|
||||||
return RegionProviderKey(
|
return RegionProviderKey(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
|
@ -95,14 +100,15 @@ class RegionProviderKey {
|
||||||
rotationDegrees: entry.rotationDegrees,
|
rotationDegrees: entry.rotationDegrees,
|
||||||
isFlipped: entry.isFlipped,
|
isFlipped: entry.isFlipped,
|
||||||
sampleSize: sampleSize,
|
sampleSize: sampleSize,
|
||||||
rect: rect,
|
regionRect: rect,
|
||||||
|
imageSize: Size(entry.width.toDouble(), entry.height.toDouble()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other.runtimeType != runtimeType) return false;
|
if (other.runtimeType != runtimeType) return false;
|
||||||
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.rect == rect && other.scale == scale;
|
return other is RegionProviderKey && other.uri == uri && other.mimeType == mimeType && other.rotationDegrees == rotationDegrees && other.isFlipped == isFlipped && other.sampleSize == sampleSize && other.regionRect == regionRect && other.imageSize == imageSize && other.scale == scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -113,12 +119,13 @@ class RegionProviderKey {
|
||||||
isFlipped,
|
isFlipped,
|
||||||
mimeType,
|
mimeType,
|
||||||
sampleSize,
|
sampleSize,
|
||||||
rect,
|
regionRect,
|
||||||
|
imageSize,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'RegionProviderKey(uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, rect=$rect, scale=$scale)';
|
return 'RegionProviderKey(uri=$uri, mimeType=$mimeType, rotationDegrees=$rotationDegrees, isFlipped=$isFlipped, sampleSize=$sampleSize, regionRect=$regionRect, imageSize=$imageSize, scale=$scale)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ class MetadataTab extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MetadataTabState extends State<MetadataTab> {
|
class _MetadataTabState extends State<MetadataTab> {
|
||||||
Future<Map> _contentResolverMetadataLoader, _exifInterfaceMetadataLoader, _mediaMetadataLoader;
|
Future<Map> _bitmapFactoryLoader, _contentResolverMetadataLoader, _exifInterfaceMetadataLoader, _mediaMetadataLoader;
|
||||||
|
|
||||||
// MediaStore timestamp keys
|
// MediaStore timestamp keys
|
||||||
static const secondTimestampKeys = ['date_added', 'date_modified', 'date_expires', 'isPlayed'];
|
static const secondTimestampKeys = ['date_added', 'date_modified', 'date_expires', 'isPlayed'];
|
||||||
|
@ -33,6 +33,7 @@ class _MetadataTabState extends State<MetadataTab> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadMetadata() {
|
void _loadMetadata() {
|
||||||
|
_bitmapFactoryLoader = MetadataService.getBitmapFactoryInfo(entry);
|
||||||
_contentResolverMetadataLoader = MetadataService.getContentResolverMetadata(entry);
|
_contentResolverMetadataLoader = MetadataService.getContentResolverMetadata(entry);
|
||||||
_exifInterfaceMetadataLoader = MetadataService.getExifInterfaceMetadata(entry);
|
_exifInterfaceMetadataLoader = MetadataService.getExifInterfaceMetadata(entry);
|
||||||
_mediaMetadataLoader = MetadataService.getMediaMetadataRetrieverMetadata(entry);
|
_mediaMetadataLoader = MetadataService.getMediaMetadataRetrieverMetadata(entry);
|
||||||
|
@ -77,6 +78,10 @@ class _MetadataTabState extends State<MetadataTab> {
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: EdgeInsets.all(8),
|
padding: EdgeInsets.all(8),
|
||||||
children: [
|
children: [
|
||||||
|
FutureBuilder<Map>(
|
||||||
|
future: _bitmapFactoryLoader,
|
||||||
|
builder: (context, snapshot) => builder(context, snapshot, 'Bitmap Factory'),
|
||||||
|
),
|
||||||
FutureBuilder<Map>(
|
FutureBuilder<Map>(
|
||||||
future: _contentResolverMetadataLoader,
|
future: _contentResolverMetadataLoader,
|
||||||
builder: (context, snapshot) => builder(context, snapshot, 'Content Resolver'),
|
builder: (context, snapshot) => builder(context, snapshot, 'Content Resolver'),
|
||||||
|
|
|
@ -37,9 +37,6 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
|
|
||||||
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
|
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
|
||||||
|
|
||||||
// margin around visible area to fetch surrounding tiles in advance
|
|
||||||
static const preFetchMargin = 0.0;
|
|
||||||
|
|
||||||
// magic number used to derive sample size from scale
|
// magic number used to derive sample size from scale
|
||||||
static const scaleFactor = 2.0;
|
static const scaleFactor = 2.0;
|
||||||
|
|
||||||
|
@ -79,8 +76,8 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (viewStateNotifier == null) return SizedBox.shrink();
|
if (viewStateNotifier == null) return SizedBox.shrink();
|
||||||
|
|
||||||
final displayWidth = entry.displaySize.width;
|
final displayWidth = entry.displaySize.width.round();
|
||||||
final displayHeight = entry.displaySize.height;
|
final displayHeight = entry.displaySize.height.round();
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: viewStateNotifier,
|
animation: viewStateNotifier,
|
||||||
|
@ -97,32 +94,40 @@ class _TiledImageViewState extends State<TiledImageView> {
|
||||||
((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx),
|
((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx),
|
||||||
((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy),
|
((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy),
|
||||||
);
|
);
|
||||||
final viewRect = (viewOrigin & viewportSize).inflate(preFetchMargin);
|
final viewRect = viewOrigin & viewportSize;
|
||||||
|
|
||||||
final tiles = <RegionTile>[];
|
final tiles = <RegionTile>[];
|
||||||
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
|
var minSampleSize = min(_sampleSizeForScale(scale), _maxSampleSize);
|
||||||
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
|
for (var sampleSize = _maxSampleSize; sampleSize >= minSampleSize; sampleSize = (sampleSize / 2).floor()) {
|
||||||
final layerRegionSize = Size.square(_tileSide * sampleSize);
|
// for the largest sample size (matching the initial scale), the whole image is in view
|
||||||
for (var x = 0.0; x < displayWidth; x += layerRegionSize.width) {
|
// so we subsample the whole image instead of splitting it in tiles
|
||||||
for (var y = 0.0; y < displayHeight; y += layerRegionSize.height) {
|
final useTiles = sampleSize != _maxSampleSize;
|
||||||
final regionOrigin = Offset(x, y);
|
final regionSide = (_tileSide * sampleSize).round();
|
||||||
final nextOrigin = regionOrigin.translate(layerRegionSize.width, layerRegionSize.height);
|
final layerRegionWidth = useTiles ? regionSide : displayWidth;
|
||||||
final thisRegionSize = Size(
|
final layerRegionHeight = useTiles ? regionSide : displayHeight;
|
||||||
layerRegionSize.width - (nextOrigin.dx >= displayWidth ? nextOrigin.dx - displayWidth : 0),
|
for (var x = 0; x < displayWidth; x += layerRegionWidth) {
|
||||||
layerRegionSize.height - (nextOrigin.dy >= displayHeight ? nextOrigin.dy - displayHeight : 0),
|
for (var y = 0; y < displayHeight; y += layerRegionHeight) {
|
||||||
);
|
final nextX = x + layerRegionWidth;
|
||||||
final tileRect = regionOrigin * scale & thisRegionSize * scale;
|
final nextY = y + layerRegionHeight;
|
||||||
|
final thisRegionWidth = layerRegionWidth - (nextX >= displayWidth ? nextX - displayWidth : 0);
|
||||||
|
final thisRegionHeight = layerRegionHeight - (nextY >= displayHeight ? nextY - displayHeight : 0);
|
||||||
|
final tileRect = Rect.fromLTWH(x * scale, y * scale, thisRegionWidth * scale, thisRegionHeight * scale);
|
||||||
|
|
||||||
// only build visible tiles
|
// only build visible tiles
|
||||||
if (viewRect.overlaps(tileRect)) {
|
if (viewRect.overlaps(tileRect)) {
|
||||||
var regionRect = regionOrigin & thisRegionSize;
|
Rectangle<int> regionRect;
|
||||||
|
|
||||||
// apply EXIF orientation
|
|
||||||
if (_transform != null) {
|
if (_transform != null) {
|
||||||
regionRect = Rect.fromPoints(
|
// apply EXIF orientation
|
||||||
MatrixUtils.transformPoint(_transform, regionRect.topLeft),
|
final regionRectDouble = Rect.fromLTWH(x.toDouble(), y.toDouble(), thisRegionWidth.toDouble(), thisRegionHeight.toDouble());
|
||||||
MatrixUtils.transformPoint(_transform, regionRect.bottomRight),
|
final tl = MatrixUtils.transformPoint(_transform, regionRectDouble.topLeft);
|
||||||
|
final br = MatrixUtils.transformPoint(_transform, regionRectDouble.bottomRight);
|
||||||
|
regionRect = Rectangle<int>.fromPoints(
|
||||||
|
Point<int>(tl.dx.round(), tl.dy.round()),
|
||||||
|
Point<int>(br.dx.round(), br.dy.round()),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
regionRect = Rectangle<int>(x, y, thisRegionWidth, thisRegionHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles.add(RegionTile(
|
tiles.add(RegionTile(
|
||||||
|
@ -164,7 +169,8 @@ class RegionTile extends StatefulWidget {
|
||||||
|
|
||||||
// `tileRect` uses Flutter view coordinates
|
// `tileRect` uses Flutter view coordinates
|
||||||
// `regionRect` uses the raw image pixel coordinates
|
// `regionRect` uses the raw image pixel coordinates
|
||||||
final Rect tileRect, regionRect;
|
final Rect tileRect;
|
||||||
|
final Rectangle<int> regionRect;
|
||||||
final int sampleSize;
|
final int sampleSize;
|
||||||
|
|
||||||
const RegionTile({
|
const RegionTile({
|
||||||
|
@ -268,6 +274,6 @@ class _RegionTileState extends State<RegionTile> {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties.add(IntProperty('contentId', widget.entry.contentId));
|
properties.add(IntProperty('contentId', widget.entry.contentId));
|
||||||
properties.add(IntProperty('sampleSize', widget.sampleSize));
|
properties.add(IntProperty('sampleSize', widget.sampleSize));
|
||||||
properties.add(DiagnosticsProperty<Rect>('regionRect', widget.regionRect));
|
properties.add(DiagnosticsProperty<Rectangle<int>>('regionRect', widget.regionRect));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue