diff --git a/src/backend/model/fileaccess/MetadataLoader.ts b/src/backend/model/fileaccess/MetadataLoader.ts index 5a61af21..8feb8427 100644 --- a/src/backend/model/fileaccess/MetadataLoader.ts +++ b/src/backend/model/fileaccess/MetadataLoader.ts @@ -139,13 +139,13 @@ export class MetadataLoader { fullPathWithoutExt + '.xmp', fullPathWithoutExt + '.XMP', ]; - for (const sidecarPath of sidecarPaths) { if (fs.existsSync(sidecarPath)) { const sidecarData: any = await exifr.sidecar(sidecarPath); if (sidecarData !== undefined) { MetadataLoader.mapMetadata(metadata, sidecarData); } + break; //Break the loop as soon as a sidecar is found, no need to check the extra sidecar paths } } } catch (err) { @@ -316,6 +316,8 @@ export class MetadataLoader { const orientation = MetadataLoader.getOrientation(exif); MetadataLoader.mapImageDimensions(metadata, exif, orientation); MetadataLoader.mapKeywords(metadata, exif); + MetadataLoader.mapTitle(metadata, exif); + MetadataLoader.mapCaption(metadata, exif); MetadataLoader.mapTimestampAndOffset(metadata, exif); MetadataLoader.mapCameraData(metadata, exif); MetadataLoader.mapGPS(metadata, exif); @@ -370,6 +372,15 @@ export class MetadataLoader { } } + private static mapTitle(metadata: PhotoMetadata, exif: any) { + metadata.title = exif.dc?.title?.value || metadata.title || exif.photoshop?.Headline || exif.acdsee?.caption; //acdsee caption holds the title when data is saved by digikam. Used as last resort if iptc and dc do not contain the data + } + + private static mapCaption(metadata: PhotoMetadata, exif: any) { + metadata.caption = exif.dc?.description?.value || metadata.caption || exif.ifd0?.ImageDescription || exif.exif?.UserComment?.value || exif.Iptc4xmpCore?.ExtDescrAccessibility?.value ||exif.acdsee?.notes; + } + + private static mapTimestampAndOffset(metadata: PhotoMetadata, exif: any) { metadata.creationDate = Utils.timestampToMS(exif?.photoshop?.DateCreated, null) || Utils.timestampToMS(exif?.xmp?.CreateDate, null) || diff --git a/src/common/Utils.ts b/src/common/Utils.ts index db21bb56..a9a7a862 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -132,7 +132,7 @@ export class Utils { //replace : with - in the yyyy-mm-dd part of the timestamp. let formattedTimestamp = timestamp.substring(0,9).replaceAll(':', '-') + timestamp.substring(9,timestamp.length); if (formattedTimestamp.indexOf("Z") > 0) { //replace Z (and what comes after the Z) with offset - formattedTimestamp.substring(0, formattedTimestamp.indexOf("Z")) + (offset ? offset : '+00:00'); + formattedTimestamp = formattedTimestamp.substring(0, formattedTimestamp.indexOf("Z")) + (offset ? offset : '+00:00'); } else if (formattedTimestamp.indexOf("+") > 0) { //don't do anything } else { //add offset formattedTimestamp = formattedTimestamp + (offset ? offset : '+00:00'); diff --git a/test/backend/assets/description.json b/test/backend/assets/description.json new file mode 100644 index 00000000..677f3940 --- /dev/null +++ b/test/backend/assets/description.json @@ -0,0 +1,35 @@ +{ + "size": { + "width": 3144, + "height": 2288 + }, + "creationDate": -2195790002000, + "fileSize": 8909621, + "keywords": [], + "faces": [ + { + "name": "Hulda Stein", + "box": { + "width": 645, + "height": 645, + "left": 1919, + "top": 547 + } + }, + { + "name": "John Amschler", + "box": { + "width": 709, + "height": 709, + "left": 544, + "top": 394 + } + } + ], + "cameraData": { + "make": "NIKON CORPORATION", + "model": "NIKON D200" + }, + "caption": "Hulda and John's Wedding", + "title": "The Wedding" +} \ No newline at end of file diff --git a/test/backend/assets/description.png b/test/backend/assets/description.png new file mode 100644 index 00000000..6f906d67 Binary files /dev/null and b/test/backend/assets/description.png differ diff --git a/test/backend/assets/digikam.jpg b/test/backend/assets/digikam.jpg new file mode 100644 index 00000000..534135e4 Binary files /dev/null and b/test/backend/assets/digikam.jpg differ diff --git a/test/backend/assets/digikam.json b/test/backend/assets/digikam.json new file mode 100644 index 00000000..87a627a1 --- /dev/null +++ b/test/backend/assets/digikam.json @@ -0,0 +1,43 @@ +{ + "cameraData": { + "ISO": 200, + "exposure": 0.008, + "fStop": 2.8, + "focalLength": 9.4, + "make": "FUJIFILM", + "model": "FinePix F601 ZOOM" + }, + "creationDate": 1126423382000, + "creationDateOffset": "+09:00", + "fileSize": 14134, + "size": { + "height": 5, + "width": 7 + }, + "title": "Digikam Title field", + "caption": "Digikam Caption field", + "faces": [ + { + "box": { + "height": 2, + "left": 3, + "top": 2, + "width": 2 + }, + "name": "Æske Øllegård" + } + ], + "keywords": [ + "æÆøØåÅéÉüÜäÄöÖïÏñÑ" + ], + "positionData": { + "GPSData": { + "latitude": 35.9524, + "longitude": 139.863355 + }, + "city": "Shimizu", + "country": "Japan", + "state": "Chiba Ken" + }, + "rating": 4 +} \ No newline at end of file diff --git a/test/backend/assets/sidecar/flatxmp.jpg b/test/backend/assets/sidecar/flatxmp.jpg new file mode 100644 index 00000000..c119808d Binary files /dev/null and b/test/backend/assets/sidecar/flatxmp.jpg differ diff --git a/test/backend/assets/sidecar/flatxmp.json b/test/backend/assets/sidecar/flatxmp.json new file mode 100644 index 00000000..008108db --- /dev/null +++ b/test/backend/assets/sidecar/flatxmp.json @@ -0,0 +1,33 @@ +{ + "size": { + "width": 10, + "height": 5 + }, + "caption": "Description of image", + "creationDate": 328817998000, + "faces": [ + { + "box": { + "height": 1, + "left": 6, + "top": 2, + "width": 2 + }, + "name": "Person1" + }, + { + "box": { + "height": 2, + "left": 2, + "top": 1, + "width": 2 + }, + "name": "Person2" + } + ], + "fileSize": 1430, + "keywords": [ + "Thing3" + ], + "title": "The title" +} \ No newline at end of file diff --git a/test/backend/assets/sidecar/flatxmp.xmp b/test/backend/assets/sidecar/flatxmp.xmp new file mode 100644 index 00000000..74506a10 --- /dev/null +++ b/test/backend/assets/sidecar/flatxmp.xmp @@ -0,0 +1,329 @@ + + + + + + + 0, 0 + 32, 22 + 64, 56 + 128, 128 + 192, 196 + 255, 255 + + + + + + + + + + + + + + + + + + + Digitized By Testmaster + + + + + The title + + + + + © Testmaster 2024 + + + + + Person1 + Person2 + Thing3 + + + + + Description of image + + + + + All Rights Reserved + + + + + User comment + + + + + 400 + + + + + + + alt text + + + + + extended description + + + + + Person1 + Person2 + + + + + Person1 + Person2 + + + + + Person1 + Person2 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/headline.jpg b/test/backend/assets/sidecar/headline.jpg new file mode 100644 index 00000000..c119808d Binary files /dev/null and b/test/backend/assets/sidecar/headline.jpg differ diff --git a/test/backend/assets/sidecar/headline.json b/test/backend/assets/sidecar/headline.json new file mode 100644 index 00000000..7d6cb058 --- /dev/null +++ b/test/backend/assets/sidecar/headline.json @@ -0,0 +1,12 @@ +{ + "size": { + "width": 10, + "height": 5 + }, + "creationDate": 1185683698000, + "creationDateOffset": "+13:45", + "fileSize": 1430, + "keywords": ["Person 1", "Person 2"], + "caption": "XMP description", + "title": "Photoshop Headline" +} \ No newline at end of file diff --git a/test/backend/assets/sidecar/headline.xmp b/test/backend/assets/sidecar/headline.xmp new file mode 100644 index 00000000..db1f80cf --- /dev/null +++ b/test/backend/assets/sidecar/headline.xmp @@ -0,0 +1,210 @@ + + + + + + + + alt text + + + + Testville + Testica + Testroad 2 + 7357 + Testas + test@example.com + + + + extended description + + + + + + 4244 + 28.0-80.0 mm f/2.8 + 28/1 80/1 14/5 14/5 + + + + True + 0 + 0 + +50 + ACR 3.3 + 0 + 0 + 25 + +58 + False + -0.35 + 0 + 0 + 0 + False + True + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 75 + 0 + 0 + 50 + 25 + 0 + family-220.png + 0 + 0 + -24 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 32 + 25 + 0 + 0 + 0 + 0 + 0 + 4150 + +32 + + + 0, 0 + 32, 22 + 64, 56 + 128, 128 + 192, 196 + 255, 255 + + + Medium Contrast + 3.7 + 0 + 0 + Custom + + + + + + XMP Creator + + + + + XMP description + + + image/png + + + XMP Creator + + + + + Person 1 + Person 2 + + + + + + 24361/8200 + 1 + 0 + 0 + 2007-07-29T18:19:58+13:45 + 1/1 + 0221 + 1/1 + 0 + 2 + 1/8 + 14/5 + 3 + + False + False + 0 + False + 0 + + 28/1 + 42 + 1 + + + 400 + + + 0 + 24361/8200 + 3 + 0 + 0 + 1 + 2 + 0 + 3/1 + 0 + 0 + + + + + Archivalist + 1980-06-02T18:19:58 + Photoshop Headline + + + + CamMake + CamModel + 1 + + + + 2007-07-29T18:19:58+13:45 + Adobe Photoshop Lightroom 13.2 Classic (Windows) + 2014-03-23T01:17:49-05:00 + 2014-03-22T19:09:41-05:00 + + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/testimagedesc1.jpg b/test/backend/assets/sidecar/testimagedesc1.jpg new file mode 100644 index 00000000..f0fa299a Binary files /dev/null and b/test/backend/assets/sidecar/testimagedesc1.jpg differ diff --git a/test/backend/assets/sidecar/testimagedesc1.json b/test/backend/assets/sidecar/testimagedesc1.json new file mode 100644 index 00000000..bc0aac3b --- /dev/null +++ b/test/backend/assets/sidecar/testimagedesc1.json @@ -0,0 +1,54 @@ +{ + "cameraData": { + "ISO": 3200, + "exposure": 0.00125, + "fStop": 5.6, + "focalLength": 85, + "lens": "EF-S15-85mm f/3.5-5.6 IS USM", + "make": "Canon", + "model": "óüöúőűáé ÓÜÖÚŐŰÁÉ" + }, + "caption": "Test caption", + "creationDate": 1434018566690, + "faces": [ + { + "box": { + "height": 2, + "width": 2, + "left": 7, + "top": 3 + }, + "name": "squirrel" + }, + { + "box": { + "height": 3, + "width": 2, + "left": 4, + "top": 4 + }, + "name": "special_chars űáéúőóüío?._:" + } + ], + "fileSize": 39424, + "keywords": [ + "Berkley", + "USA", + "űáéúőóüö ŰÁÉÚŐÓÜÖ" + ], + "positionData": { + "GPSData": { + "latitude": 37.871093, + "longitude": -122.25678 + }, + "city": "test city őúéáűóöí-.,)(=", + "country": "test country őúéáűóöí-.,)(=/%!+\"'", + "state": "test state őúéáűóöí-.,)(" + }, + "rating": 3, + "size": { + "height": 10, + "width": 14 + }, + "title": "Test caption" +} diff --git a/test/backend/assets/sidecar/testimagedesc1.xmp b/test/backend/assets/sidecar/testimagedesc1.xmp new file mode 100644 index 00000000..f27c3386 --- /dev/null +++ b/test/backend/assets/sidecar/testimagedesc1.xmp @@ -0,0 +1,86 @@ + + + + + + + Patrik + + + + + Test caption + + + + + + Copyright test + + + + + Berkley + USA + special_chars űáéúőóüío?._: + squirrel + űáéúőóüö ŰÁÉÚŐÓÜÖ + + + + + Test caption + + + + + + + + + + + + + + Berkley + USA + űáéúőóüö ŰÁÉÚŐÓÜÖ + + + + + Berkley + USA + űáéúőóüö ŰÁÉÚŐÓÜÖ + + + + + + + + + + + + + + + + + + + + + + Berkley + USA + special_chars űáéúőóüío?._: + squirrel + űáéúőóüö ŰÁÉÚŐÓÜÖ + + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/testimagedesc2.jpg b/test/backend/assets/sidecar/testimagedesc2.jpg new file mode 100644 index 00000000..f0fa299a Binary files /dev/null and b/test/backend/assets/sidecar/testimagedesc2.jpg differ diff --git a/test/backend/assets/sidecar/testimagedesc2.json b/test/backend/assets/sidecar/testimagedesc2.json new file mode 100644 index 00000000..803e2afd --- /dev/null +++ b/test/backend/assets/sidecar/testimagedesc2.json @@ -0,0 +1,56 @@ +{ + "cameraData": { + "ISO": 3200, + "exposure": 0.00125, + "fStop": 5.6, + "focalLength": 85, + "lens": "EF-S15-85mm f/3.5-5.6 IS USM", + "make": "Canon", + "model": "óüöúőűáé ÓÜÖÚŐŰÁÉ" + }, + "caption": "Test caption", + "creationDate": 1434018566000, + "faces": [ + { + "box": { + "height": 2, + "width": 2, + "left": 7, + "top": 3 + }, + "name": "squirrel" + }, + { + "box": { + "height": 3, + "width": 2, + "left": 4, + "top": 4 + }, + "name": "special_chars űáéúőóüío?._:" + } + ], + "fileSize": 39424, + "keywords": [ + "Berkley", + "USA", + "űáéúőóüö ŰÁÉÚŐÓÜÖ", + "special_chars űáéúőóüío?._:", + "squirrel" + ], + "positionData": { + "GPSData": { + "latitude": 37.871093, + "longitude": -122.25678 + }, + "city": "test city őúéáűóöí-.,)(=", + "country": "test country őúéáűóöí-.,)(=/%!+\"'", + "state": "test state őúéáűóöí-.,)(" + }, + "rating": 3, + "size": { + "height": 10, + "width": 14 + }, + "title": "Test caption" +} diff --git a/test/backend/assets/sidecar/testimagedesc2.xmp b/test/backend/assets/sidecar/testimagedesc2.xmp new file mode 100644 index 00000000..ca499497 --- /dev/null +++ b/test/backend/assets/sidecar/testimagedesc2.xmp @@ -0,0 +1,139 @@ + + + + + + US + Sublocation test + + + + + + Patrik + + + + + Test caption + + + + + Copyright test + + + + + Berkley + USA + special_chars űáéúőóüío?._: + squirrel + űáéúőóüö ŰÁÉÚŐÓÜÖ + + + + + Test caption + + + + + + 40761/8200 + 1 + 0 + 2015-06-11T10:29:26 + 0230 + 0/1 + 0 + 6 + 1/800 + 28/5 + + False + False + 2 + False + 0 + + 85/1 + 2 + 1036800/181 + 691200/119 + 90/1 + 37,52.2656N + 122,15.4068W + 2.2.0.0 + + + 3200 + + + 18325/3649 + 5 + 0 + 19307/2002 + 0 + + + + 123063022888 + EF-S15-85mm f/3.5-5.6 IS USM + 0000129324 + + + 15/1 + 85/1 + 0/0 + 0/0 + + + 3200 + 2 + + + + test city őúéáűóöí-.,)(= + test country őúéáűóöí-.,)(=/%!+"' + 2015-06-11T10:29:26 + test state őúéáűóöí-.,)( + + + + Patrik + 6 + + + Copyright test + + + + + Test caption + + + Canon + óüöúőűáé ÓÜÖÚŐŰÁÉ + 1 + 3 + Adobe Photoshop Lightroom 6.1 (Windows) + 94/1 + 94/1 + + + + 2015-06-11T10:29:26 + Adobe Photoshop Lightroom 6.1 (Windows) + 2015-07-24T22:45:50 + + + + \ No newline at end of file diff --git a/test/backend/unit/model/threading/DiskManagerWorker.spec.ts b/test/backend/unit/model/threading/DiskManagerWorker.spec.ts index a04ee32f..2b163d58 100644 --- a/test/backend/unit/model/threading/DiskManagerWorker.spec.ts +++ b/test/backend/unit/model/threading/DiskManagerWorker.spec.ts @@ -24,7 +24,7 @@ describe('DiskMangerWorker', () => { ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets'); const dir = await DiskManager.scanDirectory('/'); // should match the number of media (photo/video) files in the assets folder - expect(dir.media.length).to.be.equals(15); + expect(dir.media.length).to.be.equals(17); // eslint-disable-next-line @typescript-eslint/no-var-requires const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json')); const i = dir.media.findIndex(m => m.name === 'test image öüóőúéáű-.,.jpg'); diff --git a/test/backend/unit/model/threading/MetaDataLoader.spec.ts b/test/backend/unit/model/threading/MetaDataLoader.spec.ts index 9b62eb58..4e21c25d 100644 --- a/test/backend/unit/model/threading/MetaDataLoader.spec.ts +++ b/test/backend/unit/model/threading/MetaDataLoader.spec.ts @@ -35,6 +35,12 @@ describe('MetadataLoader', () => { expect(Utils.clone(data)).to.be.deep.equal(expected); }); + it('should load png with description', async () => { + const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/description.png')); + const expected = require(path.join(__dirname, '/../../../assets/description.json')); + expect(Utils.clone(data)).to.be.deep.equal(expected); + }); + it('should load jpg', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.jpg')); const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json')); @@ -179,6 +185,11 @@ describe('MetadataLoader', () => { const expected = require(path.join(__dirname, '/../../../assets/wild-1-small.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); + it('should load image with metadata saved by digikam', async () => { + const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/digikam.jpg')); + const expected = require(path.join(__dirname, '/../../../assets/digikam.json')); + expect(Utils.clone(data)).to.be.deep.equal(expected); + }); describe('should load jpg with edge case exif data', () => { const root = path.join(__dirname, '/../../../assets/edge_case_exif_data');