overlay: fixed getting shooting details with ExifInterface

This commit is contained in:
Thibault Deckers 2020-11-11 12:42:54 +09:00
parent 6a8122e456
commit 5de5b7e88e
5 changed files with 104 additions and 39 deletions

View file

@ -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) {

View file

@ -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

View file

@ -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,
); );
} }

View file

@ -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,

View file

@ -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,