cataloguing includes date sub-second data if present
This commit is contained in:
parent
d1fdac46ca
commit
b183f9ddbb
8 changed files with 94 additions and 33 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue