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
- Viewer: quick action defaults
- cataloguing includes date sub-second data if present
### 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_TIME_DIR_NAME
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.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
@ -444,11 +447,17 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// EXIF
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)) {
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) {
val orientation = it
@ -572,9 +581,9 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { 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)) {
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) {
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 ->
val metadata = ImageMetadataReader.readMetadata(input)
val tag = when (field) {
ExifInterface.TAG_DATETIME -> ExifDirectoryBase.TAG_DATETIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifDirectoryBase.TAG_DATETIME_DIGITIZED
ExifInterface.TAG_DATETIME_ORIGINAL -> ExifDirectoryBase.TAG_DATETIME_ORIGINAL
ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED
ExifInterface.TAG_DATETIME_ORIGINAL -> ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL
ExifInterface.TAG_GPS_DATESTAMP -> GpsDirectory.TAG_DATE_STAMP
else -> {
result.error("getDate-field", "unsupported ExifInterface field=$field", null)
@ -924,11 +933,24 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
when (tag) {
ExifDirectoryBase.TAG_DATETIME,
ExifDirectoryBase.TAG_DATETIME_DIGITIZED,
ExifDirectoryBase.TAG_DATETIME_ORIGINAL -> {
for (dir in metadata.getDirectoriesOfType(ExifDirectoryBase::class.java)) {
dir.getSafeDateMillis(tag) { dateMillis = it }
ExifIFD0Directory.TAG_DATETIME -> {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
dir.getDateModifiedMillis { 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 -> {

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)) {
val dateString = this.getAttribute(tag)
if (dateString != null) {
try {
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) {
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,
// but looks like some form of ISO 8601 `basic format`:
// yyyyMMddTHHmmss(.sss)?(Z|+/-hhmm)?
@ -96,18 +110,7 @@ object Metadata {
null
} ?: return 0
var dateMillis = date.time
if (subSecond != null) {
try {
val millis = (".$subSecond".toDouble() * 1000).toInt()
if (millis in 0..999) {
dateMillis += millis.toLong()
}
} catch (e: NumberFormatException) {
// ignore
}
}
return dateMillis
return date.time + parseSubSecond(subSecond)
}
// 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.metadata.Directory
import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifReader
import com.drew.metadata.exif.ExifSubIFDDirectory
import com.drew.metadata.iptc.IptcReader
import com.drew.metadata.png.PngDirectory
import deckers.thibault.aves.utils.LogUtils
@ -53,11 +55,34 @@ object MetadataExtractorHelper {
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)) {
val date = this.getDate(tag, null, TimeZone.getDefault())
if (date != null) save(date.time)
val date = this.getDate(tag, subSecond, TimeZone.getDefault())
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

View file

@ -185,7 +185,7 @@ class SourceEntry {
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_WIDTH) { width = it }
dir.getSafeInt(ExifIFD0Directory.TAG_IMAGE_HEIGHT) { height = 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
@ -218,7 +218,7 @@ class SourceEntry {
exif.getSafeInt(ExifInterface.TAG_IMAGE_WIDTH, acceptZero = false) { width = it }
exif.getSafeInt(ExifInterface.TAG_IMAGE_LENGTH, acceptZero = false) { height = it }
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) {
// ExifInterface initialization can fail with a RuntimeException

View file

@ -756,7 +756,13 @@ abstract class ImageProvider {
ExifInterface.TAG_DATETIME_DIGITIZED,
).forEach { 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))
}
}

View file

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