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.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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue