cataloguing includes date sub-second data if present

This commit is contained in:
Thibault Deckers 2022-03-24 17:34:32 +09:00
parent d1fdac46ca
commit b183f9ddbb
8 changed files with 94 additions and 33 deletions

View file

@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file.
### Changed ### Changed
- Viewer: quick action defaults - Viewer: quick action defaults
- cataloguing includes date sub-second data if present
### Removed ### Removed

View file

@ -47,6 +47,9 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_ITXT_DIR_NAME
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_LAST_MODIFICATION_TIME_FORMAT import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_LAST_MODIFICATION_TIME_FORMAT
import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_TIME_DIR_NAME import deckers.thibault.aves.metadata.MetadataExtractorHelper.PNG_TIME_DIR_NAME
import deckers.thibault.aves.metadata.MetadataExtractorHelper.extractPngProfile import deckers.thibault.aves.metadata.MetadataExtractorHelper.extractPngProfile
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getDateDigitizedMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getDateModifiedMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getDateOriginalMillis
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.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
@ -444,11 +447,17 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// EXIF // EXIF
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) { for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getSafeDateMillis(ExifDirectoryBase.TAG_DATETIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it } dir.getDateOriginalMillis { metadataMap[KEY_DATE_MILLIS] = it }
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
// fetch date modified from SubIFD directory first, as the sub-second tag is here
dir.getDateModifiedMillis { metadataMap[KEY_DATE_MILLIS] = it }
}
} }
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) { if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
dir.getSafeDateMillis(ExifDirectoryBase.TAG_DATETIME) { metadataMap[KEY_DATE_MILLIS] = it } // fallback to fetch date modified from IFD0 directory, without the sub-second tag
// in case there was no SubIFD directory
dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME, null)?.let { metadataMap[KEY_DATE_MILLIS] = it }
} }
dir.getSafeInt(ExifDirectoryBase.TAG_ORIENTATION) { dir.getSafeInt(ExifDirectoryBase.TAG_ORIENTATION) {
val orientation = it val orientation = it
@ -572,9 +581,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input) val exif = ExifInterface(input)
exif.getSafeDateMillis(ExifInterface.TAG_DATETIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it } exif.getSafeDateMillis(ExifInterface.TAG_DATETIME_ORIGINAL, ExifInterface.TAG_SUBSEC_TIME_ORIGINAL) { metadataMap[KEY_DATE_MILLIS] = it }
if (!metadataMap.containsKey(KEY_DATE_MILLIS)) { if (!metadataMap.containsKey(KEY_DATE_MILLIS)) {
exif.getSafeDateMillis(ExifInterface.TAG_DATETIME) { metadataMap[KEY_DATE_MILLIS] = it } exif.getSafeDateMillis(ExifInterface.TAG_DATETIME, ExifInterface.TAG_SUBSEC_TIME) { metadataMap[KEY_DATE_MILLIS] = it }
} }
exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) {
if (exif.isFlipped) flags = flags or MASK_IS_FLIPPED if (exif.isFlipped) flags = flags or MASK_IS_FLIPPED
@ -913,9 +922,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input) val metadata = ImageMetadataReader.readMetadata(input)
val tag = when (field) { val tag = when (field) {
ExifInterface.TAG_DATETIME -> ExifDirectoryBase.TAG_DATETIME ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifDirectoryBase.TAG_DATETIME_DIGITIZED ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED
ExifInterface.TAG_DATETIME_ORIGINAL -> ExifDirectoryBase.TAG_DATETIME_ORIGINAL ExifInterface.TAG_DATETIME_ORIGINAL -> ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL
ExifInterface.TAG_GPS_DATESTAMP -> GpsDirectory.TAG_DATE_STAMP ExifInterface.TAG_GPS_DATESTAMP -> GpsDirectory.TAG_DATE_STAMP
else -> { else -> {
result.error("getDate-field", "unsupported ExifInterface field=$field", null) result.error("getDate-field", "unsupported ExifInterface field=$field", null)
@ -924,11 +933,24 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
when (tag) { when (tag) {
ExifDirectoryBase.TAG_DATETIME, ExifIFD0Directory.TAG_DATETIME -> {
ExifDirectoryBase.TAG_DATETIME_DIGITIZED, for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
ExifDirectoryBase.TAG_DATETIME_ORIGINAL -> { dir.getDateModifiedMillis { dateMillis = it }
for (dir in metadata.getDirectoriesOfType(ExifDirectoryBase::class.java)) { }
dir.getSafeDateMillis(tag) { dateMillis = it } if (dateMillis == null) {
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME, null)?.let { dateMillis = it }
}
}
}
ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED -> {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getDateDigitizedMillis { dateMillis = it }
}
}
ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL -> {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getDateOriginalMillis { dateMillis = it }
} }
} }
GpsDirectory.TAG_DATE_STAMP -> { GpsDirectory.TAG_DATE_STAMP -> {

View file

@ -363,13 +363,17 @@ object ExifInterfaceHelper {
} }
} }
fun ExifInterface.getSafeDateMillis(tag: String, save: (value: Long) -> Unit) { fun ExifInterface.getSafeDateMillis(tag: String, subSecTag: String?, save: (value: Long) -> Unit) {
if (this.hasAttribute(tag)) { if (this.hasAttribute(tag)) {
val dateString = this.getAttribute(tag) val dateString = this.getAttribute(tag)
if (dateString != null) { if (dateString != null) {
try { try {
DATETIME_FORMAT.parse(dateString)?.let { date -> DATETIME_FORMAT.parse(dateString)?.let { date ->
save(date.time) var dateMillis = date.time
if (subSecTag != null && this.hasAttribute(subSecTag)) {
dateMillis += Metadata.parseSubSecond(this.getAttribute(subSecTag))
}
save(dateMillis)
} }
} catch (e: ParseException) { } catch (e: ParseException) {
Log.w(LOG_TAG, "failed to parse date=$dateString", e) Log.w(LOG_TAG, "failed to parse date=$dateString", e)

View file

@ -65,6 +65,20 @@ object Metadata {
} }
} }
fun parseSubSecond(subSecond: String?): Int {
if (subSecond != null) {
try {
val millis = (".$subSecond".toDouble() * 1000).toInt()
if (millis in 0..999) {
return millis
}
} catch (e: NumberFormatException) {
// ignore
}
}
return 0
}
// not sure which standards are used for the different video formats, // not sure which standards are used for the different video formats,
// but looks like some form of ISO 8601 `basic format`: // but looks like some form of ISO 8601 `basic format`:
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)? // yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?
@ -96,18 +110,7 @@ object Metadata {
null null
} ?: return 0 } ?: return 0
var dateMillis = date.time return date.time + parseSubSecond(subSecond)
if (subSecond != null) {
try {
val millis = (".$subSecond".toDouble() * 1000).toInt()
if (millis in 0..999) {
dateMillis += millis.toLong()
}
} catch (e: NumberFormatException) {
// ignore
}
}
return dateMillis
} }
// opening large PSD/TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1), // opening large PSD/TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),

View file

@ -6,7 +6,9 @@ import com.drew.lang.Rational
import com.drew.lang.SequentialByteArrayReader import com.drew.lang.SequentialByteArrayReader
import com.drew.metadata.Directory import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifDirectoryBase import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifReader import com.drew.metadata.exif.ExifReader
import com.drew.metadata.exif.ExifSubIFDDirectory
import com.drew.metadata.iptc.IptcReader import com.drew.metadata.iptc.IptcReader
import com.drew.metadata.png.PngDirectory import com.drew.metadata.png.PngDirectory
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
@ -53,11 +55,34 @@ object MetadataExtractorHelper {
if (this.containsTag(tag)) save(this.getRational(tag)) if (this.containsTag(tag)) save(this.getRational(tag))
} }
fun Directory.getSafeDateMillis(tag: Int, save: (value: Long) -> Unit) { fun Directory.getSafeDateMillis(tag: Int, subSecond: String?): Long? {
if (this.containsTag(tag)) { if (this.containsTag(tag)) {
val date = this.getDate(tag, null, TimeZone.getDefault()) val date = this.getDate(tag, subSecond, TimeZone.getDefault())
if (date != null) save(date.time) if (date != null) return date.time
} }
return null
}
// time tag and sub-second tag are *not* in the same directory
fun ExifSubIFDDirectory.getDateModifiedMillis(save: (value: Long) -> Unit) {
val parent = parent
if (parent is ExifIFD0Directory) {
val subSecond = getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME)
val dateMillis = parent.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME, subSecond)
if (dateMillis != null) save(dateMillis)
}
}
fun ExifSubIFDDirectory.getDateDigitizedMillis(save: (value: Long) -> Unit) {
val subSecond = getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED)
val dateMillis = this.getSafeDateMillis(ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED, subSecond)
if (dateMillis != null) save(dateMillis)
}
fun ExifSubIFDDirectory.getDateOriginalMillis(save: (value: Long) -> Unit) {
val subSecond = getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL)
val dateMillis = this.getSafeDateMillis(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL, subSecond)
if (dateMillis != null) save(dateMillis)
} }
// geotiff // geotiff

View file

@ -185,7 +185,7 @@ class SourceEntry {
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_WIDTH) { width = it } dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_WIDTH) { width = it }
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT) { height = it } dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT) { height = it }
dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) { sourceRotationDegrees = getRotationDegreesForExifCode(it) } dir.getSafeInt(ExifIFD0Directory.TAG_ORIENTATION) { sourceRotationDegrees = getRotationDegreesForExifCode(it) }
dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME) { sourceDateTakenMillis = it } dir.getSafeDateMillis(ExifIFD0Directory.TAG_DATETIME, null)?.let { sourceDateTakenMillis = it }
} }
// dimensions reported in EXIF do not always match the image // dimensions reported in EXIF do not always match the image
@ -218,7 +218,7 @@ class SourceEntry {
exif.getSafeInt(ExifInterface.TAG_IMAGE_WIDTH, acceptZero = false) { width = it } exif.getSafeInt(ExifInterface.TAG_IMAGE_WIDTH, acceptZero = false) { width = it }
exif.getSafeInt(ExifInterface.TAG_IMAGE_LENGTH, acceptZero = false) { height = it } exif.getSafeInt(ExifInterface.TAG_IMAGE_LENGTH, acceptZero = false) { height = it }
exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { sourceRotationDegrees = exif.rotationDegrees } exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) { sourceRotationDegrees = exif.rotationDegrees }
exif.getSafeDateMillis(ExifInterface.TAG_DATETIME) { sourceDateTakenMillis = it } exif.getSafeDateMillis(ExifInterface.TAG_DATETIME, ExifInterface.TAG_SUBSEC_TIME) { sourceDateTakenMillis = it }
} }
} catch (e: Exception) { } catch (e: Exception) {
// ExifInterface initialization can fail with a RuntimeException // ExifInterface initialization can fail with a RuntimeException

View file

@ -756,7 +756,13 @@ abstract class ImageProvider {
ExifInterface.TAG_DATETIME_DIGITIZED, ExifInterface.TAG_DATETIME_DIGITIZED,
).forEach { field -> ).forEach { field ->
if (fields.contains(field)) { if (fields.contains(field)) {
exif.getSafeDateMillis(field) { date -> val subSecTag = when (field) {
ExifInterface.TAG_DATETIME -> ExifInterface.TAG_SUBSEC_TIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifInterface.TAG_SUBSEC_TIME_DIGITIZED
ExifInterface.TAG_DATETIME_ORIGINAL -> ExifInterface.TAG_SUBSEC_TIME_ORIGINAL
else -> null
}
exif.getSafeDateMillis(field, subSecTag) { date ->
exif.setAttribute(field, ExifInterfaceHelper.DATETIME_FORMAT.format(date + shiftMillis)) exif.setAttribute(field, ExifInterfaceHelper.DATETIME_FORMAT.format(date + shiftMillis))
} }
} }

View file

@ -149,7 +149,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
showFeedback(context, copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count)); showFeedback(context, copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count));
} else { } else {
final count = movedOps.length; final count = movedOps.length;
final appMode = context.read<ValueNotifier<AppMode>>().value; final appMode = context.read<ValueNotifier<AppMode>?>()?.value;
SnackBarAction? action; SnackBarAction? action;
if (count > 0 && appMode == AppMode.main && !toBin) { if (count > 0 && appMode == AppMode.main && !toBin) {