Merge pull request #829 from grasdk/master
consolidate exif parsing libraries - rework of timestamps
1
.gitignore
vendored
@ -40,3 +40,4 @@ test.*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/coverage/
|
/coverage/
|
||||||
.nyc_output/
|
.nyc_output/
|
||||||
|
.vscode*
|
BIN
demo/images/dupl/big_ben_only_time.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
demo/images/dupl/sydney_opera_house.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
demo/images/timestamps/big_ben.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
demo/images/timestamps/big_ben_no_tsoffset_but_gps_utc.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
demo/images/timestamps/big_ben_only_time.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
demo/images/timestamps/newyear_london.jpg
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
demo/images/timestamps/newyear_sydney.jpg
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
demo/images/timestamps/sydney_opera_house.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 22 KiB |
13
package-lock.json
generated
@ -26,7 +26,6 @@
|
|||||||
"nodemailer": "6.9.4",
|
"nodemailer": "6.9.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"sharp": "0.31.3",
|
"sharp": "0.31.3",
|
||||||
"ts-exif-parser": "0.2.2",
|
|
||||||
"ts-node-iptc": "1.0.11",
|
"ts-node-iptc": "1.0.11",
|
||||||
"typeconfig": "2.1.2",
|
"typeconfig": "2.1.2",
|
||||||
"typeorm": "0.3.12",
|
"typeorm": "0.3.12",
|
||||||
@ -20052,12 +20051,6 @@
|
|||||||
"node": ">=6.10"
|
"node": ">=6.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-exif-parser": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"dependencies": {
|
|
||||||
"sax": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ts-helpers": {
|
"node_modules/ts-helpers": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@ -35085,12 +35078,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"ts-exif-parser": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"requires": {
|
|
||||||
"sax": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ts-helpers": {
|
"ts-helpers": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
@ -53,7 +53,6 @@
|
|||||||
"nodemailer": "6.9.4",
|
"nodemailer": "6.9.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"sharp": "0.31.3",
|
"sharp": "0.31.3",
|
||||||
"ts-exif-parser": "0.2.2",
|
|
||||||
"ts-node-iptc": "1.0.11",
|
"ts-node-iptc": "1.0.11",
|
||||||
"typeconfig": "2.1.2",
|
"typeconfig": "2.1.2",
|
||||||
"typeorm": "0.3.12",
|
"typeorm": "0.3.12",
|
||||||
|
@ -364,7 +364,7 @@ export class SearchManager {
|
|||||||
for (const sort of sortings) {
|
for (const sort of sortings) {
|
||||||
switch (sort.method) {
|
switch (sort.method) {
|
||||||
case SortByTypes.Date:
|
case SortByTypes.Date:
|
||||||
query.addOrderBy('media.metadata.creationDate', sort.ascending ? 'ASC' : 'DESC');
|
query.addOrderBy('media.metadata.creationDate', sort.ascending ? 'ASC' : 'DESC'); //If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). If taken into account, it will alter the sort order. Probably should not be done.
|
||||||
break;
|
break;
|
||||||
case SortByTypes.Rating:
|
case SortByTypes.Rating:
|
||||||
query.addOrderBy('media.metadata.rating', sort.ascending ? 'ASC' : 'DESC');
|
query.addOrderBy('media.metadata.rating', sort.ascending ? 'ASC' : 'DESC');
|
||||||
@ -563,7 +563,12 @@ export class SearchManager {
|
|||||||
const textParam: { [key: string]: unknown } = {};
|
const textParam: { [key: string]: unknown } = {};
|
||||||
textParam['from' + queryId] = (query as FromDateSearch).value;
|
textParam['from' + queryId] = (query as FromDateSearch).value;
|
||||||
q.where(
|
q.where(
|
||||||
`media.metadata.creationDate ${relation} :from${queryId}`,
|
`media.metadata.creationDate ${relation} :from${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-).
|
||||||
|
//Example: -600 means in the database UTC-10:00. The time 20:00 in the evening in the UTC-10 timezone, is actually 06:00 the next morning
|
||||||
|
//in UTC+00:00. To make search take that into account, one can subtract the offset from the creationDate to "pretend" the photo is taken
|
||||||
|
//in UTC time. Subtracting -600 minutes (because it's the -10:00 timezone), corresponds to adding 10 hours to the photo's timestamp, thus
|
||||||
|
//bringing it into the next day as if it was taken at UTC+00:00. Similarly subtracting a positive timezone from a timestamp will "pretend"
|
||||||
|
//the photo is taken earlier in time (e.g. subtracting 300 from the UTC+05:00 timezone).
|
||||||
textParam
|
textParam
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -585,7 +590,7 @@ export class SearchManager {
|
|||||||
const textParam: { [key: string]: unknown } = {};
|
const textParam: { [key: string]: unknown } = {};
|
||||||
textParam['to' + queryId] = (query as ToDateSearch).value;
|
textParam['to' + queryId] = (query as ToDateSearch).value;
|
||||||
q.where(
|
q.where(
|
||||||
`media.metadata.creationDate ${relation} :to${queryId}`,
|
`media.metadata.creationDate ${relation} :to${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
textParam
|
textParam
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -790,15 +795,15 @@ export class SearchManager {
|
|||||||
if (tq.negate) {
|
if (tq.negate) {
|
||||||
|
|
||||||
q.where(
|
q.where(
|
||||||
`media.metadata.creationDate >= :to${queryId}`,
|
`media.metadata.creationDate >= :to${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
textParam
|
textParam
|
||||||
).orWhere(`media.metadata.creationDate < :from${queryId}`,
|
).orWhere(`media.metadata.creationDate < :from${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
textParam);
|
textParam);
|
||||||
} else {
|
} else {
|
||||||
q.where(
|
q.where(
|
||||||
`media.metadata.creationDate < :to${queryId}`,
|
`media.metadata.creationDate < :to${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
textParam
|
textParam
|
||||||
).andWhere(`media.metadata.creationDate >= :from${queryId}`,
|
).andWhere(`media.metadata.creationDate >= :from${queryId}`, //TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
textParam);
|
textParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -821,10 +826,12 @@ export class SearchManager {
|
|||||||
if (Config.Database.type === DatabaseType.sqlite) {
|
if (Config.Database.type === DatabaseType.sqlite) {
|
||||||
if (tq.daysLength == 0) {
|
if (tq.daysLength == 0) {
|
||||||
q.where(
|
q.where(
|
||||||
|
//TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationEql} CAST(strftime('${duration}','now') AS INTEGER)`
|
`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationEql} CAST(strftime('${duration}','now') AS INTEGER)`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
q.where(
|
q.where(
|
||||||
|
//TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationTop} CAST(strftime('${duration}','now') AS INTEGER)`
|
`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationTop} CAST(strftime('${duration}','now') AS INTEGER)`
|
||||||
)[whereFN](`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationBottom} CAST(strftime('${duration}','now','-:diff${queryId} day') AS INTEGER)`,
|
)[whereFN](`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationBottom} CAST(strftime('${duration}','now','-:diff${queryId} day') AS INTEGER)`,
|
||||||
textParam);
|
textParam);
|
||||||
@ -832,10 +839,12 @@ export class SearchManager {
|
|||||||
} else {
|
} else {
|
||||||
if (tq.daysLength == 0) {
|
if (tq.daysLength == 0) {
|
||||||
q.where(
|
q.where(
|
||||||
|
//TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationEql} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
|
`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationEql} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
q.where(
|
q.where(
|
||||||
|
//TODO: If media.metadata.creationDateOffset is defined, it is an offset of minutes (+/-). See explanation above.
|
||||||
`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationTop} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
|
`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationTop} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)`
|
||||||
)[whereFN](`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationBottom} CAST(DATE_FORMAT((DATE_ADD(curdate(), INTERVAL -:diff${queryId} DAY)),'${duration}') AS SIGNED)`,
|
)[whereFN](`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationBottom} CAST(DATE_FORMAT((DATE_ADD(curdate(), INTERVAL -:diff${queryId} DAY)),'${duration}') AS SIGNED)`,
|
||||||
textParam);
|
textParam);
|
||||||
|
@ -4,6 +4,7 @@ import {MediaDimension, MediaDTO, MediaMetadata,} from '../../../../common/entit
|
|||||||
import {PersonJunctionTable} from './PersonJunctionTable';
|
import {PersonJunctionTable} from './PersonJunctionTable';
|
||||||
import {columnCharsetCS} from './EntityUtils';
|
import {columnCharsetCS} from './EntityUtils';
|
||||||
import {CameraMetadata, FaceRegion, GPSMetadata, PositionMetaData,} from '../../../../common/entities/PhotoDTO';
|
import {CameraMetadata, FaceRegion, GPSMetadata, PositionMetaData,} from '../../../../common/entities/PhotoDTO';
|
||||||
|
import { Utils } from '../../../../common/Utils';
|
||||||
|
|
||||||
export class MediaDimensionEntity implements MediaDimension {
|
export class MediaDimensionEntity implements MediaDimension {
|
||||||
@Column('int')
|
@Column('int')
|
||||||
@ -106,6 +107,15 @@ export class MediaMetadataEntity implements MediaMetadata {
|
|||||||
@Index()
|
@Index()
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
|
|
||||||
|
@Column('smallint', {
|
||||||
|
transformer: {
|
||||||
|
from: (v) => Utils.getOffsetString(v), //from database repr. as smallint (minutes) to string (+/-HH:MM)
|
||||||
|
to: (v) => Utils.getOffsetMinutes(v), //from entiry repr. as string (+/-HH:MM) to smallint (minutes)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
creationDateOffset?: string;
|
||||||
|
|
||||||
|
|
||||||
@Column('int', {unsigned: true})
|
@Column('int', {unsigned: true})
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import { FfprobeData } from 'fluent-ffmpeg';
|
|||||||
import { FileHandle } from 'fs/promises';
|
import { FileHandle } from 'fs/promises';
|
||||||
import * as util from 'node:util';
|
import * as util from 'node:util';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { ExifParserFactory, OrientationTypes } from 'ts-exif-parser';
|
|
||||||
import { IptcParser } from 'ts-node-iptc';
|
import { IptcParser } from 'ts-node-iptc';
|
||||||
import { Utils } from '../../../common/Utils';
|
import { Utils } from '../../../common/Utils';
|
||||||
import { FFmpegFactory } from '../FFmpegFactory';
|
import { FFmpegFactory } from '../FFmpegFactory';
|
||||||
@ -40,7 +39,7 @@ export class MetadataLoader {
|
|||||||
try {
|
try {
|
||||||
const stat = fs.statSync(fullPath);
|
const stat = fs.statSync(fullPath);
|
||||||
metadata.fileSize = stat.size;
|
metadata.fileSize = stat.size;
|
||||||
metadata.creationDate = stat.mtime.getTime();
|
metadata.creationDate = stat.mtime.getTime(); //Default date is file system time of last modification
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
// ignoring errors
|
// ignoring errors
|
||||||
@ -148,7 +147,8 @@ export class MetadataLoader {
|
|||||||
if (metadata.keywords.indexOf(kw) === -1) {
|
if (metadata.keywords.indexOf(kw) === -1) {
|
||||||
metadata.keywords.push(kw);
|
metadata.keywords.push(kw);
|
||||||
}
|
}
|
||||||
} }
|
}
|
||||||
|
}
|
||||||
if ((sidecarData as SideCar).xmp.Rating !== undefined) {
|
if ((sidecarData as SideCar).xmp.Rating !== undefined) {
|
||||||
metadata.rating = (sidecarData as SideCar).xmp.Rating;
|
metadata.rating = (sidecarData as SideCar).xmp.Rating;
|
||||||
}
|
}
|
||||||
@ -168,7 +168,7 @@ export class MetadataLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static readonly EMPTY_METADATA: PhotoMetadata = {
|
private static readonly EMPTY_METADATA: PhotoMetadata = {
|
||||||
size: {width: 1, height: 1},
|
size: { width: 0, height: 0 },
|
||||||
creationDate: 0,
|
creationDate: 0,
|
||||||
fileSize: 0,
|
fileSize: 0,
|
||||||
};
|
};
|
||||||
@ -177,10 +177,68 @@ export class MetadataLoader {
|
|||||||
public static async loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
|
public static async loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
|
||||||
let fileHandle: FileHandle;
|
let fileHandle: FileHandle;
|
||||||
const metadata: PhotoMetadata = {
|
const metadata: PhotoMetadata = {
|
||||||
size: {width: 1, height: 1},
|
size: { width: 0, height: 0 },
|
||||||
creationDate: 0,
|
creationDate: 0,
|
||||||
fileSize: 0,
|
fileSize: 0,
|
||||||
};
|
};
|
||||||
|
const exifrOptions = {
|
||||||
|
tiff: true,
|
||||||
|
xmp: true,
|
||||||
|
icc: false,
|
||||||
|
jfif: false, //not needed and not supported for png
|
||||||
|
ihdr: true,
|
||||||
|
iptc: false, //exifr reads UTF8-encoded data wrongly, using IptcParser instead
|
||||||
|
exif: true,
|
||||||
|
gps: true,
|
||||||
|
reviveValues: false, //don't convert timestamps
|
||||||
|
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 timestamp into milliseconds taking offset into account
|
||||||
|
const timestampToMS = (timestamp: string, offset: string) => {
|
||||||
|
if (!timestamp) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
//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');
|
||||||
|
} else if (formattedTimestamp.indexOf("+") > 0) { //don't do anything
|
||||||
|
} else { //add offset
|
||||||
|
formattedTimestamp = formattedTimestamp + (offset ? offset : '+00:00');
|
||||||
|
}
|
||||||
|
//parse into MS and return
|
||||||
|
return Date.parse(formattedTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
//function to calculate offset from exif.exif.gpsTimeStamp or exif.gps.GPSDateStamp + exif.gps.GPSTimestamp
|
||||||
|
const getTimeOffsetByGPSStamp = (timestamp: string, gpsTimeStamp: string, gps: any) => {
|
||||||
|
let UTCTimestamp = gpsTimeStamp;
|
||||||
|
if (!UTCTimestamp &&
|
||||||
|
gps &&
|
||||||
|
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(':');
|
||||||
|
}
|
||||||
|
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 = (timestampToMS(timestamp, '+00:00')- timestampToMS(UTCTimestamp, '+00:00')) / 1000 / 60;
|
||||||
|
return Utils.getOffsetString(offsetMinutes);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//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 {
|
try {
|
||||||
const data = Buffer.allocUnsafe(Config.Media.photoMetadataSize);
|
const data = Buffer.allocUnsafe(Config.Media.photoMetadataSize);
|
||||||
fileHandle = await fs.promises.open(fullPath, 'r');
|
fileHandle = await fs.promises.open(fullPath, 'r');
|
||||||
@ -193,7 +251,6 @@ export class MetadataLoader {
|
|||||||
} finally {
|
} finally {
|
||||||
await fileHandle.close();
|
await fileHandle.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
const stat = fs.statSync(fullPath);
|
const stat = fs.statSync(fullPath);
|
||||||
@ -202,118 +259,17 @@ export class MetadataLoader {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignoring errors
|
// ignoring errors
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const exif = ExifParserFactory.create(data).parse();
|
|
||||||
if (
|
|
||||||
exif.tags.ISO ||
|
|
||||||
exif.tags.Model ||
|
|
||||||
exif.tags.Make ||
|
|
||||||
exif.tags.FNumber ||
|
|
||||||
exif.tags.ExposureTime ||
|
|
||||||
exif.tags.FocalLength ||
|
|
||||||
exif.tags.LensModel
|
|
||||||
) {
|
|
||||||
if (exif.tags.Model && exif.tags.Model !== '') {
|
|
||||||
metadata.cameraData = metadata.cameraData || {};
|
|
||||||
metadata.cameraData.model = '' + exif.tags.Model;
|
|
||||||
}
|
|
||||||
if (exif.tags.Make && exif.tags.Make !== '') {
|
|
||||||
metadata.cameraData = metadata.cameraData || {};
|
|
||||||
metadata.cameraData.make = '' + exif.tags.Make;
|
|
||||||
}
|
|
||||||
if (exif.tags.LensModel && exif.tags.LensModel !== '') {
|
|
||||||
metadata.cameraData = metadata.cameraData || {};
|
|
||||||
metadata.cameraData.lens = '' + exif.tags.LensModel;
|
|
||||||
}
|
|
||||||
if (Utils.isUInt32(exif.tags.ISO)) {
|
|
||||||
metadata.cameraData = metadata.cameraData || {};
|
|
||||||
metadata.cameraData.ISO = parseInt('' + exif.tags.ISO, 10);
|
|
||||||
}
|
|
||||||
if (Utils.isFloat32(exif.tags.FocalLength)) {
|
|
||||||
metadata.cameraData = metadata.cameraData || {};
|
|
||||||
metadata.cameraData.focalLength = parseFloat(
|
|
||||||
'' + exif.tags.FocalLength
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (Utils.isFloat32(exif.tags.ExposureTime)) {
|
|
||||||
metadata.cameraData = metadata.cameraData || {};
|
|
||||||
metadata.cameraData.exposure = parseFloat(
|
|
||||||
parseFloat('' + exif.tags.ExposureTime).toFixed(6)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (Utils.isFloat32(exif.tags.FNumber)) {
|
|
||||||
metadata.cameraData = metadata.cameraData || {};
|
|
||||||
metadata.cameraData.fStop = parseFloat(
|
|
||||||
parseFloat('' + exif.tags.FNumber).toFixed(2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!isNaN(exif.tags.GPSLatitude) ||
|
|
||||||
exif.tags.GPSLongitude ||
|
|
||||||
exif.tags.GPSAltitude
|
|
||||||
) {
|
|
||||||
metadata.positionData = metadata.positionData || {};
|
|
||||||
metadata.positionData.GPSData = {};
|
|
||||||
|
|
||||||
if (Utils.isFloat32(exif.tags.GPSLongitude)) {
|
|
||||||
metadata.positionData.GPSData.longitude = parseFloat(
|
|
||||||
exif.tags.GPSLongitude.toFixed(6)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (Utils.isFloat32(exif.tags.GPSLatitude)) {
|
|
||||||
metadata.positionData.GPSData.latitude = parseFloat(
|
|
||||||
exif.tags.GPSLatitude.toFixed(6)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
exif.tags.CreateDate ||
|
|
||||||
exif.tags.DateTimeOriginal ||
|
|
||||||
exif.tags.ModifyDate
|
|
||||||
) {
|
|
||||||
metadata.creationDate =
|
|
||||||
(exif.tags.DateTimeOriginal ||
|
|
||||||
exif.tags.CreateDate ||
|
|
||||||
exif.tags.ModifyDate) * 1000;
|
|
||||||
}
|
|
||||||
if (exif.imageSize) {
|
|
||||||
metadata.size = {
|
|
||||||
width: exif.imageSize.width,
|
|
||||||
height: exif.imageSize.height,
|
|
||||||
};
|
|
||||||
} else if (
|
|
||||||
exif.tags.RelatedImageWidth &&
|
|
||||||
exif.tags.RelatedImageHeight
|
|
||||||
) {
|
|
||||||
metadata.size = {
|
|
||||||
width: exif.tags.RelatedImageWidth,
|
|
||||||
height: exif.tags.RelatedImageHeight,
|
|
||||||
};
|
|
||||||
} else if (
|
|
||||||
exif.tags.ImageWidth &&
|
|
||||||
exif.tags.ImageHeight
|
|
||||||
) {
|
|
||||||
metadata.size = {
|
|
||||||
width: exif.tags.ImageWidth,
|
|
||||||
height: exif.tags.ImageHeight,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const info = imageSize(fullPath);
|
|
||||||
metadata.size = {width: info.width, height: info.height};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Logger.debug(LOG_TAG, 'Error parsing exif', fullPath, err);
|
|
||||||
try {
|
try {
|
||||||
|
//read the actual image size, don't rely on tags for this
|
||||||
const info = imageSize(fullPath);
|
const info = imageSize(fullPath);
|
||||||
metadata.size = { width: info.width, height: info.height };
|
metadata.size = { width: info.width, height: info.height };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
metadata.size = {width: 1, height: 1};
|
//in case of failure, set dimensions to 0 so they may be read via tags
|
||||||
}
|
metadata.size = { width: 0, height: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
|
try { //Parse iptc data using the IptcParser, which works correctly for both UTF-8 and ASCII
|
||||||
const iptcData = IptcParser.parse(data);
|
const iptcData = IptcParser.parse(data);
|
||||||
if (iptcData.country_or_primary_location_name) {
|
if (iptcData.country_or_primary_location_name) {
|
||||||
metadata.positionData = metadata.positionData || {};
|
metadata.positionData = metadata.positionData || {};
|
||||||
@ -351,32 +307,12 @@ export class MetadataLoader {
|
|||||||
// Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err);
|
// Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!metadata.creationDate) {
|
|
||||||
// creationDate can be negative, when it was created before epoch (1970)
|
|
||||||
metadata.creationDate = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exifrOptions = {
|
let orientation = 1; //Orientation 1 is normal
|
||||||
tiff: true,
|
|
||||||
xmp: true,
|
|
||||||
icc: false,
|
|
||||||
jfif: false, //not needed and not supported for png
|
|
||||||
ihdr: true,
|
|
||||||
iptc: false, //exifr reads UTF8-encoded data wrongly
|
|
||||||
exif: true,
|
|
||||||
gps: true,
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
const exif = await exifr.parse(data, exifrOptions);
|
const exif = await exifr.parse(data, exifrOptions);
|
||||||
if (exif.xmp && exif.xmp.Rating) {
|
//exif is structured in sections, we read the data by section
|
||||||
metadata.rating = exif.xmp.Rating;
|
|
||||||
if (metadata.rating < 0) {
|
//dc-section (subject is the only tag we want from dc)
|
||||||
metadata.rating = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (exif.dc &&
|
if (exif.dc &&
|
||||||
exif.dc.subject &&
|
exif.dc.subject &&
|
||||||
exif.dc.subject.length > 0) {
|
exif.dc.subject.length > 0) {
|
||||||
@ -390,22 +326,172 @@ export class MetadataLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let orientation = OrientationTypes.TOP_LEFT;
|
|
||||||
if (exif.ifd0 &&
|
//ifd0 section
|
||||||
exif.ifd0.Orientation) {
|
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(
|
orientation = parseInt(
|
||||||
exif.ifd0.Orientation as any,
|
exif.ifd0.Orientation as any,
|
||||||
10
|
10
|
||||||
) as number;
|
) as number;
|
||||||
}
|
}
|
||||||
if (OrientationTypes.BOTTOM_LEFT < orientation) {
|
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
|
||||||
|
if (exif.exif.OffsetTimeOriginal) { //OffsetTimeOriginal is the corresponding offset
|
||||||
|
metadata.creationDate = timestampToMS(exif.exif.DateTimeOriginal, exif.exif.OffsetTimeOriginal);
|
||||||
|
metadata.creationDateOffset = exif.exif.OffsetTimeOriginal;
|
||||||
|
} else {
|
||||||
|
const alt_offset = exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps);
|
||||||
|
metadata.creationDate = timestampToMS(exif.exif.DateTimeOriginal, alt_offset);
|
||||||
|
metadata.creationDateOffset = alt_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)
|
||||||
|
if (exif.exif.OffsetTimeDigitized) { //OffsetTimeDigitized is the corresponding offset
|
||||||
|
metadata.creationDate = timestampToMS(exif.exif.CreateDate, exif.exif.OffsetTimeDigitized);
|
||||||
|
metadata.creationDateOffset = exif.exif.OffsetTimeDigitized;
|
||||||
|
} else {
|
||||||
|
const alt_offset = exif.exif.OffsetTimeOriginal || exif.exif.OffsetTime || getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps);
|
||||||
|
metadata.creationDate = timestampToMS(exif.exif.DateTimeOriginal, alt_offset);
|
||||||
|
metadata.creationDateOffset = alt_offset;
|
||||||
|
}
|
||||||
|
} else if (exif.ifd0?.ModifyDate) { //using else if here, because DateTimeOriginal and CreatDate have preceedence
|
||||||
|
if (exif.exif.OffsetTime) {
|
||||||
|
//exif.Offsettime is the offset corresponding to ifd0.ModifyDate
|
||||||
|
metadata.creationDate = timestampToMS(exif.ifd0.ModifyDate, exif.exif?.OffsetTime);
|
||||||
|
metadata.creationDateOffset = exif.exif?.OffsetTime
|
||||||
|
} else {
|
||||||
|
const alt_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps);
|
||||||
|
metadata.creationDate = timestampToMS(exif.ifd0.ModifyDate, alt_offset);
|
||||||
|
metadata.creationDateOffset = alt_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 || getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps);
|
||||||
|
metadata.creationDate = 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 || getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps);
|
||||||
|
metadata.creationDate = 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
|
// noinspection JSSuspiciousNameCombination
|
||||||
const height = metadata.size.width;
|
const height = metadata.size.width;
|
||||||
// noinspection JSSuspiciousNameCombination
|
// noinspection JSSuspiciousNameCombination
|
||||||
metadata.size.width = metadata.size.height;
|
metadata.size.width = metadata.size.height;
|
||||||
metadata.size.height = 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 &&
|
if (Config.Faces.enabled &&
|
||||||
exif["mwg-rs"] &&
|
exif["mwg-rs"] &&
|
||||||
exif["mwg-rs"].Regions) {
|
exif["mwg-rs"].Regions) {
|
||||||
@ -422,24 +508,24 @@ export class MetadataLoader {
|
|||||||
x: string,
|
x: string,
|
||||||
y: string
|
y: string
|
||||||
) => {
|
) => {
|
||||||
if (OrientationTypes.BOTTOM_LEFT < orientation) {
|
if (4 < orientation) { //roation is sidewards (90 or 270 degrees)
|
||||||
[x, y] = [y, x];
|
[x, y] = [y, x];
|
||||||
[w, h] = [h, w];
|
[w, h] = [h, w];
|
||||||
}
|
}
|
||||||
let swapX = 0;
|
let swapX = 0;
|
||||||
let swapY = 0;
|
let swapY = 0;
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case OrientationTypes.TOP_RIGHT:
|
case 2: //TOP RIGHT (Mirror horizontal):
|
||||||
case OrientationTypes.RIGHT_TOP:
|
case 6: //RIGHT TOP (Rotate 90 CW)
|
||||||
swapX = 1;
|
swapX = 1;
|
||||||
break;
|
break;
|
||||||
case OrientationTypes.BOTTOM_RIGHT:
|
case 3: // BOTTOM RIGHT (Rotate 180)
|
||||||
case OrientationTypes.RIGHT_BOTTOM:
|
case 7: // RIGHT BOTTOM (Mirror horizontal and rotate 90 CW)
|
||||||
swapX = 1;
|
swapX = 1;
|
||||||
swapY = 1;
|
swapY = 1;
|
||||||
break;
|
break;
|
||||||
case OrientationTypes.BOTTOM_LEFT:
|
case 4: //BOTTOM_LEFT (Mirror vertical)
|
||||||
case OrientationTypes.LEFT_BOTTOM:
|
case 8: //LEFT_BOTTOM (Rotate 270 CW)
|
||||||
swapY = 1;
|
swapY = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -451,7 +537,6 @@ export class MetadataLoader {
|
|||||||
top: Math.round(Math.abs(parseFloat(y) - swapY) * metadata.size.height),
|
top: Math.round(Math.abs(parseFloat(y) - swapY) * metadata.size.height),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Adobe Lightroom based face region structure */
|
/* Adobe Lightroom based face region structure */
|
||||||
if (
|
if (
|
||||||
regionRoot &&
|
regionRoot &&
|
||||||
@ -517,6 +602,11 @@ export class MetadataLoader {
|
|||||||
// ignoring errors
|
// ignoring errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!metadata.creationDate) {
|
||||||
|
// creationDate can be negative, when it was created before epoch (1970)
|
||||||
|
metadata.creationDate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// search for sidecar and merge metadata
|
// search for sidecar and merge metadata
|
||||||
const fullPathWithoutExt = path.parse(fullPath).name;
|
const fullPathWithoutExt = path.parse(fullPath).name;
|
||||||
@ -564,7 +654,5 @@ export class MetadataLoader {
|
|||||||
return MetadataLoader.EMPTY_METADATA;
|
return MetadataLoader.EMPTY_METADATA;
|
||||||
}
|
}
|
||||||
return metadata;
|
return metadata;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import {MediaDTOWithThPath, Messenger} from './Messenger';
|
|||||||
import {backendTexts} from '../../../common/BackendTexts';
|
import {backendTexts} from '../../../common/BackendTexts';
|
||||||
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
|
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
|
||||||
import {DefaultMessengers} from '../../../common/entities/job/JobDTO';
|
import {DefaultMessengers} from '../../../common/entities/job/JobDTO';
|
||||||
|
import {Utils} from '../../../common/Utils';
|
||||||
|
|
||||||
export class EmailMessenger extends Messenger<{
|
export class EmailMessenger extends Messenger<{
|
||||||
emailTo: string,
|
emailTo: string,
|
||||||
@ -69,7 +70,7 @@ export class EmailMessenger extends Messenger<{
|
|||||||
(media[i].metadata as PhotoMetadata).positionData?.country :
|
(media[i].metadata as PhotoMetadata).positionData?.country :
|
||||||
((media[i].metadata as PhotoMetadata).positionData?.city ?
|
((media[i].metadata as PhotoMetadata).positionData?.city ?
|
||||||
(media[i].metadata as PhotoMetadata).positionData?.city : '');
|
(media[i].metadata as PhotoMetadata).positionData?.city : '');
|
||||||
const caption = (new Date(media[i].metadata.creationDate)).getFullYear() + (location ? ', ' + location : '');
|
const caption = Utils.getFullYear(media[i].metadata.creationDate, media[i].metadata.creationDateOffset) + (location ? ', ' + location : '');
|
||||||
attachments.push({
|
attachments.push({
|
||||||
filename: media[i].name,
|
filename: media[i].name,
|
||||||
path: media[i].thumbnailPath,
|
path: media[i].thumbnailPath,
|
||||||
|
@ -110,6 +110,43 @@ export class Utils {
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getUTCFullYear(d: number | Date, offset: string) {
|
||||||
|
if (!(d instanceof Date)) {
|
||||||
|
d = new Date(d);
|
||||||
|
}
|
||||||
|
return new Date(new Date(d).toISOString().substring(0,19) + (offset ? offset : '')).getUTCFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFullYear(d: number | Date, offset: string) {
|
||||||
|
if (!(d instanceof Date)) {
|
||||||
|
d = new Date(d);
|
||||||
|
}
|
||||||
|
return new Date(new Date(d).toISOString().substring(0,19) + (offset ? offset : '')).getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static getOffsetString(offsetMinutes: number) {
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (regex.test(offsetString)) {
|
||||||
|
const hhmm = offsetString.split(":");
|
||||||
|
const hours = parseInt(hhmm[0]);
|
||||||
|
return hours < 0 ? ((hours*60) - parseInt(hhmm[1])) : ((hours*60) + parseInt(hhmm[1]));
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static renderDataSize(size: number): string {
|
static renderDataSize(size: number): string {
|
||||||
const postFixes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
const postFixes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
@ -79,6 +79,11 @@ export class ContentWrapper {
|
|||||||
(media as MediaDTO).metadata['t'] = (media as MediaDTO).metadata.creationDate / 1000; // skip millies
|
(media as MediaDTO).metadata['t'] = (media as MediaDTO).metadata.creationDate / 1000; // skip millies
|
||||||
delete (media as MediaDTO).metadata.creationDate;
|
delete (media as MediaDTO).metadata.creationDate;
|
||||||
|
|
||||||
|
if ((media as MediaDTO).metadata.creationDateOffset) {
|
||||||
|
// @ts-ignore
|
||||||
|
(media as MediaDTO).metadata['o'] = Utils.getOffsetMinutes((media as MediaDTO).metadata.creationDateOffset); // offset in minutes
|
||||||
|
delete (media as MediaDTO).metadata.creationDateOffset;
|
||||||
|
}
|
||||||
|
|
||||||
if ((media as PhotoDTO).metadata.rating) {
|
if ((media as PhotoDTO).metadata.rating) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -338,6 +343,14 @@ export class ContentWrapper {
|
|||||||
delete (media as PhotoDTO).metadata['t'];
|
delete (media as PhotoDTO).metadata['t'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (typeof (media as PhotoDTO).metadata['o'] !== 'undefined') {
|
||||||
|
// @ts-ignore
|
||||||
|
(media as PhotoDTO).metadata.creationDateOffset = Utils.getOffsetString((media as PhotoDTO).metadata['o']) ;//convert offset from minutes to String
|
||||||
|
// @ts-ignore
|
||||||
|
delete (media as PhotoDTO).metadata['o'];
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (typeof (media as PhotoDTO).metadata['r'] !== 'undefined') {
|
if (typeof (media as PhotoDTO).metadata['r'] !== 'undefined') {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -17,6 +17,7 @@ export interface MediaMetadata {
|
|||||||
size: MediaDimension;
|
size: MediaDimension;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
creationDateOffset?: string;
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
rating?: RatingTypes;
|
rating?: RatingTypes;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -33,6 +33,7 @@ export interface PhotoMetadata extends MediaMetadata {
|
|||||||
positionData?: PositionMetaData;
|
positionData?: PositionMetaData;
|
||||||
size: MediaDimension;
|
size: MediaDimension;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
|
creationDateOffset?: string;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
faces?: FaceRegion[];
|
faces?: FaceRegion[];
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ export interface VideoDTO extends MediaDTO {
|
|||||||
export interface VideoMetadata extends MediaMetadata {
|
export interface VideoMetadata extends MediaMetadata {
|
||||||
size: MediaDimension;
|
size: MediaDimension;
|
||||||
creationDate: number;
|
creationDate: number;
|
||||||
|
creationDateOffset?: string;
|
||||||
bitRate: number;
|
bitRate: number;
|
||||||
duration: number; // in milliseconds
|
duration: number; // in milliseconds
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
@ -24,7 +24,8 @@
|
|||||||
{{media.metadata.fileSize | fileSize}}
|
{{media.metadata.fileSize | fileSize}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3 align-self-center" [title]="media.metadata.creationDate">
|
<div class="col-3 align-self-center" [title]="media.metadata.creationDate">
|
||||||
{{media.metadata.creationDate | date}}, {{media.metadata.creationDate | date:'mediumTime'}}
|
{{ media.metadata.creationDate | date : 'longDate' : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }},
|
||||||
|
{{ media.metadata.creationDate | date : (media.metadata.creationDateOffset ? 'HH:mm:ss ZZZZZ' : 'HH:mm:ss') : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -205,7 +205,7 @@ export class FilterService {
|
|||||||
const startMediaDate = new Date(floorDate(minDate));
|
const startMediaDate = new Date(floorDate(minDate));
|
||||||
|
|
||||||
prefiltered.media.forEach(m => {
|
prefiltered.media.forEach(m => {
|
||||||
const key = Math.floor((floorDate(m.metadata.creationDate) - startMediaDate.getTime()) / 1000 / usedDiv);
|
const key = Math.floor((floorDate(m.metadata.creationDate) - startMediaDate.getTime()) / 1000 / usedDiv); //TODO
|
||||||
|
|
||||||
const getDate = (index: number) => {
|
const getDate = (index: number) => {
|
||||||
let d: Date;
|
let d: Date;
|
||||||
|
@ -496,7 +496,7 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
|
|||||||
case LightBoxTitleTexts.persons:
|
case LightBoxTitleTexts.persons:
|
||||||
return m.metadata.faces?.map(f => f.name)?.join(', ');
|
return m.metadata.faces?.map(f => f.name)?.join(', ');
|
||||||
case LightBoxTitleTexts.date:
|
case LightBoxTitleTexts.date:
|
||||||
return this.datePipe.transform(m.metadata.creationDate, 'longDate');
|
return this.datePipe.transform(m.metadata.creationDate, 'longDate', m.metadata.creationDateOffset);
|
||||||
case LightBoxTitleTexts.location:
|
case LightBoxTitleTexts.location:
|
||||||
return (
|
return (
|
||||||
m.metadata.positionData?.city ||
|
m.metadata.positionData?.city ||
|
||||||
|
@ -54,10 +54,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-11">
|
<div class="col-11">
|
||||||
<div class="details-main">
|
<div class="details-main">
|
||||||
{{ media.metadata.creationDate | date: (isThisYear() ? 'MMMM d' : 'longDate') : 'UTC' }}
|
{{ media.metadata.creationDate | date: (isThisYear() ? 'MMMM d' : 'longDate') : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="details-sub text-secondary row">
|
<div class="details-sub text-secondary row">
|
||||||
<div class="col-12">{{ media.metadata.creationDate | date : 'EEEE, HH:mm:ss' : 'UTC' }}</div>
|
<div class="col-12">{{ media.metadata.creationDate | date : (media.metadata.creationDateOffset ? 'EEEE, HH:mm:ss ZZZZZ' : 'EEEE, HH:mm:ss') : (media.metadata.creationDateOffset ? media.metadata.creationDateOffset : 'UTC') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -148,7 +148,7 @@ export class InfoPanelLightboxComponent implements OnInit, OnChanges {
|
|||||||
isThisYear(): boolean {
|
isThisYear(): boolean {
|
||||||
return (
|
return (
|
||||||
new Date().getFullYear() ===
|
new Date().getFullYear() ===
|
||||||
new Date(this.media.metadata.creationDate).getUTCFullYear()
|
Utils.getUTCFullYear(this.media.metadata.creationDate, this.media.metadata.creationDateOffset)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ export class GallerySortingService {
|
|||||||
private getGroupByNameFn(grouping: GroupingMethod) {
|
private getGroupByNameFn(grouping: GroupingMethod) {
|
||||||
switch (grouping.method) {
|
switch (grouping.method) {
|
||||||
case SortByTypes.Date:
|
case SortByTypes.Date:
|
||||||
return (m: MediaDTO) => this.datePipe.transform(m.metadata.creationDate, 'longDate', 'UTC');
|
return (m: MediaDTO) => this.datePipe.transform(m.metadata.creationDate, 'longDate', m.metadata.creationDateOffset);
|
||||||
|
|
||||||
case SortByTypes.Name:
|
case SortByTypes.Name:
|
||||||
return (m: MediaDTO) => m.name.at(0).toUpperCase();
|
return (m: MediaDTO) => m.name.at(0).toUpperCase();
|
||||||
|
@ -55,6 +55,7 @@ export class TestHelper {
|
|||||||
m.caption = null;
|
m.caption = null;
|
||||||
m.size = sd;
|
m.size = sd;
|
||||||
m.creationDate = 1656069387772;
|
m.creationDate = 1656069387772;
|
||||||
|
m.creationDateOffset = "+02:00"
|
||||||
m.fileSize = 123456789;
|
m.fileSize = 123456789;
|
||||||
// m.rating = 0; no rating by default
|
// m.rating = 0; no rating by default
|
||||||
|
|
||||||
@ -101,6 +102,7 @@ export class TestHelper {
|
|||||||
m.positionData = pd;
|
m.positionData = pd;
|
||||||
m.size = sd;
|
m.size = sd;
|
||||||
m.creationDate = 1656069387772;
|
m.creationDate = 1656069387772;
|
||||||
|
m.creationDateOffset = "-05:00";
|
||||||
m.fileSize = 123456789;
|
m.fileSize = 123456789;
|
||||||
// m.rating = 0; no rating by default
|
// m.rating = 0; no rating by default
|
||||||
|
|
||||||
@ -177,6 +179,7 @@ export class TestHelper {
|
|||||||
p.metadata.positionData.GPSData.latitude = 10;
|
p.metadata.positionData.GPSData.latitude = 10;
|
||||||
p.metadata.positionData.GPSData.longitude = 10;
|
p.metadata.positionData.GPSData.longitude = 10;
|
||||||
p.metadata.creationDate = 1656069387772 - 1000;
|
p.metadata.creationDate = 1656069387772 - 1000;
|
||||||
|
p.metadata.creationDateOffset = "+00:00";
|
||||||
p.metadata.rating = 1;
|
p.metadata.rating = 1;
|
||||||
p.metadata.size.height = 1000;
|
p.metadata.size.height = 1000;
|
||||||
p.metadata.size.width = 1000;
|
p.metadata.size.width = 1000;
|
||||||
@ -215,6 +218,7 @@ export class TestHelper {
|
|||||||
p.metadata.positionData.GPSData.latitude = -10;
|
p.metadata.positionData.GPSData.latitude = -10;
|
||||||
p.metadata.positionData.GPSData.longitude = -10;
|
p.metadata.positionData.GPSData.longitude = -10;
|
||||||
p.metadata.creationDate = 1656069387772 - 2000;
|
p.metadata.creationDate = 1656069387772 - 2000;
|
||||||
|
p.metadata.creationDateOffset = "+11:00";
|
||||||
p.metadata.rating = 2;
|
p.metadata.rating = 2;
|
||||||
p.metadata.size.height = 2000;
|
p.metadata.size.height = 2000;
|
||||||
p.metadata.size.width = 1000;
|
p.metadata.size.width = 1000;
|
||||||
@ -247,6 +251,7 @@ export class TestHelper {
|
|||||||
p.metadata.positionData.GPSData.latitude = 10;
|
p.metadata.positionData.GPSData.latitude = 10;
|
||||||
p.metadata.positionData.GPSData.longitude = 15;
|
p.metadata.positionData.GPSData.longitude = 15;
|
||||||
p.metadata.creationDate = 1656069387772 - 3000;
|
p.metadata.creationDate = 1656069387772 - 3000;
|
||||||
|
p.metadata.creationDateOffset = "-03:45";
|
||||||
p.metadata.rating = 3;
|
p.metadata.rating = 3;
|
||||||
p.metadata.size.height = 1000;
|
p.metadata.size.height = 1000;
|
||||||
p.metadata.size.width = 2000;
|
p.metadata.size.width = 2000;
|
||||||
@ -275,6 +280,7 @@ export class TestHelper {
|
|||||||
p.metadata.positionData.GPSData.latitude = 15;
|
p.metadata.positionData.GPSData.latitude = 15;
|
||||||
p.metadata.positionData.GPSData.longitude = 10;
|
p.metadata.positionData.GPSData.longitude = 10;
|
||||||
p.metadata.creationDate = 1656069387772 - 4000;
|
p.metadata.creationDate = 1656069387772 - 4000;
|
||||||
|
p.metadata.creationDateOffset = "+04:30";
|
||||||
p.metadata.size.height = 3000;
|
p.metadata.size.height = 3000;
|
||||||
p.metadata.size.width = 2000;
|
p.metadata.size.width = 2000;
|
||||||
|
|
||||||
@ -394,6 +400,7 @@ export class TestHelper {
|
|||||||
positionData: pd,
|
positionData: pd,
|
||||||
size: sd,
|
size: sd,
|
||||||
creationDate: Date.now() + ++TestHelper.creationCounter,
|
creationDate: Date.now() + ++TestHelper.creationCounter,
|
||||||
|
creationDateOffset: "+01:00",
|
||||||
fileSize: rndInt(10000),
|
fileSize: rndInt(10000),
|
||||||
caption: rndStr(),
|
caption: rndStr(),
|
||||||
rating: rndInt(5) as any,
|
rating: rndInt(5) as any,
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
},
|
},
|
||||||
"creationDate": 1706659327000,
|
"creationDate": 1706655727000,
|
||||||
|
"creationDateOffset": "+01:00",
|
||||||
"fileSize": 111432,
|
"fileSize": 111432,
|
||||||
"positionData": {
|
"positionData": {
|
||||||
"GPSData": {
|
"GPSData": {
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
"model": "Canon EOS 600D"
|
"model": "Canon EOS 600D"
|
||||||
},
|
},
|
||||||
"caption": "Bambi Caption",
|
"caption": "Bambi Caption",
|
||||||
"creationDate": -11630935227000,
|
"creationDate": -11630942427000,
|
||||||
|
"creationDateOffset": "+02:00",
|
||||||
"faces": [
|
"faces": [
|
||||||
{
|
{
|
||||||
"box": {
|
"box": {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"make": "NIKON",
|
"make": "NIKON",
|
||||||
"model": "E880"
|
"model": "E880"
|
||||||
},
|
},
|
||||||
"creationDate": -2211753600000,
|
"creationDate": 0,
|
||||||
"fileSize": 72850,
|
"fileSize": 72850,
|
||||||
"size": {
|
"size": {
|
||||||
"height": 768,
|
"height": 768,
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
"width": 26,
|
"width": 26,
|
||||||
"height": 26
|
"height": 26
|
||||||
},
|
},
|
||||||
"creationDate": 1707167247786,
|
"creationDate": 1599990007000,
|
||||||
|
"creationDateOffset": "+05:00",
|
||||||
"fileSize": 5758,
|
"fileSize": 5758,
|
||||||
"keywords": [
|
"keywords": [
|
||||||
],
|
],
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@ -24,5 +24,6 @@
|
|||||||
"size": {
|
"size": {
|
||||||
"height": 26,
|
"height": 26,
|
||||||
"width": 26
|
"width": 26
|
||||||
}
|
},
|
||||||
|
"creationDate": 1544748139000
|
||||||
}
|
}
|
||||||
|
BIN
test/backend/assets/timestamps/big_ben.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
25
test/backend/assets/timestamps/big_ben.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"size": {
|
||||||
|
"width": 200,
|
||||||
|
"height": 300
|
||||||
|
},
|
||||||
|
"creationDate": 1686141955000,
|
||||||
|
"creationDateOffset": "+01:00",
|
||||||
|
"fileSize": 18532,
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 18 KiB |
@ -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"
|
||||||
|
]
|
||||||
|
}
|
BIN
test/backend/assets/timestamps/big_ben_only_time.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
20
test/backend/assets/timestamps/big_ben_only_time.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"size": {
|
||||||
|
"width": 200,
|
||||||
|
"height": 300
|
||||||
|
},
|
||||||
|
"creationDate": 1686145555000,
|
||||||
|
"fileSize": 17850,
|
||||||
|
"cameraData": {
|
||||||
|
"model": "Canon EOS R5",
|
||||||
|
"make": "Canon"
|
||||||
|
},
|
||||||
|
"positionData": {
|
||||||
|
"country": "Storbritannien",
|
||||||
|
"state": "England",
|
||||||
|
"city": "St James's"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Big Ben"
|
||||||
|
]
|
||||||
|
}
|
BIN
test/backend/assets/timestamps/sydney_opera_house.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
25
test/backend/assets/timestamps/sydney_opera_house.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"size": {
|
||||||
|
"width": 300,
|
||||||
|
"height": 200
|
||||||
|
},
|
||||||
|
"creationDate": 1600512957000,
|
||||||
|
"creationDateOffset": "+10:00",
|
||||||
|
"fileSize": 22755,
|
||||||
|
"cameraData": {
|
||||||
|
"model": "ILCE-7RM3",
|
||||||
|
"make": "Sony"
|
||||||
|
},
|
||||||
|
"positionData": {
|
||||||
|
"GPSData": {
|
||||||
|
"longitude": 151.210381,
|
||||||
|
"latitude": -33.855698
|
||||||
|
},
|
||||||
|
"country": "Australien",
|
||||||
|
"state": "New South Wales",
|
||||||
|
"city": "Dawes Point"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Sydney Opera House"
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"size": {
|
||||||
|
"width": 300,
|
||||||
|
"height": 200
|
||||||
|
},
|
||||||
|
"creationDate": 1600512957000,
|
||||||
|
"creationDateOffset": "+10:00",
|
||||||
|
"fileSize": 22641,
|
||||||
|
"cameraData": {
|
||||||
|
"model": "ILCE-7RM3",
|
||||||
|
"make": "Sony"
|
||||||
|
},
|
||||||
|
"positionData": {
|
||||||
|
"GPSData": {
|
||||||
|
"longitude": 151.210381,
|
||||||
|
"latitude": -33.855698
|
||||||
|
},
|
||||||
|
"country": "Australien",
|
||||||
|
"state": "New South Wales",
|
||||||
|
"city": "Dawes Point"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Sydney Opera House"
|
||||||
|
]
|
||||||
|
}
|
@ -7,7 +7,8 @@
|
|||||||
"make": "samsung",
|
"make": "samsung",
|
||||||
"model": "SM-G975F"
|
"model": "SM-G975F"
|
||||||
},
|
},
|
||||||
"creationDate": 1619181527000,
|
"creationDate": 1619174327000,
|
||||||
|
"creationDateOffset": "+02:00",
|
||||||
"fileSize": 4877,
|
"fileSize": 4877,
|
||||||
"rating":3,
|
"rating":3,
|
||||||
"size": {
|
"size": {
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
"make": "samsung",
|
"make": "samsung",
|
||||||
"model": "SM-G975F"
|
"model": "SM-G975F"
|
||||||
},
|
},
|
||||||
"creationDate": 1614703656000,
|
"creationDate": 1614700056000,
|
||||||
|
"creationDateOffset": "+01:00",
|
||||||
"fileSize": 4709,
|
"fileSize": 4709,
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Max",
|
"Max",
|
||||||
|
@ -24,11 +24,16 @@ describe('MetadataLoader', () => {
|
|||||||
|
|
||||||
it('should load png', async () => {
|
it('should load png', async () => {
|
||||||
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/test_png.png'));
|
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/test_png.png'));
|
||||||
delete data.creationDate; // creation time for png not supported
|
|
||||||
const expected = require(path.join(__dirname, '/../../../assets/test_png.json'));
|
const expected = require(path.join(__dirname, '/../../../assets/test_png.json'));
|
||||||
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should load png with faces and dates', async () => {
|
||||||
|
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/png_with_faces_and_dates.png'));
|
||||||
|
const expected = require(path.join(__dirname, '/../../../assets/png_with_faces_and_dates.json'));
|
||||||
|
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
||||||
|
});
|
||||||
|
|
||||||
it('should load jpg', async () => {
|
it('should load jpg', async () => {
|
||||||
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.jpg'));
|
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.jpg'));
|
||||||
const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json'));
|
const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json'));
|
||||||
@ -69,6 +74,31 @@ describe('MetadataLoader', () => {
|
|||||||
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should load jpg with timestamps, timezone AEST (UTC+10) and gps (UTC)', async () => {
|
||||||
|
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/timestamps/sydney_opera_house.jpg'));
|
||||||
|
const expected = require(path.join(__dirname, '/../../../assets/timestamps/sydney_opera_house.json'));
|
||||||
|
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('should load jpg with timestamps and gps (UTC) and calculate offset +10', async () => {
|
||||||
|
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/timestamps/sydney_opera_house_no_tsoffset_but_gps_utc.jpg'));
|
||||||
|
const expected = require(path.join(__dirname, '/../../../assets/timestamps/sydney_opera_house_no_tsoffset_but_gps_utc.json'));
|
||||||
|
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('should load jpg with timestamps, timezone BST (UTC+1) and gps (UTC)', async () => {
|
||||||
|
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/timestamps/big_ben.jpg'));
|
||||||
|
const expected = require(path.join(__dirname, '/../../../assets/timestamps/big_ben.json'));
|
||||||
|
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
||||||
|
});
|
||||||
|
it('should load jpg with timestamps and gps (UTC) and calculate offset +1', async () => {
|
||||||
|
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/timestamps/big_ben_no_tsoffset_but_gps_utc.jpg'));
|
||||||
|
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 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'));
|
||||||
|
expect(Utils.clone(data)).to.be.deep.equal(expected);
|
||||||
|
});
|
||||||
describe('should load jpg with proper height and orientation', () => {
|
describe('should load jpg with proper height and orientation', () => {
|
||||||
it('jpg 1', async () => {
|
it('jpg 1', async () => {
|
||||||
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/orientation/broken_orientation_exif.jpg'));
|
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/orientation/broken_orientation_exif.jpg'));
|
||||||
|