diff --git a/.gitignore b/.gitignore
index 6efad7e3..2e0fc4c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ test/e2e/**/*.js
test/e2e/**/*.js.map
test/*.js
test/*.js.map
+test/tmp/*
benchmark/**/*.js
benchmark/**/*.js.map
gulpfile.js
diff --git a/src/backend/model/fileaccess/MetadataLoader.ts b/src/backend/model/fileaccess/MetadataLoader.ts
index e0fcb50f..58b027c8 100644
--- a/src/backend/model/fileaccess/MetadataLoader.ts
+++ b/src/backend/model/fileaccess/MetadataLoader.ts
@@ -1,7 +1,6 @@
import * as fs from 'fs';
import { imageSize } from 'image-size';
import { Config } from '../../../common/config/private/Config';
-import { SideCar } from '../../../common/entities/MediaDTO';
import { FaceRegion, PhotoMetadata } from '../../../common/entities/PhotoDTO';
import { VideoMetadata } from '../../../common/entities/VideoDTO';
import { Logger } from '../../Logger';
@@ -142,57 +141,9 @@ export class MetadataLoader {
for (const sidecarPath of sidecarPaths) {
if (fs.existsSync(sidecarPath)) {
- const sidecarData = await exifr.sidecar(sidecarPath);
+ const sidecarData: any = await exifr.sidecar(sidecarPath);
if (sidecarData !== undefined) {
- if ((sidecarData as SideCar).dc !== undefined) {
- if ((sidecarData as SideCar).dc.subject !== undefined) {
- if (metadata.keywords === undefined) {
- metadata.keywords = [];
- }
- let keywords = (sidecarData as SideCar).dc.subject || [];
- if (typeof keywords === 'string') {
- keywords = [keywords];
- }
- for (const kw of keywords) {
- if (metadata.keywords.indexOf(kw) === -1) {
- metadata.keywords.push(kw);
- }
- }
- }
- }
- let hasPhotoshopDate = false;
- if ((sidecarData as SideCar).photoshop !== undefined) {
- if ((sidecarData as SideCar).photoshop.DateCreated !== undefined) {
- const date = Utils.timestampToMS((sidecarData as SideCar).photoshop.DateCreated, null);
- if (date) {
- metadata.creationDate = date;
- hasPhotoshopDate = true;
- }
- }
- }
- if (Object.hasOwn(sidecarData, 'xap')) {
- (sidecarData as any)['xmp'] = (sidecarData as any)['xap'];
- delete (sidecarData as any)['xap'];
- }
- if ((sidecarData as SideCar).xmp !== undefined) {
- if (
- (sidecarData as SideCar).xmp.Rating !== undefined &&
- (sidecarData as SideCar).xmp.Rating > 0
- ) {
- metadata.rating = (sidecarData as SideCar).xmp.Rating;
- }
- if (
- !hasPhotoshopDate && (
- (sidecarData as SideCar).xmp.CreateDate !== undefined ||
- (sidecarData as SideCar).xmp.ModifyDate !== undefined
- )
- ) {
- metadata.creationDate =
- Utils.timestampToMS((sidecarData as SideCar).xmp.CreateDate, null) ||
- Utils.timestampToMS((sidecarData as SideCar).xmp.ModifyDate, null) ||
- metadata.creationDate;
- }
- }
+ MetadataLoader.mapMetadata(metadata, sidecarData);
}
}
}
@@ -205,6 +156,7 @@ export class MetadataLoader {
Logger.silly(LOG_TAG, 'Error loading metadata for : ' + fullPath);
Logger.silly(err);
}
+
return metadata;
}
@@ -235,14 +187,6 @@ export class MetadataLoader {
translateValues: false, //don't translate orientation from numbers to strings etc.
mergeOutput: false //don't merge output, because things like Microsoft Rating (percent) and xmp.rating will be merged
};
-
- //Function to convert html code for special characters into their corresponding character (used in exif.photoshop-section)
- const unescape = (tag: string) => {
- return tag.replace(/([0-9]{1,3});/gi, function (match, numStr) {
- return String.fromCharCode(parseInt(numStr, 10));
- });
- }
-
try {
let bufferSize = Config.Media.photoMetadataSize;
try {
@@ -254,6 +198,15 @@ export class MetadataLoader {
} catch (err) {
// ignoring errors
}
+ try {
+ //read the actual image size, don't rely on tags for this
+ const info = imageSize(fullPath);
+ metadata.size = { width: info.width, height: info.height };
+ } catch (e) {
+ //in case of failure, set dimensions to 0 so they may be read via tags
+ metadata.size = { width: 0, height: 0 };
+ }
+
const data = Buffer.allocUnsafe(bufferSize);
fileHandle = await fs.promises.open(fullPath, 'r');
@@ -267,14 +220,6 @@ export class MetadataLoader {
await fileHandle.close();
}
try {
- try {
- //read the actual image size, don't rely on tags for this
- const info = imageSize(fullPath);
- metadata.size = { width: info.width, height: info.height };
- } catch (e) {
- //in case of failure, set dimensions to 0 so they may be read via tags
- metadata.size = { width: 0, height: 0 };
- }
try { //Parse iptc data using the IptcParser, which works correctly for both UTF-8 and ASCII
@@ -316,298 +261,12 @@ export class MetadataLoader {
}
try {
- let orientation = 1; //Orientation 1 is normal
const exif = await exifr.parse(data, exifrOptions);
- //exif is structured in sections, we read the data by section
-
- //dc-section (subject is the only tag we want from dc)
- if (exif.dc &&
- exif.dc.subject &&
- exif.dc.subject.length > 0) {
- const subj = Array.isArray(exif.dc.subject) ? exif.dc.subject : [exif.dc.subject];
- if (metadata.keywords === undefined) {
- metadata.keywords = [];
- }
- for (const kw of subj) {
- if (metadata.keywords.indexOf(kw) === -1) {
- metadata.keywords.push(kw);
- }
- }
- }
-
- //ifd0 section
- if (exif.ifd0) {
- if (exif.ifd0.ImageWidth && metadata.size.width <= 0) {
- metadata.size.width = exif.ifd0.ImageWidth;
- }
- if (exif.ifd0.ImageHeight && metadata.size.height <= 0) {
- metadata.size.height = exif.ifd0.ImageHeight;
- }
- if (exif.ifd0.Orientation) {
- orientation = parseInt(
- exif.ifd0.Orientation as any,
- 10
- ) as number;
- }
- if (exif.ifd0.Make && exif.ifd0.Make !== '') {
- metadata.cameraData = metadata.cameraData || {};
- metadata.cameraData.make = '' + exif.ifd0.Make;
- }
- if (exif.ifd0.Model && exif.ifd0.Model !== '') {
- metadata.cameraData = metadata.cameraData || {};
- metadata.cameraData.model = '' + exif.ifd0.Model;
- }
- //if (exif.ifd0.ModifyDate) {} //Deferred to the exif-section where the other timestamps are
- }
-
- //exif section starting with the date sectino
- if (exif.exif) {
- //Preceedence of dates: exif.DateTimeOriginal, exif.CreateDate, ifd0.ModifyDate, ihdr["Creation Time"], xmp.MetadataDate, file system date
- //Filesystem is the absolute last resort, and it's hard to write tests for, since file system dates are changed on e.g. git clone.
- if (exif.exif.DateTimeOriginal) {
- //DateTimeOriginal is when the camera shutter closed
- let offset = exif.exif.OffsetTimeOriginal; //OffsetTimeOriginal is the corresponding offset
- 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.creationDateOffset = offset;
- } 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)
- let 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.creationDateOffset = offset;
- } else if (exif.ifd0?.ModifyDate) { //using else if here, because DateTimeOriginal and CreatDate have preceedence
- let 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.creationDateOffset = offset
- } 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);
- metadata.creationDateOffset = 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.creationDateOffset = any_offset;
- }
- if (exif.exif.LensModel && exif.exif.LensModel !== '') {
- metadata.cameraData = metadata.cameraData || {};
- metadata.cameraData.lens = '' + exif.exif.LensModel;
- }
- if (Utils.isUInt32(exif.exif.ISO)) {
- metadata.cameraData = metadata.cameraData || {};
- metadata.cameraData.ISO = parseInt('' + exif.exif.ISO, 10);
- }
- if (Utils.isFloat32(exif.exif.FocalLength)) {
- metadata.cameraData = metadata.cameraData || {};
- metadata.cameraData.focalLength = parseFloat(
- '' + exif.exif.FocalLength
- );
- }
- if (Utils.isFloat32(exif.exif.ExposureTime)) {
- metadata.cameraData = metadata.cameraData || {};
- metadata.cameraData.exposure = parseFloat(
- parseFloat('' + exif.exif.ExposureTime).toFixed(6)
- );
- }
- if (Utils.isFloat32(exif.exif.FNumber)) {
- metadata.cameraData = metadata.cameraData || {};
- metadata.cameraData.fStop = parseFloat(
- parseFloat('' + exif.exif.FNumber).toFixed(2)
- );
- }
- if (exif.exif.ExifImageWidth && metadata.size.width <= 0) {
- metadata.size.width = exif.exif.ExifImageWidth;
- }
- if (exif.exif.ExifImageHeight && metadata.size.height <= 0) {
- metadata.size.height = exif.exif.ExifImageHeight;
- }
- }
-
- //gps section
- if (exif.gps) {
- metadata.positionData = metadata.positionData || {};
- metadata.positionData.GPSData = metadata.positionData.GPSData || {};
-
- if (Utils.isFloat32(exif.gps.longitude)) {
- metadata.positionData.GPSData.longitude = parseFloat(
- exif.gps.longitude.toFixed(6)
- );
- }
- if (Utils.isFloat32(exif.gps.latitude)) {
- metadata.positionData.GPSData.latitude = parseFloat(
- exif.gps.latitude.toFixed(6)
- );
- }
-
- if (metadata.positionData) {
- if (!metadata.positionData.GPSData ||
- Object.keys(metadata.positionData.GPSData).length === 0) {
- metadata.positionData.GPSData = undefined;
- metadata.positionData = undefined;
- }
- }
- }
- //photoshop section (sometimes has City, Country and State)
- if (exif.photoshop) {
- if (!metadata.positionData?.country && exif.photoshop.Country) {
- metadata.positionData = metadata.positionData || {};
- metadata.positionData.country = unescape(exif.photoshop.Country);
- }
- if (!metadata.positionData?.state && exif.photoshop.State) {
- metadata.positionData = metadata.positionData || {};
- metadata.positionData.state = unescape(exif.photoshop.State);
- }
- if (!metadata.positionData?.city && exif.photoshop.City) {
- metadata.positionData = metadata.positionData || {};
- metadata.positionData.city = unescape(exif.photoshop.City);
- }
- }
-
- ///////////////////////////////////////
- metadata.size.height = Math.max(metadata.size.height, 1); //ensure height dimension is positive
- metadata.size.width = Math.max(metadata.size.width, 1); //ensure width dimension is positive
-
- //Before moving on to the XMP section (particularly the regions (mwg-rs))
- //we need to switch width and height for images that are rotated sideways
- if (4 < orientation) { //Orientation is sideways (rotated 90% or 270%)
- // noinspection JSSuspiciousNameCombination
- const height = metadata.size.width;
- // noinspection JSSuspiciousNameCombination
- metadata.size.width = metadata.size.height;
- metadata.size.height = height;
- }
- ///////////////////////////////////////
-
- //xmp section
- if (exif.xmp && exif.xmp.Rating) {
- metadata.rating = exif.xmp.Rating;
- if (metadata.rating < 0) {
- metadata.rating = 0;
- }
- }
- //xmp."mwg-rs" section
- if (Config.Faces.enabled &&
- exif["mwg-rs"] &&
- exif["mwg-rs"].Regions) {
- const faces: FaceRegion[] = [];
- const regionListVal = Array.isArray(exif["mwg-rs"].Regions.RegionList) ? exif["mwg-rs"].Regions.RegionList : [exif["mwg-rs"].Regions.RegionList];
- if (regionListVal) {
- for (const regionRoot of regionListVal) {
- let type;
- let name;
- let box;
- const createFaceBox = (
- w: string,
- h: string,
- x: string,
- y: string
- ) => {
- if (4 < orientation) { //roation is sidewards (90 or 270 degrees)
- [x, y] = [y, x];
- [w, h] = [h, w];
- }
- let swapX = 0;
- let swapY = 0;
- switch (orientation) {
- case 2: //TOP RIGHT (Mirror horizontal):
- case 6: //RIGHT TOP (Rotate 90 CW)
- swapX = 1;
- break;
- case 3: // BOTTOM RIGHT (Rotate 180)
- case 7: // RIGHT BOTTOM (Mirror horizontal and rotate 90 CW)
- swapX = 1;
- swapY = 1;
- break;
- case 4: //BOTTOM_LEFT (Mirror vertical)
- case 8: //LEFT_BOTTOM (Rotate 270 CW)
- swapY = 1;
- break;
- }
- // converting ratio to px
- return {
- width: Math.round(parseFloat(w) * metadata.size.width),
- height: Math.round(parseFloat(h) * metadata.size.height),
- left: Math.round(Math.abs(parseFloat(x) - swapX) * metadata.size.width),
- top: Math.round(Math.abs(parseFloat(y) - swapY) * metadata.size.height),
- };
- };
- /* Adobe Lightroom based face region structure */
- if (
- regionRoot &&
- regionRoot['rdf:Description'] &&
- regionRoot['rdf:Description'] &&
- regionRoot['rdf:Description']['mwg-rs:Area']
- ) {
- const region = regionRoot['rdf:Description'];
- const regionBox = region['mwg-rs:Area'].attributes;
-
- name = region['mwg-rs:Name'];
- type = region['mwg-rs:Type'];
- box = createFaceBox(
- regionBox['stArea:w'],
- regionBox['stArea:h'],
- regionBox['stArea:x'],
- regionBox['stArea:y']
- );
- /* Load exiftool edited face region structure, see github issue #191 */
- } else if (
- regionRoot &&
- regionRoot.Name &&
- regionRoot.Type &&
- regionRoot.Area
- ) {
- const regionBox = regionRoot.Area;
- name = regionRoot.Name;
- type = regionRoot.Type;
- box = createFaceBox(
- regionBox.w,
- regionBox.h,
- regionBox.x,
- regionBox.y
- );
- }
-
- if (type !== 'Face' || !name) {
- continue;
- }
-
- // convert center base box to corner based box
- box.left = Math.round(Math.max(0, box.left - box.width / 2));
- box.top = Math.round(Math.max(0, box.top - box.height / 2));
-
-
- faces.push({ name, box });
- }
- }
- if (faces.length > 0) {
- metadata.faces = faces; // save faces
- if (Config.Faces.keywordsToPersons) {
- // remove faces from keywords
- metadata.faces.forEach((f) => {
- const index = metadata.keywords.indexOf(f.name);
- if (index !== -1) {
- metadata.keywords.splice(index, 1);
- }
- });
- }
- }
- }
+ MetadataLoader.mapMetadata(metadata, exif);
} catch (err) {
// ignoring errors
}
- if (!metadata.creationDate) {
- // creationDate can be negative, when it was created before epoch (1970)
- metadata.creationDate = 0;
- }
-
try {
// search for sidecar and merge metadata
const fullPathWithoutExt = path.join(path.parse(fullPath).dir, path.parse(fullPath).name);
@@ -620,74 +279,9 @@ export class MetadataLoader {
for (const sidecarPath of sidecarPaths) {
if (fs.existsSync(sidecarPath)) {
- const sidecarData = await exifr.sidecar(sidecarPath);
-
+ const sidecarData: any = await exifr.sidecar(sidecarPath, exifrOptions);
if (sidecarData !== undefined) {
- if ((sidecarData as SideCar).dc !== undefined) {
- if ((sidecarData as SideCar).dc.subject !== undefined) {
- if (metadata.keywords === undefined) {
- metadata.keywords = [];
- }
- let keywords = (sidecarData as SideCar).dc.subject || [];
- if (typeof keywords === 'string') {
- keywords = [keywords];
- }
- for (const kw of keywords) {
- if (metadata.keywords.indexOf(kw) === -1) {
- metadata.keywords.push(kw);
- }
- }
- }
- }
- let hasPhotoshopDate = false;
- if ((sidecarData as SideCar).photoshop !== undefined) {
- if ((sidecarData as SideCar).photoshop.DateCreated !== undefined) {
- const date = Utils.timestampToMS((sidecarData as SideCar).photoshop.DateCreated, null);
- if (date) {
- metadata.creationDate = date;
- hasPhotoshopDate = true;
- }
- }
- }
- if (Object.hasOwn(sidecarData, 'xap')) {
- (sidecarData as any)['xmp'] = (sidecarData as any)['xap'];
- delete (sidecarData as any)['xap'];
- }
- if ((sidecarData as SideCar).xmp !== undefined) {
- if (
- (sidecarData as SideCar).xmp.Rating !== undefined &&
- (sidecarData as SideCar).xmp.Rating > 0
- ) {
- metadata.rating = (sidecarData as SideCar).xmp.Rating;
- }
- if (
- !hasPhotoshopDate && (
- (sidecarData as SideCar).xmp.CreateDate !== undefined ||
- (sidecarData as SideCar).xmp.ModifyDate !== undefined
- )
- ) {
- metadata.creationDate =
- Utils.timestampToMS((sidecarData as SideCar).xmp.CreateDate, null) ||
- Utils.timestampToMS((sidecarData as SideCar).xmp.ModifyDate, null) ||
- metadata.creationDate;
- }
- }
- if ((sidecarData as SideCar).exif !== undefined) {
- if (
- (sidecarData as SideCar).exif.GPSLatitude !== undefined &&
- (sidecarData as SideCar).exif.GPSLongitude !== undefined
- ) {
- metadata.positionData = metadata.positionData || {};
- metadata.positionData.GPSData = {};
-
- metadata.positionData.GPSData.longitude = Utils.xmpExifGpsCoordinateToDecimalDegrees(
- (sidecarData as SideCar).exif.GPSLongitude
- );
- metadata.positionData.GPSData.latitude = Utils.xmpExifGpsCoordinateToDecimalDegrees(
- (sidecarData as SideCar).exif.GPSLatitude
- );
- }
- }
+ MetadataLoader.mapMetadata(metadata, sidecarData);
}
}
}
@@ -695,7 +289,10 @@ export class MetadataLoader {
Logger.silly(LOG_TAG, 'Error loading sidecar metadata for : ' + fullPath);
Logger.silly(err);
}
-
+ if (!metadata.creationDate) {
+ // creationDate can be negative, when it was created before epoch (1970)
+ metadata.creationDate = 0;
+ }
} catch (err) {
Logger.error(LOG_TAG, 'Error during reading photo: ' + fullPath);
console.error(err);
@@ -708,4 +305,320 @@ export class MetadataLoader {
}
return metadata;
}
+
+ private static mapMetadata(metadata: PhotoMetadata, exif: any) {
+ //replace adobe xap-section with xmp to reuse parsing
+ if (Object.hasOwn(exif, 'xap')) {
+ exif['xmp'] = exif['xap'];
+ delete exif['xap'];
+ }
+ let orientation = MetadataLoader.getOrientation(exif);
+ MetadataLoader.mapImageDimensions(metadata, exif, orientation);
+ MetadataLoader.mapKeywords(metadata, exif);
+ MetadataLoader.mapTimestampAndOffset(metadata, exif);
+ MetadataLoader.mapCameraData(metadata, exif);
+ MetadataLoader.mapGPS(metadata, exif);
+ MetadataLoader.mapToponyms(metadata, exif);
+ MetadataLoader.mapRating(metadata, exif);
+ if (Config.Faces.enabled) {
+ MetadataLoader.mapFaces(metadata, exif, orientation);
+ }
+
+ }
+ private static getOrientation(exif: any): number {
+ let orientation = 1; //Orientation 1 is normal
+ if (exif.ifd0?.Orientation != undefined) {
+ orientation = parseInt(exif.ifd0.Orientation as any, 10) as number;
+ }
+ return orientation;
+ }
+
+ private static mapImageDimensions(metadata: PhotoMetadata, exif: any, orientation: number) {
+ if (metadata.size.width <= 0) {
+ metadata.size.width = exif.ifd0?.ImageWidth || exif.exif?.ExifImageWidth;
+ }
+ if (metadata.size.height <= 0) {
+ metadata.size.height = exif.ifd0?.ImageHeight || exif.exif?.ExifImageHeight;
+ }
+ metadata.size.height = Math.max(metadata.size.height, 1); //ensure height dimension is positive
+ metadata.size.width = Math.max(metadata.size.width, 1); //ensure width dimension is positive
+
+ //we need to switch width and height for images that are rotated sideways
+ if (4 < orientation) { //Orientation is sideways (rotated 90% or 270%)
+ // noinspection JSSuspiciousNameCombination
+ const height = metadata.size.width;
+ // noinspection JSSuspiciousNameCombination
+ metadata.size.width = metadata.size.height;
+ metadata.size.height = height;
+ }
+ }
+
+ private static mapKeywords(metadata: PhotoMetadata, exif: any) {
+ if (exif.dc &&
+ exif.dc.subject &&
+ exif.dc.subject.length > 0) {
+ const subj = Array.isArray(exif.dc.subject) ? exif.dc.subject : [exif.dc.subject];
+ if (metadata.keywords === undefined) {
+ metadata.keywords = [];
+ }
+ for (const kw of subj) {
+ if (metadata.keywords.indexOf(kw) === -1) {
+ metadata.keywords.push(kw);
+ }
+ }
+ }
+ }
+
+ private static mapTimestampAndOffset(metadata: PhotoMetadata, exif: any) {
+ metadata.creationDate = Utils.timestampToMS(exif?.photoshop?.DateCreated, null) ||
+ Utils.timestampToMS(exif?.xmp?.CreateDate, null) ||
+ Utils.timestampToMS(exif?.xmp?.ModifyDate, null) ||
+ metadata.creationDate;
+
+ metadata.creationDateOffset = Utils.timestampToOffsetString(exif?.photoshop?.DateCreated) ||
+ Utils.timestampToOffsetString(exif?.xmp?.CreateDate) ||
+ metadata.creationDateOffset;
+ if (exif.exif) {
+ let offset = undefined;
+ //Preceedence of dates: exif.DateTimeOriginal, exif.CreateDate, ifd0.ModifyDate, ihdr["Creation Time"], xmp.MetadataDate, file system date
+ //Filesystem is the absolute last resort, and it's hard to write tests for, since file system dates are changed on e.g. git clone.
+ if (exif.exif.DateTimeOriginal) {
+ //DateTimeOriginal is when the camera shutter closed
+ offset = exif.exif.OffsetTimeOriginal; //OffsetTimeOriginal is the corresponding offset
+ 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);
+ } 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);
+ } 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);
+ } 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);
+ offset = any_offset;
+ }
+ metadata.creationDateOffset = offset || metadata.creationDateOffset;
+ }
+ }
+
+ private static mapCameraData(metadata: PhotoMetadata, exif: any) {
+ metadata.cameraData = metadata.cameraData || {};
+ if (exif.ifd0) {
+ if (exif.ifd0.Make && exif.ifd0.Make !== '') {
+ metadata.cameraData.make = '' + exif.ifd0.Make;
+ }
+ if (exif.ifd0.Model && exif.ifd0.Model !== '') {
+ metadata.cameraData.model = '' + exif.ifd0.Model;
+ }
+ }
+ if (exif.exif) {
+ if (exif.exif.LensModel && exif.exif.LensModel !== '') {
+ metadata.cameraData.lens = '' + exif.exif.LensModel;
+ }
+ if (Utils.isUInt32(exif.exif.ISO)) {
+ metadata.cameraData.ISO = parseInt('' + exif.exif.ISO, 10);
+ }
+ if (Utils.isFloat32(exif.exif.FocalLength)) {
+ metadata.cameraData.focalLength = parseFloat(
+ '' + exif.exif.FocalLength
+ );
+ }
+ if (Utils.isFloat32(exif.exif.ExposureTime)) {
+ metadata.cameraData.exposure = parseFloat(
+ parseFloat('' + exif.exif.ExposureTime).toFixed(6)
+ );
+ }
+ if (Utils.isFloat32(exif.exif.FNumber)) {
+ metadata.cameraData.fStop = parseFloat(
+ parseFloat('' + exif.exif.FNumber).toFixed(2)
+ );
+ }
+ }
+ if (Object.keys(metadata.cameraData).length === 0) {
+ delete metadata.cameraData;
+ }
+ }
+
+ private static mapGPS(metadata: PhotoMetadata, exif: any) {
+ try {
+ if (exif.gps || (exif.exif && exif.exif.GPSLatitude && exif.exif.GPSLongitude)) {
+ metadata.positionData = metadata.positionData || {};
+ metadata.positionData.GPSData = metadata.positionData.GPSData || {};
+
+ metadata.positionData.GPSData.longitude = Utils.isFloat32(exif.gps?.longitude) ? exif.gps.longitude : Utils.xmpExifGpsCoordinateToDecimalDegrees(exif.exif.GPSLongitude);
+ metadata.positionData.GPSData.latitude = Utils.isFloat32(exif.gps?.latitude) ? exif.gps.latitude : Utils.xmpExifGpsCoordinateToDecimalDegrees(exif.exif.GPSLatitude);
+
+ metadata.positionData.GPSData.longitude = parseFloat(metadata.positionData.GPSData.longitude.toFixed(6))
+ metadata.positionData.GPSData.latitude = parseFloat(metadata.positionData.GPSData.latitude.toFixed(6))
+ }
+ } catch (err) {
+ Logger.error(LOG_TAG, 'Error during reading of GPS data: ' + err);
+ } finally {
+ if (metadata.positionData?.GPSData &&
+ (Object.keys(metadata.positionData.GPSData).length === 0 ||
+ metadata.positionData.GPSData.longitude === undefined ||
+ metadata.positionData.GPSData.latitude === undefined)) {
+ delete metadata.positionData.GPSData;
+ }
+ if (metadata.positionData) {
+ if (Object.keys(metadata.positionData).length === 0) {
+ delete metadata.positionData;
+ }
+ }
+ }
+ }
+
+ private static mapToponyms(metadata: PhotoMetadata, exif: any) {
+ //Function to convert html code for special characters into their corresponding character (used in exif.photoshop-section)
+ const unescape = (tag: string) => {
+ return tag.replace(/([0-9]{1,3});/gi, function (match, numStr) {
+ return String.fromCharCode(parseInt(numStr, 10));
+ });
+ }
+ //photoshop section sometimes has City, Country and State
+ if (exif.photoshop) {
+ if (!metadata.positionData?.country && exif.photoshop.Country) {
+ metadata.positionData = metadata.positionData || {};
+ metadata.positionData.country = unescape(exif.photoshop.Country);
+ }
+ if (!metadata.positionData?.state && exif.photoshop.State) {
+ metadata.positionData = metadata.positionData || {};
+ metadata.positionData.state = unescape(exif.photoshop.State);
+ }
+ if (!metadata.positionData?.city && exif.photoshop.City) {
+ metadata.positionData = metadata.positionData || {};
+ metadata.positionData.city = unescape(exif.photoshop.City);
+ }
+ }
+ }
+
+ private static mapRating(metadata: PhotoMetadata, exif: any) {
+ if (exif.xmp &&
+ exif.xmp.Rating !== undefined) {
+ metadata.rating = exif.xmp.Rating;
+ }
+ }
+
+ private static mapFaces(metadata: PhotoMetadata, exif: any, orientation: number) {
+ //xmp."mwg-rs" section
+ if (exif["mwg-rs"] &&
+ exif["mwg-rs"].Regions) {
+ const faces: FaceRegion[] = [];
+ const regionListVal = Array.isArray(exif["mwg-rs"].Regions.RegionList) ? exif["mwg-rs"].Regions.RegionList : [exif["mwg-rs"].Regions.RegionList];
+ if (regionListVal) {
+ for (const regionRoot of regionListVal) {
+ let type;
+ let name;
+ let box;
+ const createFaceBox = (
+ w: string,
+ h: string,
+ x: string,
+ y: string
+ ) => {
+ if (4 < orientation) { //roation is sidewards (90 or 270 degrees)
+ [x, y] = [y, x];
+ [w, h] = [h, w];
+ }
+ let swapX = 0;
+ let swapY = 0;
+ switch (orientation) {
+ case 2: //TOP RIGHT (Mirror horizontal):
+ case 6: //RIGHT TOP (Rotate 90 CW)
+ swapX = 1;
+ break;
+ case 3: // BOTTOM RIGHT (Rotate 180)
+ case 7: // RIGHT BOTTOM (Mirror horizontal and rotate 90 CW)
+ swapX = 1;
+ swapY = 1;
+ break;
+ case 4: //BOTTOM_LEFT (Mirror vertical)
+ case 8: //LEFT_BOTTOM (Rotate 270 CW)
+ swapY = 1;
+ break;
+ }
+ // converting ratio to px
+ return {
+ width: Math.round(parseFloat(w) * metadata.size.width),
+ height: Math.round(parseFloat(h) * metadata.size.height),
+ left: Math.round(Math.abs(parseFloat(x) - swapX) * metadata.size.width),
+ top: Math.round(Math.abs(parseFloat(y) - swapY) * metadata.size.height),
+ };
+ };
+ /* Adobe Lightroom based face region structure */
+ if (
+ regionRoot &&
+ regionRoot['rdf:Description'] &&
+ regionRoot['rdf:Description'] &&
+ regionRoot['rdf:Description']['mwg-rs:Area']
+ ) {
+ const region = regionRoot['rdf:Description'];
+ const regionBox = region['mwg-rs:Area'].attributes;
+
+ name = region['mwg-rs:Name'];
+ type = region['mwg-rs:Type'];
+ box = createFaceBox(
+ regionBox['stArea:w'],
+ regionBox['stArea:h'],
+ regionBox['stArea:x'],
+ regionBox['stArea:y']
+ );
+ /* Load exiftool edited face region structure, see github issue #191 */
+ } else if (
+ regionRoot &&
+ regionRoot.Name &&
+ regionRoot.Type &&
+ regionRoot.Area
+ ) {
+ const regionBox = regionRoot.Area;
+ name = regionRoot.Name;
+ type = regionRoot.Type;
+ box = createFaceBox(
+ regionBox.w,
+ regionBox.h,
+ regionBox.x,
+ regionBox.y
+ );
+ }
+
+ if (type !== 'Face' || !name) {
+ continue;
+ }
+
+ // convert center base box to corner based box
+ box.left = Math.round(Math.max(0, box.left - box.width / 2));
+ box.top = Math.round(Math.max(0, box.top - box.height / 2));
+
+
+ faces.push({ name, box });
+ }
+ }
+ if (faces.length > 0) {
+ metadata.faces = faces; // save faces
+ if (Config.Faces.keywordsToPersons) {
+ // remove faces from keywords
+ metadata.faces.forEach((f) => {
+ const index = metadata.keywords.indexOf(f.name);
+ if (index !== -1) {
+ metadata.keywords.splice(index, 1);
+ }
+ });
+ }
+ }
+ }
+ }
}
diff --git a/src/common/Utils.ts b/src/common/Utils.ts
index 710a43f8..44604e75 100644
--- a/src/common/Utils.ts
+++ b/src/common/Utils.ts
@@ -141,6 +141,22 @@ export class Utils {
return Date.parse(formattedTimestamp);
}
+ //function to extract offset string from timestamp string, returns undefined if timestamp does not contain offset
+ static timestampToOffsetString(timestamp: string) {
+ try {
+ const idx = timestamp.indexOf("+");
+ if (idx > 0) {
+ return timestamp.substring(idx, timestamp.length);
+ }
+ if (timestamp.indexOf("Z") > 0) {
+ return '+00:00';
+ }
+ return undefined;
+ } catch (err) {
+ return undefined;
+ }
+ }
+
//function to calculate offset from exif.exif.gpsTimeStamp or exif.gps.GPSDateStamp + exif.gps.GPSTimestamp
static getTimeOffsetByGPSStamp(timestamp: string, gpsTimeStamp: string, gps: any) {
let UTCTimestamp = gpsTimeStamp;
@@ -383,11 +399,14 @@ export class Utils {
}
public static xmpExifGpsCoordinateToDecimalDegrees(text: string): number {
+ if (!text) {
+ return undefined;
+ }
const parts = text.match(/^([0-9]+),([0-9.]+)([EWNS])$/);
const degrees: number = parseInt(parts[1], 10);
const minutes: number = parseFloat(parts[2]);
const sign = (parts[3] === "N" || parts[3] === "E") ? 1 : -1;
- return sign * (degrees + (minutes / 60.0))
+ return (sign * (degrees + (minutes / 60.0)))
}
}
diff --git a/src/common/entities/MediaDTO.ts b/src/common/entities/MediaDTO.ts
index 05acc4ce..92807a39 100644
--- a/src/common/entities/MediaDTO.ts
+++ b/src/common/entities/MediaDTO.ts
@@ -29,34 +29,6 @@ export interface MediaDimension {
height: number;
}
-export interface SideCar {
- exif?: SideCarExif;
- dc?: SideCarDc;
- xmp?: SideCarXmp;
- photoshop?: SideCarPhotoshop;
-}
-
-export interface SideCarExif {
- GPSLatitude?: string;
- GPSLongitude?: string;
-}
-
-export interface SideCarDc {
- subject?: string[];
-}
-
-export interface SideCarXmp {
- Rating?: RatingTypes;
- CreateDate?: string;
- ModifyDate?: string;
-}
-
-export interface SideCarPhotoshop {
- // Corresponds to Exif.Photo.DateTimeOriginal. No corresponding key exists in
- // the xmp namespace!
- DateCreated?: string;
-}
-
export const MediaDTOUtils = {
hasPositionData: (media: MediaDTO): boolean => {
return (
diff --git a/test/backend/assets/Chars.json b/test/backend/assets/Chars.json
index 83bec784..03352e6c 100644
--- a/test/backend/assets/Chars.json
+++ b/test/backend/assets/Chars.json
@@ -17,7 +17,7 @@
},
"keywords": [
],
- "rating": 0,
+ "rating": -1,
"faces": [
{
"box": {
diff --git a/test/backend/assets/Chars_exiftool.json b/test/backend/assets/Chars_exiftool.json
index 4c09dfec..7f57c2f2 100644
--- a/test/backend/assets/Chars_exiftool.json
+++ b/test/backend/assets/Chars_exiftool.json
@@ -16,7 +16,7 @@
},
"keywords": [
],
- "rating": 0,
+ "rating": -1,
"faces": [
{
"box": {
diff --git a/test/backend/assets/sidecar/20240107_110258.json b/test/backend/assets/sidecar/20240107_110258.json
index a466e4f9..0bece513 100644
--- a/test/backend/assets/sidecar/20240107_110258.json
+++ b/test/backend/assets/sidecar/20240107_110258.json
@@ -20,8 +20,8 @@
],
"positionData": {
"GPSData": {
- "latitude": 50.08958748333333,
- "longitude": 14.397409516666666
+ "latitude": 50.089587,
+ "longitude": 14.39741
}
},
"rating": 3
diff --git a/test/backend/assets/sidecar/20240121_102400.json b/test/backend/assets/sidecar/20240121_102400.json
index 551b7d9a..58c49509 100644
--- a/test/backend/assets/sidecar/20240121_102400.json
+++ b/test/backend/assets/sidecar/20240121_102400.json
@@ -21,8 +21,8 @@
],
"positionData": {
"GPSData": {
- "latitude": 50.08958748333333,
- "longitude": 14.397409516666666
+ "latitude": 50.089587,
+ "longitude": 14.39741
}
},
"rating": 3
diff --git a/test/backend/assets/sidecar/20240128_105420.json b/test/backend/assets/sidecar/20240128_105420.json
index 40587ac4..de4bde68 100644
--- a/test/backend/assets/sidecar/20240128_105420.json
+++ b/test/backend/assets/sidecar/20240128_105420.json
@@ -11,5 +11,11 @@
"keywords": [
"Travel"
],
- "rating": 3
-}
+ "rating": 3,
+ "positionData": {
+ "GPSData": {
+ "latitude": 50.089587,
+ "longitude": 14.39741
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/backend/assets/sidecar/20240128_120909.json b/test/backend/assets/sidecar/20240128_120909.json
index c8e2ddd0..22ee4209 100644
--- a/test/backend/assets/sidecar/20240128_120909.json
+++ b/test/backend/assets/sidecar/20240128_120909.json
@@ -11,5 +11,11 @@
"keywords": [
"Travel"
],
- "rating": 3
-}
+ "rating": 3,
+ "positionData": {
+ "GPSData": {
+ "latitude": 50.089587,
+ "longitude": 14.39741
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/backend/assets/sidecar/20240128_185808.json b/test/backend/assets/sidecar/20240128_185808.json
index 9d53fca1..e9defe72 100644
--- a/test/backend/assets/sidecar/20240128_185808.json
+++ b/test/backend/assets/sidecar/20240128_185808.json
@@ -21,8 +21,8 @@
],
"positionData": {
"GPSData": {
- "latitude": 50.08958748333333,
- "longitude": 14.397409516666666
+ "latitude": 50.089587,
+ "longitude": 14.39741
}
},
"rating": 3
diff --git a/test/backend/assets/sidecar/Chars.jpg b/test/backend/assets/sidecar/Chars.jpg
new file mode 100644
index 00000000..2bf92826
Binary files /dev/null and b/test/backend/assets/sidecar/Chars.jpg differ
diff --git a/test/backend/assets/sidecar/Chars.jpg.xmp b/test/backend/assets/sidecar/Chars.jpg.xmp
new file mode 100644
index 00000000..c7e83b2a
--- /dev/null
+++ b/test/backend/assets/sidecar/Chars.jpg.xmp
@@ -0,0 +1,172 @@
+
+
+
+
+
+ SWE
+ Jönköping
+
+
+
+
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ
+ abcdefghijklmnopqrstuvwxyz
+ abcdefghijklmnopqrstuvwxyz
+
+
+
+
+
+
+
+
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ
+ 0.294791666666667, 0.13287037037037, 0.107291666666667, 0.237962962962963
+
+
+ abcdefghijklmnopqrstuvwxyz
+ 0.451041666666667, 0.13287037037037, 0.110416666666667, 0.24537037037037
+
+
+ abcdefghijklmnopqrstuvwxyz
+ 0.605208333333333, 0.138425925925926, 0.110416666666667, 0.24537037037037
+
+
+
+
+
+
+
+
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ
+ abcdefghijklmnopqrstuvwxyz
+
+
+
+
+
+ 65535
+
+
+ 1
+ 2
+ 3
+ 0
+
+
+ 2024-01-31T00:02:07+01:00
+ 0232
+ 0100
+ 102/1
+ 0
+ 57,46.8417528N
+ 14,9.7753146E
+ WGS-84
+ 1899-12-30T01:00:14Z
+ 2.2.0.0
+
+
+
+
+
+ 1080
+ pixel
+ 1920
+
+
+
+
+
+ 0.237962962962963
+ normalized
+ 0.107291666666667
+ 0.3484375
+ 0.251851851851852
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ
+ Face
+
+
+
+ 0.24537037037037
+ normalized
+ 0.110416666666667
+ 0.50625
+ 0.255555555555556
+
+ abcdefghijklmnopqrstuvwxyz
+ Face
+
+
+
+ 0.24537037037037
+ normalized
+ 0.110416666666667
+ 0.660416666666667
+ 0.261111111111111
+
+ abcdefghijklmnopqrstuvwxyz
+ Face
+
+
+
+
+
+
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ, abcdefghijklmnopqrstuvwxyz
+
+
+
+ Jönköping
+ Sverige
+ Jönköping
+
+
+
+
+
+ 8
+
+
+ 1080
+ 1920
+ 2
+ 300/1
+ 1
+
+
+ 1
+ 1
+
+
+ 300/1
+
+
+
+ 2024-01-31T00:02:07+01:00
+ Tag That Photo
+ 2024-01-30T23:02:06Z
+ 2024-01-31T00:02:07+01:00
+ -1
+
+
+
+
\ No newline at end of file
diff --git a/test/backend/assets/sidecar/Chars.json b/test/backend/assets/sidecar/Chars.json
new file mode 100644
index 00000000..b30a3ded
--- /dev/null
+++ b/test/backend/assets/sidecar/Chars.json
@@ -0,0 +1,50 @@
+{
+ "size": {
+ "width": 1920,
+ "height": 1080
+ },
+ "creationDate": 1706655727000,
+ "creationDateOffset": "+01:00",
+ "fileSize": 101948,
+ "positionData": {
+ "GPSData": {
+ "longitude": 14.162922,
+ "latitude": 57.780696
+ },
+ "country": "Sverige",
+ "state": "Jönköping",
+ "city": "Jönköping"
+ },
+ "keywords": [
+ ],
+ "rating": -1,
+ "faces": [
+ {
+ "box": {
+ "width": 206,
+ "height": 257,
+ "left": 566,
+ "top": 144
+ },
+ "name": "æÆøØåÅéÉüÜäÄöÖïÏñÑ"
+ },
+ {
+ "name": "abcdefghijklmnopqrstuvwxyz",
+ "box": {
+ "width": 212,
+ "height": 265,
+ "left": 866,
+ "top": 144
+ }
+ },
+ {
+ "name": "abcdefghijklmnopqrstuvwxyz",
+ "box": {
+ "width": 212,
+ "height": 265,
+ "left": 1162,
+ "top": 150
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/backend/assets/sidecar/Chars_exiftool.jpg b/test/backend/assets/sidecar/Chars_exiftool.jpg
new file mode 100644
index 00000000..2bf92826
Binary files /dev/null and b/test/backend/assets/sidecar/Chars_exiftool.jpg differ
diff --git a/test/backend/assets/sidecar/Chars_exiftool.jpg.xmp b/test/backend/assets/sidecar/Chars_exiftool.jpg.xmp
new file mode 100644
index 00000000..efdfb0b9
--- /dev/null
+++ b/test/backend/assets/sidecar/Chars_exiftool.jpg.xmp
@@ -0,0 +1,173 @@
+
+
+
+
+
+ SWE
+ Jönköping
+
+
+
+
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ
+ abcdefghijklmnopqrstuvwxyz
+ abcdefghijklmnopqrstuvwxyz
+
+
+
+
+
+
+
+
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ
+ 0.294791666666667, 0.13287037037037, 0.107291666666667, 0.237962962962963
+
+
+ abcdefghijklmnopqrstuvwxyz
+ 0.451041666666667, 0.13287037037037, 0.110416666666667, 0.24537037037037
+
+
+ abcdefghijklmnopqrstuvwxyz
+ 0.605208333333333, 0.138425925925926, 0.110416666666667, 0.24537037037037
+
+
+
+
+
+
+
+
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ
+ abcdefghijklmnopqrstuvwxyz
+
+
+
+
+
+ 65535
+
+
+ 1
+ 2
+ 3
+ 0
+
+
+ 2024-01-30T12:00:00+01:00
+ 0232
+ 0100
+ 102/1
+ 0
+ 57,46.8417528N
+ 14,9.7753146E
+ WGS-84
+ 1899-12-30T01:00:14Z
+ 2.2.0.0
+
+
+
+
+
+ 1080
+ pixel
+ 1920
+
+
+
+
+
+ 0.237962962962963
+ normalized
+ 0.107291666666667
+ 0.3484375
+ 0.251851851851852
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ
+ Face
+
+
+
+ 0.24537037037037
+ normalized
+ 0.110416666666667
+ 0.50625
+ 0.255555555555556
+
+ abcdefghijklmnopqrstuvwxyz
+ Face
+
+
+
+ 0.24537037037037
+ normalized
+ 0.110416666666667
+ 0.660416666666667
+ 0.261111111111111
+
+ abcdefghijklmnopqrstuvwxyz
+ Face
+
+
+
+
+
+
+
+ æÆøØåÅéÉüÜäÄöÖïÏñÑ, abcdefghijklmnopqrstuvwxyz
+
+
+
+ Jönköping
+ Sverige
+ Jönköping
+
+
+
+
+
+ 8
+
+
+ 1080
+ 1920
+ 1
+ 2
+ 300/1
+ 1
+
+
+ 1
+ 1
+
+
+ 300/1
+
+
+
+ 2024-01-30T12:00:00+01:00
+ Tag That Photo
+ 2024-01-30T23:02:06Z
+ 2024-01-30T12:00:00+01:00
+ -1
+
+
+
+
\ No newline at end of file
diff --git a/test/backend/assets/sidecar/Chars_exiftool.json b/test/backend/assets/sidecar/Chars_exiftool.json
new file mode 100644
index 00000000..febff88f
--- /dev/null
+++ b/test/backend/assets/sidecar/Chars_exiftool.json
@@ -0,0 +1,50 @@
+{
+ "size": {
+ "width": 1920,
+ "height": 1080
+ },
+ "creationDate": 1706612400000,
+ "creationDateOffset": "+01:00",
+ "fileSize": 101948,
+ "positionData": {
+ "GPSData": {
+ "longitude": 14.162922,
+ "latitude": 57.780696
+ },
+ "country": "Sverige",
+ "state": "Jönköping",
+ "city": "Jönköping"
+ },
+ "keywords": [
+ ],
+ "rating": -1,
+ "faces": [
+ {
+ "box": {
+ "width": 206,
+ "height": 257,
+ "left": 566,
+ "top": 144
+ },
+ "name": "æÆøØåÅéÉüÜäÄöÖïÏñÑ"
+ },
+ {
+ "name": "abcdefghijklmnopqrstuvwxyz",
+ "box": {
+ "width": 212,
+ "height": 265,
+ "left": 866,
+ "top": 144
+ }
+ },
+ {
+ "name": "abcdefghijklmnopqrstuvwxyz",
+ "box": {
+ "width": 212,
+ "height": 265,
+ "left": 1162,
+ "top": 150
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/test/backend/assets/sidecar/bunny_1sec.json b/test/backend/assets/sidecar/bunny_1sec.json
index eec9a09a..d7cd2e46 100644
--- a/test/backend/assets/sidecar/bunny_1sec.json
+++ b/test/backend/assets/sidecar/bunny_1sec.json
@@ -6,6 +6,7 @@
"bitRate": 1794127,
"duration": 290,
"creationDate": 1542482851000,
+ "creationDateOffset": "+01:00",
"fileSize": 65073,
"fps": 40000,
"keywords": [
diff --git a/test/backend/assets/sidecar/bunny_1sec_v2.json b/test/backend/assets/sidecar/bunny_1sec_v2.json
index eec9a09a..d7cd2e46 100644
--- a/test/backend/assets/sidecar/bunny_1sec_v2.json
+++ b/test/backend/assets/sidecar/bunny_1sec_v2.json
@@ -6,6 +6,7 @@
"bitRate": 1794127,
"duration": 290,
"creationDate": 1542482851000,
+ "creationDateOffset": "+01:00",
"fileSize": 65073,
"fps": 40000,
"keywords": [
diff --git a/test/backend/assets/sidecar/bunny_1sec_v3.json b/test/backend/assets/sidecar/bunny_1sec_v3.json
index d3abbdba..825f71e7 100644
--- a/test/backend/assets/sidecar/bunny_1sec_v3.json
+++ b/test/backend/assets/sidecar/bunny_1sec_v3.json
@@ -6,6 +6,7 @@
"bitRate": 1794127,
"duration": 290,
"creationDate": 1542482851000,
+ "creationDateOffset": "+01:00",
"fileSize": 65073,
"fps": 40000,
"keywords": [
diff --git a/test/backend/assets/sidecar/metadata.json b/test/backend/assets/sidecar/metadata.json
index 3593766b..1f7eab71 100644
--- a/test/backend/assets/sidecar/metadata.json
+++ b/test/backend/assets/sidecar/metadata.json
@@ -3,6 +3,7 @@
"width": 10,
"height": 5
},
+ "rating": 0,
"creationDate": 1710188754000,
"fileSize": 5095,
"keywords": [
diff --git a/test/backend/assets/sidecar/metadata_v2.json b/test/backend/assets/sidecar/metadata_v2.json
index 36a37353..ceda26fb 100644
--- a/test/backend/assets/sidecar/metadata_v2.json
+++ b/test/backend/assets/sidecar/metadata_v2.json
@@ -5,6 +5,7 @@
},
"creationDate": 1710188754000,
"fileSize": 5095,
+ "rating": 0,
"keywords": [
"floor",
"book",
diff --git a/test/backend/assets/sidecar/no_metadata.json b/test/backend/assets/sidecar/no_metadata.json
index 2ce48ad6..6574ba17 100644
--- a/test/backend/assets/sidecar/no_metadata.json
+++ b/test/backend/assets/sidecar/no_metadata.json
@@ -4,6 +4,7 @@
"height": 5
},
"creationDate": 1542482851000,
+ "creationDateOffset": "+01:00",
"fileSize": 1430,
"keywords": [
"first",
diff --git a/test/backend/assets/sidecar/no_metadata_v2.json b/test/backend/assets/sidecar/no_metadata_v2.json
index 2ce48ad6..6574ba17 100644
--- a/test/backend/assets/sidecar/no_metadata_v2.json
+++ b/test/backend/assets/sidecar/no_metadata_v2.json
@@ -4,6 +4,7 @@
"height": 5
},
"creationDate": 1542482851000,
+ "creationDateOffset": "+01:00",
"fileSize": 1430,
"keywords": [
"first",
diff --git a/test/backend/assets/sidecar/no_metadata_v3.json b/test/backend/assets/sidecar/no_metadata_v3.json
index 7f904f44..2c27c2a6 100644
--- a/test/backend/assets/sidecar/no_metadata_v3.json
+++ b/test/backend/assets/sidecar/no_metadata_v3.json
@@ -4,6 +4,7 @@
"height": 5
},
"creationDate": 1542482851000,
+ "creationDateOffset": "+01:00",
"fileSize": 1430,
"keywords": [
"first"
diff --git a/test/tmp/sqlite.db-journal b/test/tmp/sqlite.db-journal
new file mode 100644
index 00000000..41fef9ee
Binary files /dev/null and b/test/tmp/sqlite.db-journal differ