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.describeAll
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.getSafeRational
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis
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.MetadataExtractorHelper.getSafeBoolean
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.getSafeRational
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
@ -353,37 +354,62 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
val metadataMap = HashMap<String, Any>()
if (isVideo(mimeType) || !isSupportedByMetadataExtractor(mimeType)) {
if (isVideo(mimeType)) {
result.success(metadataMap)
return
}
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getSafeDescription(ExifSubIFDDirectory.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it }
dir.getSafeDescription(ExifSubIFDDirectory.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it }
dir.getSafeDescription(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = "ISO$it" }
dir.getSafeRational(ExifSubIFDDirectory.TAG_EXPOSURE_TIME) {
// TAG_EXPOSURE_TIME as a string is sometimes a ratio, sometimes a decimal
// so we explicitly request it as a rational (e.g. 1/100, 1/14, 71428571/1000000000, 4000/1000, 2000000000/500000000)
// and process it to make sure the numerator is `1` when the ratio value is less than 1
val num = it.numerator
val denom = it.denominator
metadataMap[KEY_EXPOSURE_TIME] = when {
num >= denom -> "${it.toSimpleString(true)}"
num != 1L && num != 0L -> Rational(1, (denom / num.toDouble()).roundToLong()).toString()
else -> it.toString()
}
val saveExposureTime: (value: Rational) -> Unit = {
// `TAG_EXPOSURE_TIME` as a string is sometimes a ratio, sometimes a decimal
// so we explicitly request it as a rational (e.g. 1/100, 1/14, 71428571/1000000000, 4000/1000, 2000000000/500000000)
// and process it to make sure the numerator is `1` when the ratio value is less than 1
val num = it.numerator
val denom = it.denominator
metadataMap[KEY_EXPOSURE_TIME] = when {
num >= denom -> "${it.toSimpleString(true)}"
num != 1L && num != 0L -> Rational(1, (denom / num.toDouble()).roundToLong()).toString()
else -> it.toString()
}
}
var foundExif = false
if (isSupportedByMetadataExtractor(mimeType)) {
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)
} ?: result.error("getOverlayMetadata-noinput", "failed to get metadata for uri=$uri", null)
} catch (e: Exception) {
result.error("getOverlayMetadata-exception", "failed to get metadata for uri=$uri", e.message)
} catch (e: NoClassDefFoundError) {
result.error("getOverlayMetadata-exception", "failed to get metadata for uri=$uri", e.message)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
} catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
}
}
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) {

View file

@ -10,12 +10,15 @@ import com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirector
import com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory
import deckers.thibault.aves.utils.LogUtils
import java.util.*
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.roundToLong
object ExifInterfaceHelper {
private val LOG_TAG = LogUtils.createTag(ExifInterfaceHelper::class.java)
private const val precisionErrorTolerance = 1e-10
// ExifInterface always states it has the following attributes
// and returns "0" instead of "null" when they are actually missing
private val neverNullTags = listOf(
@ -279,7 +282,7 @@ object ExifInterfaceHelper {
private fun toRational(s: String?): Rational? {
s ?: return null
// convert "12345/100"
// e.g. "12345/100" to Rational(12345, 100)
val parts = s.split("/")
if (parts.size == 2) {
val numerator = parts[0].toLongOrNull() ?: return null
@ -287,9 +290,20 @@ object ExifInterfaceHelper {
return Rational(numerator, denominator)
}
// convert "123.45"
var d = s.toDoubleOrNull() ?: return null
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
while (d != floor(d)) {
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) {
if (this.hasAttribute(tag)) {
// 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:geocoder/model.dart';
import 'package:intl/intl.dart';
class DateMetadata {
final int contentId, dateMillis;
@ -109,19 +110,25 @@ class CatalogMetadata {
class OverlayMetadata {
final String aperture, exposureTime, focalLength, iso;
static final apertureFormat = NumberFormat('0.0', 'en_US');
static final focalLengthFormat = NumberFormat('0.#', 'en_US');
OverlayMetadata({
String aperture,
this.exposureTime,
this.focalLength,
this.iso,
}) : aperture = aperture?.replaceFirst('f', 'ƒ');
double aperture,
String exposureTime,
double focalLength,
int iso,
}) : 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) {
return OverlayMetadata(
aperture: map['aperture'],
exposureTime: map['exposureTime'],
focalLength: map['focalLength'],
iso: map['iso'],
aperture: map['aperture'] as double,
exposureTime: map['exposureTime'] as String,
focalLength: map['focalLength'] as double,
iso: map['iso'] as int,
);
}

View file

@ -64,7 +64,7 @@ class MetadataService {
if (entry.isSvg) return null;
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>{
'mimeType': entry.mimeType,
'uri': entry.uri,

View file

@ -151,7 +151,7 @@ class _FullscreenBottomOverlayContent extends AnimatedWidget {
final positionTitle = [
if (position != null) position,
if (entry.bestTitle != null) entry.bestTitle,
].join(' '); // em dash
].join(' ');
final hasShootingDetails = details != null && !details.isEmpty && settings.showOverlayShootingDetails;
return Column(
mainAxisSize: MainAxisSize.min,