#814 parse exif date written as epoch time
This commit is contained in:
parent
3909b9223d
commit
b129255dca
2 changed files with 102 additions and 2 deletions
|
@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
- temporary files remaining in the cache directory forever
|
- temporary files remaining in the cache directory forever
|
||||||
- detecting motion photos with more items in the XMP Container directory
|
- detecting motion photos with more items in the XMP Container directory
|
||||||
|
- parsing EXIF date written as epoch time
|
||||||
|
|
||||||
## <a id="v1.9.7"></a>[v1.9.7] - 2023-10-17
|
## <a id="v1.9.7"></a>[v1.9.7] - 2023-10-17
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,14 @@ import deckers.thibault.aves.utils.LogUtils
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.GregorianCalendar
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
object Helper {
|
object Helper {
|
||||||
private val LOG_TAG = LogUtils.createTag<Helper>()
|
private val LOG_TAG = LogUtils.createTag<Helper>()
|
||||||
|
@ -150,12 +156,105 @@ object Helper {
|
||||||
|
|
||||||
fun Directory.getSafeDateMillis(tag: Int, subSecond: String?): Long? {
|
fun Directory.getSafeDateMillis(tag: Int, subSecond: String?): Long? {
|
||||||
if (this.containsTag(tag)) {
|
if (this.containsTag(tag)) {
|
||||||
val date = this.getDate(tag, subSecond, TimeZone.getDefault())
|
val date = this.getDatePlus(tag, subSecond, TimeZone.getDefault())
|
||||||
if (date != null) return date.time
|
if (date != null) return date.time
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This seems to cover all known Exif and Xmp date strings
|
||||||
|
// Note that " : : : : " is a valid date string according to the Exif spec (which means 'unknown date'): http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html
|
||||||
|
private val datePatterns = arrayOf(
|
||||||
|
"yyyy:MM:dd HH:mm:ss",
|
||||||
|
"yyyy:MM:dd HH:mm",
|
||||||
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
|
"yyyy-MM-dd HH:mm",
|
||||||
|
"yyyy.MM.dd HH:mm:ss",
|
||||||
|
"yyyy.MM.dd HH:mm",
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss",
|
||||||
|
"yyyy-MM-dd'T'HH:mm",
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
"yyyy-MM",
|
||||||
|
"yyyyMMdd", // as used in IPTC data
|
||||||
|
"yyyy"
|
||||||
|
)
|
||||||
|
private val subsecondPattern = Pattern.compile("(\\d\\d:\\d\\d:\\d\\d)(\\.\\d+)")
|
||||||
|
private val timeZonePattern = Pattern.compile("(Z|[+-]\\d\\d:\\d\\d|[+-]\\d\\d\\d\\d)$")
|
||||||
|
private val calendar: Calendar = GregorianCalendar()
|
||||||
|
private const val PARSED_DATE_YEAR_MAX = 10000
|
||||||
|
|
||||||
|
// adapted from `metadata-extractor` v2.18.0 `Directory.getDate()`
|
||||||
|
// to also parse dates written as timestamps
|
||||||
|
private fun Directory.getDatePlus(tagType: Int, subSecond: String?, timeZone: TimeZone?): Date? {
|
||||||
|
var effectiveSubSecond = subSecond
|
||||||
|
var effectiveTimeZone = timeZone
|
||||||
|
val o = this.getObject(tagType)
|
||||||
|
if (o is Date) return o
|
||||||
|
|
||||||
|
var date: Date? = null
|
||||||
|
if (o is String || o is StringValue) {
|
||||||
|
var dateString = o.toString()
|
||||||
|
|
||||||
|
// if the date string has subsecond information, it supersedes the subsecond parameter
|
||||||
|
val subsecondMatcher = subsecondPattern.matcher(dateString)
|
||||||
|
if (subsecondMatcher.find()) {
|
||||||
|
effectiveSubSecond = subsecondMatcher.group(2)?.substring(1)
|
||||||
|
dateString = subsecondMatcher.replaceAll("$1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the date string has time zone information, it supersedes the timeZone parameter
|
||||||
|
val timeZoneMatcher = timeZonePattern.matcher(dateString)
|
||||||
|
if (timeZoneMatcher.find()) {
|
||||||
|
effectiveTimeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replace("Z".toRegex(), ""))
|
||||||
|
dateString = timeZoneMatcher.replaceAll("")
|
||||||
|
}
|
||||||
|
for (datePattern in datePatterns) {
|
||||||
|
try {
|
||||||
|
val parsed = SimpleDateFormat(datePattern, Locale.ROOT).apply {
|
||||||
|
this.timeZone = effectiveTimeZone ?: TimeZone.getTimeZone("GMT") // don't interpret zone time
|
||||||
|
}.parse(dateString)
|
||||||
|
if (parsed != null) {
|
||||||
|
calendar.time = parsed
|
||||||
|
if (calendar.get(Calendar.YEAR) < PARSED_DATE_YEAR_MAX) {
|
||||||
|
date = parsed
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: ParseException) {
|
||||||
|
// simply try the next pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (date == null) {
|
||||||
|
val dateLong = dateString.toLongOrNull()
|
||||||
|
if (dateLong != null) {
|
||||||
|
val epochTimeMillis = when (dateLong) {
|
||||||
|
in 0..99999999999 -> dateLong * 1000 // seconds
|
||||||
|
in 100000000000..99999999999999 -> dateLong // millis
|
||||||
|
in 100000000000000..9999999999999999 -> dateLong / 1000 // micros
|
||||||
|
else -> dateLong / 1000000 // nanos
|
||||||
|
}
|
||||||
|
date = Date(epochTimeMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (date == null) return null
|
||||||
|
|
||||||
|
if (effectiveSubSecond != null) {
|
||||||
|
try {
|
||||||
|
val millisecond = (".$effectiveSubSecond".toDouble() * 1000).toInt()
|
||||||
|
if (millisecond in 0..999) {
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.time = date
|
||||||
|
calendar[Calendar.MILLISECOND] = millisecond
|
||||||
|
return calendar.time
|
||||||
|
}
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
// time tag and sub-second tag are *not* in the same directory
|
// time tag and sub-second tag are *not* in the same directory
|
||||||
fun ExifSubIFDDirectory.getDateModifiedMillis(save: (value: Long) -> Unit) {
|
fun ExifSubIFDDirectory.getDateModifiedMillis(save: (value: Long) -> Unit) {
|
||||||
val parent = parent
|
val parent = parent
|
||||||
|
|
Loading…
Reference in a new issue