exif: use location for timezone
Signed-off-by: Varun Patil <varunpatil@ucla.edu>pull/563/head
parent
86ceaf7bb2
commit
3b24a62ba0
|
@ -2,6 +2,11 @@
|
|||
|
||||
This file is manually updated. Please file an issue if something is missing.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **Feature**: Use GPS location data for timezone calculation.
|
||||
Many cameras do not store the timezone in EXIF data. This feature allows Memories to use the GPS location data to calculate the timezone. To take advantage of this, you will need to run `occ memories:places-setup` followed by `occ memories:index --clear` (or `occ memories:index -f`) to reindex your photos.
|
||||
|
||||
## v4.12.4 + v4.12.5 (2023-03-23)
|
||||
|
||||
- These releases significantly overhaul the application logic for better maintainability. If you run into any regressions, please [file a bug report](https://github.com/pulsejet/memories/issues).
|
||||
|
|
|
@ -71,6 +71,9 @@ class PlacesBackend extends Backend
|
|||
$count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count');
|
||||
$query->select('e.osm_id', 'e.name', $count)->from('memories_planet', 'e');
|
||||
|
||||
// WHERE these are not special clusters (e.g. timezone)
|
||||
$query->where($query->expr()->gt('e.admin_level', $query->createNamedParameter(0)));
|
||||
|
||||
// WHERE there are items with this osm_id
|
||||
$query->innerJoin('e', 'memories_places', 'mp', $query->expr()->eq('mp.osm_id', 'e.osm_id'));
|
||||
|
||||
|
|
|
@ -202,9 +202,10 @@ class PlacesSetup extends Command
|
|||
$boundaries = $data['geometry'];
|
||||
|
||||
// Skip some places
|
||||
if ($adminLevel <= 1 || $adminLevel >= 10) {
|
||||
if ($adminLevel > -2 && ($adminLevel <= 1 || $adminLevel >= 10)) {
|
||||
// <=1: These are too general, e.g. "Earth"? or invalid
|
||||
// >=10: These are too specific, e.g. "Community Board"
|
||||
// <-1: These are special, e.g. "Timezone" = -7
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ trait TimelineQuerySingleItem
|
|||
->from('memories_places', 'mp')
|
||||
->innerJoin('mp', 'memories_planet', 'e', $qb->expr()->eq('mp.osm_id', 'e.osm_id'))
|
||||
->where($qb->expr()->eq('mp.fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT)))
|
||||
->andWhere($qb->expr()->gt('e.admin_level', $qb->createNamedParameter(0)))
|
||||
->orderBy('e.admin_level', 'DESC')
|
||||
;
|
||||
$places = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
|
||||
|
|
|
@ -10,7 +10,6 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
|
|||
use OCP\Files\File;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IPreview;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
require_once __DIR__.'/../ExifFields.php';
|
||||
|
||||
|
@ -136,77 +135,44 @@ class TimelineWrite
|
|||
return 2;
|
||||
}
|
||||
|
||||
// Get more parameters
|
||||
// Video parameters
|
||||
$videoDuration = round((float) ($isvideo ? ($exif['Duration'] ?? $exif['TrackDuration'] ?? 0) : 0));
|
||||
|
||||
// Process location data
|
||||
// This also modifies the exif array in-place to set the LocationTZID
|
||||
[$lat, $lon, $mapCluster] = $this->processExifLocation($fileId, $exif, $prevRow);
|
||||
|
||||
// Get date parameters (after setting timezone offset)
|
||||
$dateTaken = Exif::getDateTaken($file, $exif);
|
||||
$dayId = floor($dateTaken / 86400);
|
||||
$dateTaken = gmdate('Y-m-d H:i:s', $dateTaken);
|
||||
|
||||
// Store the acutal epoch with the EXIF data
|
||||
$exif['DateTimeEpoch'] = $dateTaken->getTimestamp();
|
||||
|
||||
// Store the date taken in the database as UTC (local date) only
|
||||
// Basically, assume everything happens in Greenwich
|
||||
$dateLocalUtc = Exif::forgetTimezone($dateTaken)->getTimestamp();
|
||||
$dateTakenStr = gmdate('Y-m-d H:i:s', $dateLocalUtc);
|
||||
|
||||
// We need to use the local time in UTC for the dayId
|
||||
// This way two photos in different timezones on the same date locally
|
||||
// end up in the same dayId group
|
||||
$dayId = floor($dateLocalUtc / 86400);
|
||||
|
||||
// Get size of image
|
||||
[$w, $h] = Exif::getDimensions($exif);
|
||||
|
||||
// Get live photo ID of video part
|
||||
$liveid = $this->livePhoto->getLivePhotoId($file, $exif);
|
||||
|
||||
// Video parameters
|
||||
$videoDuration = 0;
|
||||
if ($isvideo) {
|
||||
$videoDuration = round((float) ($exif['Duration'] ?? $exif['TrackDuration'] ?? 0));
|
||||
}
|
||||
|
||||
// Clean up EXIF to keep only useful metadata
|
||||
$filteredExif = [];
|
||||
foreach ($exif as $key => $value) {
|
||||
// Truncate any fields > 2048 chars
|
||||
if (\is_string($value) && \strlen($value) > 2048) {
|
||||
$value = substr($value, 0, 2048);
|
||||
}
|
||||
|
||||
// Only keep fields in the whitelist
|
||||
if (\array_key_exists($key, EXIF_FIELDS_LIST)) {
|
||||
$filteredExif[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Store JSON string
|
||||
$exifJson = json_encode($filteredExif);
|
||||
|
||||
// Store error if data > 64kb
|
||||
if (\is_string($exifJson)) {
|
||||
if (\strlen($exifJson) > 65535) {
|
||||
$exifJson = json_encode(['error' => 'Exif data too large']);
|
||||
}
|
||||
} else {
|
||||
$exifJson = json_encode(['error' => 'Exif data encoding error']);
|
||||
}
|
||||
|
||||
// Store location data
|
||||
$lat = \array_key_exists('GPSLatitude', $exif) ? (float) $exif['GPSLatitude'] : null;
|
||||
$lon = \array_key_exists('GPSLongitude', $exif) ? (float) $exif['GPSLongitude'] : null;
|
||||
$oldLat = $prevRow ? (float) $prevRow['lat'] : null;
|
||||
$oldLon = $prevRow ? (float) $prevRow['lon'] : null;
|
||||
$mapCluster = $prevRow ? (int) $prevRow['mapcluster'] : -1;
|
||||
|
||||
if ($lat || $lon || $oldLat || $oldLon) {
|
||||
try {
|
||||
$mapCluster = $this->mapGetCluster($mapCluster, $lat, $lon, $oldLat, $oldLon);
|
||||
} catch (\Error $e) {
|
||||
$logger = \OC::$server->get(LoggerInterface::class);
|
||||
$logger->log(3, 'Error updating map cluster data: '.$e->getMessage(), ['app' => 'memories']);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->updatePlacesData($fileId, $lat, $lon);
|
||||
} catch (\Error $e) {
|
||||
$logger = \OC::$server->get(LoggerInterface::class);
|
||||
$logger->log(3, 'Error updating places data: '.$e->getMessage(), ['app' => 'memories']);
|
||||
}
|
||||
}
|
||||
|
||||
// NULL if invalid
|
||||
$mapCluster = $mapCluster <= 0 ? null : $mapCluster;
|
||||
// Get exif json
|
||||
$exifJson = $this->getExifJson($exif);
|
||||
|
||||
// Parameters for insert or update
|
||||
$params = [
|
||||
'fileid' => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT),
|
||||
'objectid' => $query->createNamedParameter((string) $fileId, IQueryBuilder::PARAM_STR),
|
||||
'dayid' => $query->createNamedParameter($dayId, IQueryBuilder::PARAM_INT),
|
||||
'datetaken' => $query->createNamedParameter($dateTaken, IQueryBuilder::PARAM_STR),
|
||||
'datetaken' => $query->createNamedParameter($dateTakenStr, IQueryBuilder::PARAM_STR),
|
||||
'mtime' => $query->createNamedParameter($mtime, IQueryBuilder::PARAM_INT),
|
||||
'isvideo' => $query->createNamedParameter($isvideo, IQueryBuilder::PARAM_INT),
|
||||
'video_duration' => $query->createNamedParameter($videoDuration, IQueryBuilder::PARAM_INT),
|
||||
|
@ -284,4 +250,38 @@ class TimelineWrite
|
|||
$this->connection->executeStatement($p->getTruncateTableSQL('*PREFIX*'.$table, false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert EXIF data to filtered JSON string.
|
||||
*/
|
||||
private function getExifJson(array $exif): string
|
||||
{
|
||||
// Clean up EXIF to keep only useful metadata
|
||||
$filteredExif = [];
|
||||
foreach ($exif as $key => $value) {
|
||||
// Truncate any fields > 2048 chars
|
||||
if (\is_string($value) && \strlen($value) > 2048) {
|
||||
$value = substr($value, 0, 2048);
|
||||
}
|
||||
|
||||
// Only keep fields in the whitelist
|
||||
if (\array_key_exists($key, EXIF_FIELDS_LIST)) {
|
||||
$filteredExif[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Store JSON string
|
||||
$exifJson = json_encode($filteredExif);
|
||||
|
||||
// Store error if data > 64kb
|
||||
if (\is_string($exifJson)) {
|
||||
if (\strlen($exifJson) > 65535) {
|
||||
$exifJson = json_encode(['error' => 'Exif data too large']);
|
||||
}
|
||||
} else {
|
||||
$exifJson = json_encode(['error' => 'Exif data encoding error']);
|
||||
}
|
||||
|
||||
return $exifJson;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,26 +6,76 @@ namespace OCA\Memories\Db;
|
|||
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
trait TimelineWritePlaces
|
||||
{
|
||||
protected IDBConnection $connection;
|
||||
|
||||
/**
|
||||
* Process the location part of exif data.
|
||||
*
|
||||
* Also update the exif data with the tzid from location (LocationTZID)
|
||||
*
|
||||
* @param int $fileId The file ID
|
||||
* @param array $exif The exif data
|
||||
* @param array|bool $prevRow The previous row of data
|
||||
*
|
||||
* @return array Update values
|
||||
*/
|
||||
protected function processExifLocation(int $fileId, array &$exif, mixed $prevRow): array
|
||||
{
|
||||
// Store location data
|
||||
$lat = \array_key_exists('GPSLatitude', $exif) ? (float) $exif['GPSLatitude'] : null;
|
||||
$lon = \array_key_exists('GPSLongitude', $exif) ? (float) $exif['GPSLongitude'] : null;
|
||||
$oldLat = $prevRow ? (float) $prevRow['lat'] : null;
|
||||
$oldLon = $prevRow ? (float) $prevRow['lon'] : null;
|
||||
$mapCluster = $prevRow ? (int) $prevRow['mapcluster'] : -1;
|
||||
$osmIds = [];
|
||||
|
||||
if ($lat || $lon || $oldLat || $oldLon) {
|
||||
try {
|
||||
$mapCluster = $this->mapGetCluster($mapCluster, $lat, $lon, $oldLat, $oldLon);
|
||||
} catch (\Error $e) {
|
||||
$logger = \OC::$server->get(LoggerInterface::class);
|
||||
$logger->log(3, 'Error updating map cluster data: '.$e->getMessage(), ['app' => 'memories']);
|
||||
}
|
||||
|
||||
try {
|
||||
$osmIds = $this->updatePlacesData($fileId, $lat, $lon);
|
||||
} catch (\Error $e) {
|
||||
$logger = \OC::$server->get(LoggerInterface::class);
|
||||
$logger->log(3, 'Error updating places data: '.$e->getMessage(), ['app' => 'memories']);
|
||||
}
|
||||
}
|
||||
|
||||
// NULL if invalid
|
||||
$mapCluster = $mapCluster <= 0 ? null : $mapCluster;
|
||||
|
||||
// Set tzid from location if not present
|
||||
$this->setTzidFromLocation($exif, $osmIds);
|
||||
|
||||
// Return update values
|
||||
return [$lat, $lon, $mapCluster, $osmIds];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add places data for a file.
|
||||
*
|
||||
* @param int $fileId The file ID
|
||||
* @param null|float $lat The latitude of the file
|
||||
* @param null|float $lon The longitude of the file
|
||||
*
|
||||
* @return array The list of osm_id of the places
|
||||
*/
|
||||
protected function updatePlacesData(int $fileId, $lat, $lon): void
|
||||
protected function updatePlacesData(int $fileId, $lat, $lon): array
|
||||
{
|
||||
// Get GIS type
|
||||
$gisType = \OCA\Memories\Util::placesGISType();
|
||||
|
||||
// Check if valid
|
||||
if ($gisType <= 0) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
// Delete previous records
|
||||
|
@ -37,7 +87,7 @@ trait TimelineWritePlaces
|
|||
|
||||
// Just remove from if the point is no longer valid
|
||||
if (null === $lat || null === $lon) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
// Construct WHERE clause depending on GIS type
|
||||
|
@ -47,7 +97,7 @@ trait TimelineWritePlaces
|
|||
} elseif (2 === $gisType) {
|
||||
$where = "POINT('{$lon},{$lat}') <@ geometry";
|
||||
} else {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
// Make query to memories_planet table
|
||||
|
@ -82,5 +132,36 @@ trait TimelineWritePlaces
|
|||
}
|
||||
|
||||
$this->connection->commit();
|
||||
|
||||
// Return list of osm_id
|
||||
return array_map(fn ($row) => $row['osm_id'], $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set timezone offset from location if not present.
|
||||
*
|
||||
* @param array $exif The exif data
|
||||
* @param array $osmIds The list of osm_id of the places
|
||||
*/
|
||||
private function setTzidFromLocation(array &$exif, array $osmIds): void
|
||||
{
|
||||
// Make sure we have some places
|
||||
if (empty($osmIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get timezone offset from places
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->select('name')
|
||||
->from('memories_planet')
|
||||
->where($query->expr()->in('osm_id', $query->createNamedParameter($osmIds, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->andWhere($query->expr()->eq('admin_level', $query->createNamedParameter(-7, IQueryBuilder::PARAM_INT)))
|
||||
;
|
||||
|
||||
// Get name of timezone
|
||||
$tzName = $query->executeQuery()->fetchOne();
|
||||
if ($tzName) {
|
||||
$exif['LocationTZID'] = $tzName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
47
lib/Exif.php
47
lib/Exif.php
|
@ -157,12 +157,8 @@ class Exif
|
|||
|
||||
/**
|
||||
* Parse date from exif format and throw error if invalid.
|
||||
*
|
||||
* @param array $exif
|
||||
*
|
||||
* @return int unix timestamp
|
||||
*/
|
||||
public static function parseExifDate(array $exif)
|
||||
public static function parseExifDate(array $exif): \DateTime
|
||||
{
|
||||
// Get date from exif
|
||||
$exifDate = $exif['SubSecDateTimeOriginal'] ?? $exif['DateTimeOriginal'] ?? $exif['CreateDate'] ?? null;
|
||||
|
@ -171,13 +167,16 @@ class Exif
|
|||
}
|
||||
|
||||
// Get timezone from exif
|
||||
$exifTz = $exif["OffsetTimeOriginal"] ?? $exif["OffsetTime"] ?? null;
|
||||
try {
|
||||
$parseTz = new \DateTimeZone($exifTz);
|
||||
$exifTz = $exif['OffsetTimeOriginal'] ?? $exif['OffsetTime'] ?? $exif['LocationTZID'] ?? null;
|
||||
$exifTz = new \DateTimeZone($exifTz);
|
||||
} catch (\Error $e) {
|
||||
$parseTz = new \DateTimeZone('UTC');
|
||||
$exifTz = null;
|
||||
}
|
||||
|
||||
// Force UTC if no timezone found
|
||||
$parseTz = $exifTz ?? new \DateTimeZone('UTC');
|
||||
|
||||
// https://github.com/pulsejet/memories/pull/397
|
||||
// https://github.com/pulsejet/memories/issues/485
|
||||
|
||||
|
@ -199,23 +198,28 @@ class Exif
|
|||
}
|
||||
}
|
||||
|
||||
// If we couldn't parse the date, throw an error
|
||||
if (!$parsedDate) {
|
||||
throw new \Exception("Invalid date: {$exifDate}");
|
||||
}
|
||||
|
||||
// Filter out dates before 1800 A.D.
|
||||
if ($parsedDate->getTimestamp() < -5364662400) { // 1800 A.D.
|
||||
throw new \Exception("Date too old: {$exifDate}");
|
||||
}
|
||||
|
||||
return $parsedDate->getTimestamp();
|
||||
// Force the timezone to be the same as parseTz
|
||||
if ($exifTz) {
|
||||
$parsedDate->setTimezone($exifTz);
|
||||
}
|
||||
|
||||
return $parsedDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the date taken from either the file or exif data if available.
|
||||
*
|
||||
* @return int unix timestamp
|
||||
*/
|
||||
public static function getDateTaken(File $file, array $exif)
|
||||
public static function getDateTaken(File $file, array $exif): \DateTime
|
||||
{
|
||||
try {
|
||||
return self::parseExifDate($exif);
|
||||
|
@ -224,7 +228,24 @@ class Exif
|
|||
}
|
||||
|
||||
// Fall back to modification time
|
||||
return $file->getMtime();
|
||||
try {
|
||||
$parseTz = new \DateTimeZone(getenv('TZ')); // debian
|
||||
} catch (\Error $e) {
|
||||
$parseTz = new \DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
$dt = new \DateTime('@'.$file->getMtime(), $parseTz);
|
||||
$dt->setTimezone($parseTz);
|
||||
|
||||
return self::forgetTimezone($dt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert time to local date in UTC.
|
||||
*/
|
||||
public static function forgetTimezone(\DateTime $date): \DateTime
|
||||
{
|
||||
return new \DateTime($date->format('Y-m-d H:i:s'), new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,10 @@ const EXIF_FIELDS_LIST = [
|
|||
'OffsetTime' => true,
|
||||
'ModifyDate' => true,
|
||||
|
||||
// Generated date fields
|
||||
'DateTimeEpoch' => true,
|
||||
'LocationTZID' => true,
|
||||
|
||||
// Camera Info
|
||||
'Make' => true,
|
||||
'Model' => true,
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"leaflet": "^1.9.3",
|
||||
"leaflet-edgebuffer": "^1.0.6",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.42",
|
||||
"path-posix": "^1.0.0",
|
||||
"photoswipe": "^5.3.6",
|
||||
"plyr": "^3.7.7",
|
||||
|
@ -7248,6 +7249,17 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.42",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.42.tgz",
|
||||
"integrity": "sha512-tjI9goqwzkflKSTxJo+jC/W8riTFwEjjunssmFvAWlvNVApjbkJM7UHggyKO0q1Fd/kZVKY77H7C9A0XKhhAFw==",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mpd-parser": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.0.1.tgz",
|
||||
|
@ -16810,6 +16822,14 @@
|
|||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"moment-timezone": {
|
||||
"version": "0.5.42",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.42.tgz",
|
||||
"integrity": "sha512-tjI9goqwzkflKSTxJo+jC/W8riTFwEjjunssmFvAWlvNVApjbkJM7UHggyKO0q1Fd/kZVKY77H7C9A0XKhhAFw==",
|
||||
"requires": {
|
||||
"moment": "^2.29.4"
|
||||
}
|
||||
},
|
||||
"mpd-parser": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.0.1.tgz",
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
"leaflet": "^1.9.3",
|
||||
"leaflet-edgebuffer": "^1.0.6",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.42",
|
||||
"path-posix": "^1.0.0",
|
||||
"photoswipe": "^5.3.6",
|
||||
"plyr": "^3.7.7",
|
||||
|
|
|
@ -58,7 +58,9 @@ import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
|
|||
import axios from "@nextcloud/axios";
|
||||
import { subscribe, unsubscribe } from "@nextcloud/event-bus";
|
||||
import { getCanonicalLocale } from "@nextcloud/l10n";
|
||||
|
||||
import moment from "moment";
|
||||
import "moment-timezone";
|
||||
|
||||
import * as utils from "../services/Utils";
|
||||
|
||||
|
@ -176,26 +178,45 @@ export default defineComponent({
|
|||
|
||||
/** Date taken info */
|
||||
dateOriginal(): moment.Moment | null {
|
||||
const m = moment.utc(this.baseInfo.datetaken * 1000);
|
||||
const epoch = this.exif.DateTimeEpoch || this.baseInfo.datetaken;
|
||||
const m = moment.utc(epoch * 1000);
|
||||
if (!m.isValid()) return null;
|
||||
m.locale(getCanonicalLocale());
|
||||
|
||||
// set timezeon
|
||||
const tz = this.exif["OffsetTimeOriginal"] || this.exif["OffsetTime"];
|
||||
if (tz) m.utcOffset(tz);
|
||||
// The fallback to datetaken can be eventually removed
|
||||
// and then this can be discarded
|
||||
if (this.exif.DateTimeEpoch) {
|
||||
const tzOffset =
|
||||
this.exif["OffsetTimeOriginal"] || this.exif["OffsetTime"];
|
||||
const tzId = this.exif["LocationTZID"];
|
||||
|
||||
if (tzOffset) {
|
||||
m.utcOffset(tzOffset);
|
||||
} else if (tzId) {
|
||||
m.tz(tzId);
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
},
|
||||
|
||||
dateOriginalStr(): string | null {
|
||||
if (!this.dateOriginal) return null;
|
||||
return utils.getLongDateStr(this.dateOriginal.toDate(), true);
|
||||
return utils.getLongDateStr(
|
||||
new Date(this.baseInfo.datetaken * 1000),
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
dateOriginalTime(): string[] | null {
|
||||
if (!this.dateOriginal) return null;
|
||||
|
||||
return [this.dateOriginal.format("h:mm A Z")];
|
||||
let format = "h:mm A";
|
||||
const fields = ["OffsetTimeOriginal", "OffsetTime", "LocationTZID"];
|
||||
if (fields.some((key) => this.exif[key])) {
|
||||
format += " Z";
|
||||
}
|
||||
|
||||
return [this.dateOriginal.format(format)];
|
||||
},
|
||||
|
||||
/** Camera make and model info */
|
||||
|
|
Loading…
Reference in New Issue