overlay: fixed getting shooting details with ExifInterface
This commit is contained in:
parent
6a8122e456
commit
5de5b7e88e
5 changed files with 104 additions and 39 deletions
|
@ -28,7 +28,9 @@ import com.drew.metadata.xmp.XmpDirectory
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
||||||
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt
|
||||||
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeRational
|
||||||
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
|
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
|
||||||
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis
|
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDescription
|
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDescription
|
||||||
|
@ -38,7 +40,6 @@ import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode
|
||||||
import deckers.thibault.aves.metadata.Metadata.isFlippedForExifCode
|
import deckers.thibault.aves.metadata.Metadata.isFlippedForExifCode
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeBoolean
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeBoolean
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDescription
|
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
||||||
|
@ -353,37 +354,62 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadataMap = HashMap<String, Any>()
|
val metadataMap = HashMap<String, Any>()
|
||||||
if (isVideo(mimeType) || !isSupportedByMetadataExtractor(mimeType)) {
|
if (isVideo(mimeType)) {
|
||||||
result.success(metadataMap)
|
result.success(metadataMap)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
StorageUtils.openInputStream(context, uri)?.use { input ->
|
val saveExposureTime: (value: Rational) -> Unit = {
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
// `TAG_EXPOSURE_TIME` as a string is sometimes a ratio, sometimes a decimal
|
||||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
// so we explicitly request it as a rational (e.g. 1/100, 1/14, 71428571/1000000000, 4000/1000, 2000000000/500000000)
|
||||||
dir.getSafeDescription(ExifSubIFDDirectory.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it }
|
// and process it to make sure the numerator is `1` when the ratio value is less than 1
|
||||||
dir.getSafeDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it }
|
val num = it.numerator
|
||||||
dir.getSafeDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = "ISO$it" }
|
val denom = it.denominator
|
||||||
dir.getSafeRational(ExifSubIFDDirectory.TAG_EXPOSURE_TIME) {
|
metadataMap[KEY_EXPOSURE_TIME] = when {
|
||||||
// TAG_EXPOSURE_TIME as a string is sometimes a ratio, sometimes a decimal
|
num >= denom -> "${it.toSimpleString(true)}″"
|
||||||
// so we explicitly request it as a rational (e.g. 1/100, 1/14, 71428571/1000000000, 4000/1000, 2000000000/500000000)
|
num != 1L && num != 0L -> Rational(1, (denom / num.toDouble()).roundToLong()).toString()
|
||||||
// and process it to make sure the numerator is `1` when the ratio value is less than 1
|
else -> it.toString()
|
||||||
val num = it.numerator
|
}
|
||||||
val denom = it.denominator
|
}
|
||||||
metadataMap[KEY_EXPOSURE_TIME] = when {
|
|
||||||
num >= denom -> "${it.toSimpleString(true)}″"
|
var foundExif = false
|
||||||
num != 1L && num != 0L -> Rational(1, (denom / num.toDouble()).roundToLong()).toString()
|
if (isSupportedByMetadataExtractor(mimeType)) {
|
||||||
else -> it.toString()
|
try {
|
||||||
}
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
|
val metadata = ImageMetadataReader.readMetadata(input)
|
||||||
|
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||||
|
foundExif = true
|
||||||
|
dir.getSafeRational(ExifSubIFDDirectory.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
|
||||||
|
dir.getSafeRational(ExifSubIFDDirectory.TAG_EXPOSURE_TIME, saveExposureTime)
|
||||||
|
dir.getSafeRational(ExifSubIFDDirectory.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it.numerator.toDouble() / it.denominator }
|
||||||
|
dir.getSafeInt(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.success(metadataMap)
|
} catch (e: Exception) {
|
||||||
} ?: result.error("getOverlayMetadata-noinput", "failed to get metadata for uri=$uri", null)
|
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
|
||||||
} catch (e: Exception) {
|
} catch (e: NoClassDefFoundError) {
|
||||||
result.error("getOverlayMetadata-exception", "failed to get metadata for uri=$uri", e.message)
|
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
|
||||||
} catch (e: NoClassDefFoundError) {
|
}
|
||||||
result.error("getOverlayMetadata-exception", "failed to get metadata for uri=$uri", e.message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!foundExif) {
|
||||||
|
// fallback to read EXIF via ExifInterface
|
||||||
|
try {
|
||||||
|
StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||||
|
val exif = ExifInterface(input)
|
||||||
|
exif.getSafeDouble(ExifInterface.TAG_F_NUMBER) { metadataMap[KEY_APERTURE] = it }
|
||||||
|
exif.getSafeRational(ExifInterface.TAG_EXPOSURE_TIME, saveExposureTime)
|
||||||
|
exif.getSafeDouble(ExifInterface.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it }
|
||||||
|
exif.getSafeInt(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY) { metadataMap[KEY_ISO] = it }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ExifInterface initialization can fail with a RuntimeException
|
||||||
|
// caused by an internal MediaMetadataRetriever failure
|
||||||
|
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for uri=$uri", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success(metadataMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getContentResolverMetadata(call: MethodCall, result: MethodChannel.Result) {
|
private fun getContentResolverMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
|
|
@ -10,12 +10,15 @@ import com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirector
|
||||||
import com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory
|
import com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
object ExifInterfaceHelper {
|
object ExifInterfaceHelper {
|
||||||
private val LOG_TAG = LogUtils.createTag(ExifInterfaceHelper::class.java)
|
private val LOG_TAG = LogUtils.createTag(ExifInterfaceHelper::class.java)
|
||||||
|
|
||||||
|
private const val precisionErrorTolerance = 1e-10
|
||||||
|
|
||||||
// ExifInterface always states it has the following attributes
|
// ExifInterface always states it has the following attributes
|
||||||
// and returns "0" instead of "null" when they are actually missing
|
// and returns "0" instead of "null" when they are actually missing
|
||||||
private val neverNullTags = listOf(
|
private val neverNullTags = listOf(
|
||||||
|
@ -279,7 +282,7 @@ object ExifInterfaceHelper {
|
||||||
private fun toRational(s: String?): Rational? {
|
private fun toRational(s: String?): Rational? {
|
||||||
s ?: return null
|
s ?: return null
|
||||||
|
|
||||||
// convert "12345/100"
|
// e.g. "12345/100" to Rational(12345, 100)
|
||||||
val parts = s.split("/")
|
val parts = s.split("/")
|
||||||
if (parts.size == 2) {
|
if (parts.size == 2) {
|
||||||
val numerator = parts[0].toLongOrNull() ?: return null
|
val numerator = parts[0].toLongOrNull() ?: return null
|
||||||
|
@ -287,9 +290,20 @@ object ExifInterfaceHelper {
|
||||||
return Rational(numerator, denominator)
|
return Rational(numerator, denominator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert "123.45"
|
|
||||||
var d = s.toDoubleOrNull() ?: return null
|
var d = s.toDoubleOrNull() ?: return null
|
||||||
if (d == 0.0) return Rational(0, 1)
|
if (d == 0.0) return Rational(0, 1)
|
||||||
|
|
||||||
|
// e.g. "0.02564102564102564" to Rational(1, 39)
|
||||||
|
if (d < 1) {
|
||||||
|
val numerator = 1L
|
||||||
|
val f = numerator / d
|
||||||
|
val denominator = f.roundToLong()
|
||||||
|
if (abs(f - denominator) < precisionErrorTolerance) {
|
||||||
|
return Rational(numerator, denominator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. "123.45" to Rational(12345, 100)
|
||||||
var denominator: Long = 1
|
var denominator: Long = 1
|
||||||
while (d != floor(d)) {
|
while (d != floor(d)) {
|
||||||
denominator *= 10
|
denominator *= 10
|
||||||
|
@ -321,6 +335,24 @@ object ExifInterfaceHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ExifInterface.getSafeDouble(tag: String, save: (value: Double) -> Unit) {
|
||||||
|
if (this.hasAttribute(tag)) {
|
||||||
|
val value = this.getAttributeDouble(tag, Double.NaN)
|
||||||
|
if (!value.isNaN()) {
|
||||||
|
save(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ExifInterface.getSafeRational(tag: String, save: (value: Rational) -> Unit) {
|
||||||
|
if (this.hasAttribute(tag)) {
|
||||||
|
val value = toRational(this.getAttribute(tag))
|
||||||
|
if (value != null) {
|
||||||
|
save(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun ExifInterface.getSafeDateMillis(tag: String, save: (value: Long) -> Unit) {
|
fun ExifInterface.getSafeDateMillis(tag: String, save: (value: Long) -> Unit) {
|
||||||
if (this.hasAttribute(tag)) {
|
if (this.hasAttribute(tag)) {
|
||||||
// TODO TLAD parse date with "yyyy:MM:dd HH:mm:ss" or find the original long
|
// TODO TLAD parse date with "yyyy:MM:dd HH:mm:ss" or find the original long
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:geocoder/model.dart';
|
import 'package:geocoder/model.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class DateMetadata {
|
class DateMetadata {
|
||||||
final int contentId, dateMillis;
|
final int contentId, dateMillis;
|
||||||
|
@ -109,19 +110,25 @@ class CatalogMetadata {
|
||||||
class OverlayMetadata {
|
class OverlayMetadata {
|
||||||
final String aperture, exposureTime, focalLength, iso;
|
final String aperture, exposureTime, focalLength, iso;
|
||||||
|
|
||||||
|
static final apertureFormat = NumberFormat('0.0', 'en_US');
|
||||||
|
static final focalLengthFormat = NumberFormat('0.#', 'en_US');
|
||||||
|
|
||||||
OverlayMetadata({
|
OverlayMetadata({
|
||||||
String aperture,
|
double aperture,
|
||||||
this.exposureTime,
|
String exposureTime,
|
||||||
this.focalLength,
|
double focalLength,
|
||||||
this.iso,
|
int iso,
|
||||||
}) : aperture = aperture?.replaceFirst('f', 'ƒ');
|
}) : aperture = aperture != null ? 'ƒ/${apertureFormat.format(aperture)}' : null,
|
||||||
|
exposureTime = exposureTime,
|
||||||
|
focalLength = focalLength != null ? '${focalLengthFormat.format(focalLength)} mm' : null,
|
||||||
|
iso = iso != null ? 'ISO$iso' : null;
|
||||||
|
|
||||||
factory OverlayMetadata.fromMap(Map map) {
|
factory OverlayMetadata.fromMap(Map map) {
|
||||||
return OverlayMetadata(
|
return OverlayMetadata(
|
||||||
aperture: map['aperture'],
|
aperture: map['aperture'] as double,
|
||||||
exposureTime: map['exposureTime'],
|
exposureTime: map['exposureTime'] as String,
|
||||||
focalLength: map['focalLength'],
|
focalLength: map['focalLength'] as double,
|
||||||
iso: map['iso'],
|
iso: map['iso'] as int,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ class MetadataService {
|
||||||
if (entry.isSvg) return null;
|
if (entry.isSvg) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// return map with string descriptions for: 'aperture' 'exposureTime' 'focalLength' 'iso'
|
// return map with values for: 'aperture' (double), 'exposureTime' (description), 'focalLength' (double), 'iso' (int)
|
||||||
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
final result = await platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
|
|
|
@ -151,7 +151,7 @@ class _FullscreenBottomOverlayContent extends AnimatedWidget {
|
||||||
final positionTitle = [
|
final positionTitle = [
|
||||||
if (position != null) position,
|
if (position != null) position,
|
||||||
if (entry.bestTitle != null) entry.bestTitle,
|
if (entry.bestTitle != null) entry.bestTitle,
|
||||||
].join(' — '); // em dash
|
].join(' • ');
|
||||||
final hasShootingDetails = details != null && !details.isEmpty && settings.showOverlayShootingDetails;
|
final hasShootingDetails = details != null && !details.isEmpty && settings.showOverlayShootingDetails;
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
Loading…
Reference in a new issue