diff --git a/src/backend/model/fileaccess/MetadataLoader.ts b/src/backend/model/fileaccess/MetadataLoader.ts index a7f474da..576db791 100644 --- a/src/backend/model/fileaccess/MetadataLoader.ts +++ b/src/backend/model/fileaccess/MetadataLoader.ts @@ -380,27 +380,27 @@ export class MetadataLoader { if (!offset) { //Find offset among other options if possible offset = exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); } - metadata.creationDate = Utils.timestampToMS(exif.exif.DateTimeOriginal, offset); + metadata.creationDate = Utils.timestampToMS(exif.exif.DateTimeOriginal, offset) || metadata.creationDate; } else if (exif.exif.CreateDate) { //using else if here, because DateTimeOriginal has preceedence //Create is when the camera wrote the file (typically within the same ms as shutter close) offset = exif.exif.OffsetTimeDigitized; //OffsetTimeDigitized is the corresponding offset if (!offset) { //Find offset among other options if possible offset = exif.exif.OffsetTimeOriginal || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); } - metadata.creationDate = Utils.timestampToMS(exif.exif.CreateDate, offset); + metadata.creationDate = Utils.timestampToMS(exif.exif.CreateDate, offset) || metadata.creationDate; } else if (exif.ifd0?.ModifyDate) { //using else if here, because DateTimeOriginal and CreatDate have preceedence offset = exif.exif.OffsetTime; //exif.Offsettime is the offset corresponding to ifd0.ModifyDate if (!offset) { //Find offset among other options if possible offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); } - metadata.creationDate = Utils.timestampToMS(exif.ifd0.ModifyDate, offset); + metadata.creationDate = Utils.timestampToMS(exif.ifd0.ModifyDate, offset) || metadata.creationDate; } else if (exif.ihdr && exif.ihdr["Creation Time"]) {// again else if (another fallback date if the good ones aren't there) { const any_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); metadata.creationDate = Utils.timestampToMS(exif.ihdr["Creation Time"], any_offset); offset = any_offset; } else if (exif.xmp?.MetadataDate) {// again else if (another fallback date if the good ones aren't there - metadata date is probably later than actual creation date, but much better than file time) { const any_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); - metadata.creationDate = Utils.timestampToMS(exif.xmp.MetadataDate, any_offset); + metadata.creationDate = Utils.timestampToMS(exif.xmp.MetadataDate, any_offset) || metadata.creationDate; offset = any_offset; } metadata.creationDateOffset = offset || metadata.creationDateOffset; diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 2750cf29..ce4f5269 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -186,12 +186,12 @@ export class Utils { gps.GPSDateStamp && gps.GPSTimeStamp) { //else use exif.gps.GPS*Stamp if available //GPS timestamp is always UTC (+00:00) - UTCTimestamp = gps.GPSDateStamp.replaceAll(':', '-') + gps.GPSTimeStamp.join(':'); + UTCTimestamp = gps.GPSDateStamp.replaceAll(':', '-') + " " + gps.GPSTimeStamp.map((num: any) => Utils.zeroPad(num ,2)).join(':'); } if (UTCTimestamp && timestamp) { //offset in minutes is the difference between gps timestamp and given timestamp //to calculate this correctly, we have to work with the same offset - const offsetMinutes = (Utils.timestampToMS(timestamp, '+00:00')- Utils.timestampToMS(UTCTimestamp, '+00:00')) / 1000 / 60; + const offsetMinutes: number = Math.round((Utils.timestampToMS(timestamp, '+00:00')- Utils.timestampToMS(UTCTimestamp, '+00:00')) / 1000 / 60); return Utils.getOffsetString(offsetMinutes); } else { return undefined; @@ -202,13 +202,22 @@ export class Utils { if (-720 <= offsetMinutes && offsetMinutes <= 840) { //valid offset is within -12 and +14 hrs (https://en.wikipedia.org/wiki/List_of_UTC_offsets) return (offsetMinutes < 0 ? "-" : "+") + //leading +/- - ("0" + Math.trunc(Math.abs(offsetMinutes) / 60)).slice(-2) + ":" + //zeropadded hours and ':' - ("0" + Math.abs(offsetMinutes) % 60).slice(-2); //zeropadded minutes + Utils.zeroPad(Math.trunc(Math.abs(offsetMinutes) / 60), 2) + ":" + //zeropadded hours and ':' + Utils.zeroPad((Math.abs(offsetMinutes) % 60), 2); //zeropadded minutes } else { return undefined; } } + static zeroPad(number: any, length: number): string { + if (!isNaN(number)) { + const zerosToAdd = Math.max(length - String(number).length, 0); + return '0'.repeat(zerosToAdd) + number; + } else { + return '0'.repeat(number); + } + } + static getOffsetMinutes(offsetString: string) { //Convert offset string (+HH:MM or -HH:MM) into a minute value const regex = /^([+-](0[0-9]|1[0-4]):[0-5][0-9])$/; //checks if offset is between -14:00 and +14:00. //-12:00 is the lowest valid UTC-offset, but we allow down to -14 for efficiency diff --git a/test/backend/assets/date_issue.json b/test/backend/assets/date_issue.json index ee6f2cf0..555977fc 100644 --- a/test/backend/assets/date_issue.json +++ b/test/backend/assets/date_issue.json @@ -7,7 +7,8 @@ "make": "HUAWEI", "model": "HUAWEI G6-L11" }, - "creationDate": 1460826466000, + "creationDate": 1460819266000, + "creationDateOffset": "+02:00", "fileSize": 1980, "size": { "height": 1, diff --git a/test/backend/assets/edge_case_exif_data/date_error.json b/test/backend/assets/edge_case_exif_data/date_error.json index 7d973302..889aaf1c 100644 --- a/test/backend/assets/edge_case_exif_data/date_error.json +++ b/test/backend/assets/edge_case_exif_data/date_error.json @@ -7,7 +7,7 @@ "make": "NIKON", "model": "E880" }, - "creationDate": 0, + "creationDate": "fileModificationTime", "fileSize": 72850, "size": { "height": 768, diff --git a/test/backend/assets/orientation/broken_orientation_exif.json b/test/backend/assets/orientation/broken_orientation_exif.json index 237c10d5..b66b9bf6 100644 --- a/test/backend/assets/orientation/broken_orientation_exif.json +++ b/test/backend/assets/orientation/broken_orientation_exif.json @@ -3,7 +3,8 @@ "width": 3024, "height": 4032 }, - "creationDate": 1518964712000, + "creationDate": 1518982712000, + "creationDateOffset": "-05:00", "fileSize": 256001, "cameraData": { "model": "Pixel 2", diff --git a/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min.jpg b/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min.jpg new file mode 100644 index 00000000..ce8586ee Binary files /dev/null and b/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min.jpg differ diff --git a/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min.json b/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min.json new file mode 100644 index 00000000..28844e96 --- /dev/null +++ b/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min.json @@ -0,0 +1,25 @@ +{ + "size": { + "width": 200, + "height": 300 + }, + "creationDate": 1686141955000, + "creationDateOffset": "+01:00", + "fileSize": 18663, + "cameraData": { + "model": "Canon EOS R5", + "make": "Canon" + }, + "positionData": { + "GPSData": { + "longitude": -0.124575, + "latitude": 51.500694 + }, + "country": "Storbritannien", + "state": "England", + "city": "St James's" + }, + "keywords": [ + "Big Ben" + ] +} \ No newline at end of file diff --git a/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min_no_xmpgps.jpg b/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min_no_xmpgps.jpg new file mode 100644 index 00000000..c1cf0ff1 Binary files /dev/null and b/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min_no_xmpgps.jpg differ diff --git a/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min_no_xmpgps.json b/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min_no_xmpgps.json new file mode 100644 index 00000000..df48c9ee --- /dev/null +++ b/test/backend/assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min_no_xmpgps.json @@ -0,0 +1,25 @@ +{ + "size": { + "width": 200, + "height": 300 + }, + "creationDate": 1686141955000, + "creationDateOffset": "+01:00", + "fileSize": 18601, + "cameraData": { + "model": "Canon EOS R5", + "make": "Canon" + }, + "positionData": { + "GPSData": { + "longitude": -0.124575, + "latitude": 51.500694 + }, + "country": "Storbritannien", + "state": "England", + "city": "St James's" + }, + "keywords": [ + "Big Ben" + ] +} \ No newline at end of file diff --git a/test/backend/unit/model/threading/MetaDataLoader.spec.ts b/test/backend/unit/model/threading/MetaDataLoader.spec.ts index c47f4975..525c6a88 100644 --- a/test/backend/unit/model/threading/MetaDataLoader.spec.ts +++ b/test/backend/unit/model/threading/MetaDataLoader.spec.ts @@ -11,6 +11,18 @@ import {DatabaseType} from '../../../../../src/common/config/private/PrivateConf declare const before: any; +function getFileModificationTime(filename: string): Promise { + return new Promise((resolve, reject) => { + fs.stat(filename, (err, stats) => { + if (err) { + reject(err); + } else { + resolve(stats.mtime); + } + }); + }); +} + describe('MetadataLoader', () => { // loading default settings (this might have been changed by other tests) @@ -114,6 +126,16 @@ describe('MetadataLoader', () => { const expected = require(path.join(__dirname, '/../../../assets/timestamps/big_ben_no_tsoffset_but_gps_utc.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); + it('should load jpg with timestamps and gps (UTC) and calculate offset +1, but GPS is off by 1 min', async () => { + const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min.jpg')); + const expected = require(path.join(__dirname, '/../../../assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min.json')); + expect(Utils.clone(data)).to.be.deep.equal(expected); + }); + it('should load jpg with timestamps and gps (UTC) and calculate offset +1, but GPS is off by 1 min - no XMP GPS', async () => { + const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min_no_xmpgps.jpg')); + const expected = require(path.join(__dirname, '/../../../assets/timestamps/big_ben_no_tsoffset_but_gps_utc_off_by_1min_no_xmpgps.json')); + expect(Utils.clone(data)).to.be.deep.equal(expected); + }); it('should load jpg with timestamps but no offset and no GPS to calculate it from', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/timestamps/big_ben_only_time.jpg')); const expected = require(path.join(__dirname, '/../../../assets/timestamps/big_ben_only_time.json')); @@ -213,6 +235,16 @@ describe('MetadataLoader', () => { it(item, async () => { const data = await MetadataLoader.loadPhotoMetadata(fullFilePath); const expected = require(fullFilePath.split('.').slice(0, -1).join('.') + '.json'); + + if (expected.creationDate == "fileModificationTime") { + await getFileModificationTime(fullFilePath).then((modificationTime: any) => { + if (modificationTime) { + expected.creationDate = new Date(modificationTime).getTime(); + } else { + expected.creationDate = 0; + } + }) + } if (expected.skip) { expected.skip.forEach((s: string) => { delete (data as any)[s];