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
|
### Changed
|
||||||
|
|
||||||
- Viewer: quick action defaults
|
- Viewer: quick action defaults
|
||||||
|
- cataloguing includes date sub-second data if present
|
||||||
|
|
||||||
### Removed
|
### 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_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 -> {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue