1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-23 01:27:14 +02:00

Merge pull request #829 from grasdk/master

consolidate exif parsing libraries - rework of timestamps
This commit is contained in:
Patrik J. Braun 2024-03-02 22:22:54 +01:00 committed by GitHub
commit 9a90a15e82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 921 additions and 609 deletions

3
.gitignore vendored
View File

@ -39,4 +39,5 @@ test.*
*.sublime-workspace *.sublime-workspace
.DS_Store .DS_Store
/coverage/ /coverage/
.nyc_output/ .nyc_output/
.vscode*

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

13
package-lock.json generated
View File

@ -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,

View File

@ -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",

View File

@ -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,8 +590,8 @@ 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
); );
return q; return q;
@ -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);

View File

@ -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')
@ -105,6 +106,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;

File diff suppressed because it is too large Load Diff

View File

@ -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,

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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[];
} }

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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 ||

View File

@ -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>

View File

@ -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)
); );
} }

View File

@ -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();

View File

@ -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,

View File

@ -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": {

View File

@ -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": {

View File

@ -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,

View File

@ -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": [
], ],

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -24,5 +24,6 @@
"size": { "size": {
"height": 26, "height": 26,
"width": 26 "width": 26
} },
"creationDate": 1544748139000
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View 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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -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"
]
}

View File

@ -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": {

View File

@ -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",

View File

@ -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'));