mirror of
https://github.com/bpatrik/pigallery2.git
synced 2024-12-23 01:27:14 +02:00
Merge pull request #868 from grasdk/master
Metadata mapping and consolidation
This commit is contained in:
commit
086bc47a01
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,6 +17,7 @@ test/e2e/**/*.js
|
||||
test/e2e/**/*.js.map
|
||||
test/*.js
|
||||
test/*.js.map
|
||||
test/tmp/*
|
||||
benchmark/**/*.js
|
||||
benchmark/**/*.js.map
|
||||
gulpfile.js
|
||||
|
@ -1,9 +1,9 @@
|
||||
import * as fs from 'fs';
|
||||
import { imageSize } from 'image-size';
|
||||
import { Config } from '../../../common/config/private/Config';
|
||||
import { SideCar } from '../../../common/entities/MediaDTO';
|
||||
import { FaceRegion, PhotoMetadata } from '../../../common/entities/PhotoDTO';
|
||||
import { VideoMetadata } from '../../../common/entities/VideoDTO';
|
||||
import { RatingTypes } from '../../../common/entities/MediaDTO';
|
||||
import { Logger } from '../../Logger';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
@ -142,57 +142,9 @@ export class MetadataLoader {
|
||||
|
||||
for (const sidecarPath of sidecarPaths) {
|
||||
if (fs.existsSync(sidecarPath)) {
|
||||
const sidecarData = await exifr.sidecar(sidecarPath);
|
||||
const sidecarData: any = await exifr.sidecar(sidecarPath);
|
||||
if (sidecarData !== undefined) {
|
||||
if ((sidecarData as SideCar).dc !== undefined) {
|
||||
if ((sidecarData as SideCar).dc.subject !== undefined) {
|
||||
if (metadata.keywords === undefined) {
|
||||
metadata.keywords = [];
|
||||
}
|
||||
let keywords = (sidecarData as SideCar).dc.subject || [];
|
||||
if (typeof keywords === 'string') {
|
||||
keywords = [keywords];
|
||||
}
|
||||
for (const kw of keywords) {
|
||||
if (metadata.keywords.indexOf(kw) === -1) {
|
||||
metadata.keywords.push(kw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hasPhotoshopDate = false;
|
||||
if ((sidecarData as SideCar).photoshop !== undefined) {
|
||||
if ((sidecarData as SideCar).photoshop.DateCreated !== undefined) {
|
||||
const date = Utils.timestampToMS((sidecarData as SideCar).photoshop.DateCreated, null);
|
||||
if (date) {
|
||||
metadata.creationDate = date;
|
||||
hasPhotoshopDate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(sidecarData, 'xap')) {
|
||||
(sidecarData as any)['xmp'] = (sidecarData as any)['xap'];
|
||||
delete (sidecarData as any)['xap'];
|
||||
}
|
||||
if ((sidecarData as SideCar).xmp !== undefined) {
|
||||
if (
|
||||
(sidecarData as SideCar).xmp.Rating !== undefined &&
|
||||
(sidecarData as SideCar).xmp.Rating > 0
|
||||
) {
|
||||
metadata.rating = (sidecarData as SideCar).xmp.Rating;
|
||||
}
|
||||
if (
|
||||
!hasPhotoshopDate && (
|
||||
(sidecarData as SideCar).xmp.CreateDate !== undefined ||
|
||||
(sidecarData as SideCar).xmp.ModifyDate !== undefined
|
||||
)
|
||||
) {
|
||||
metadata.creationDate =
|
||||
Utils.timestampToMS((sidecarData as SideCar).xmp.CreateDate, null) ||
|
||||
Utils.timestampToMS((sidecarData as SideCar).xmp.ModifyDate, null) ||
|
||||
metadata.creationDate;
|
||||
}
|
||||
}
|
||||
MetadataLoader.mapMetadata(metadata, sidecarData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -205,6 +157,7 @@ export class MetadataLoader {
|
||||
Logger.silly(LOG_TAG, 'Error loading metadata for : ' + fullPath);
|
||||
Logger.silly(err);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@ -235,14 +188,6 @@ export class MetadataLoader {
|
||||
translateValues: false, //don't translate orientation from numbers to strings etc.
|
||||
mergeOutput: false //don't merge output, because things like Microsoft Rating (percent) and xmp.rating will be merged
|
||||
};
|
||||
|
||||
//Function to convert html code for special characters into their corresponding character (used in exif.photoshop-section)
|
||||
const unescape = (tag: string) => {
|
||||
return tag.replace(/&#([0-9]{1,3});/gi, function (match, numStr) {
|
||||
return String.fromCharCode(parseInt(numStr, 10));
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let bufferSize = Config.Media.photoMetadataSize;
|
||||
try {
|
||||
@ -254,6 +199,15 @@ export class MetadataLoader {
|
||||
} catch (err) {
|
||||
// ignoring errors
|
||||
}
|
||||
try {
|
||||
//read the actual image size, don't rely on tags for this
|
||||
const info = imageSize(fullPath);
|
||||
metadata.size = { width: info.width, height: info.height };
|
||||
} catch (e) {
|
||||
//in case of failure, set dimensions to 0 so they may be read via tags
|
||||
metadata.size = { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
|
||||
const data = Buffer.allocUnsafe(bufferSize);
|
||||
fileHandle = await fs.promises.open(fullPath, 'r');
|
||||
@ -267,14 +221,6 @@ export class MetadataLoader {
|
||||
await fileHandle.close();
|
||||
}
|
||||
try {
|
||||
try {
|
||||
//read the actual image size, don't rely on tags for this
|
||||
const info = imageSize(fullPath);
|
||||
metadata.size = { width: info.width, height: info.height };
|
||||
} catch (e) {
|
||||
//in case of failure, set dimensions to 0 so they may be read via tags
|
||||
metadata.size = { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
|
||||
try { //Parse iptc data using the IptcParser, which works correctly for both UTF-8 and ASCII
|
||||
@ -316,11 +262,99 @@ export class MetadataLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
let orientation = 1; //Orientation 1 is normal
|
||||
const exif = await exifr.parse(data, exifrOptions);
|
||||
//exif is structured in sections, we read the data by section
|
||||
MetadataLoader.mapMetadata(metadata, exif);
|
||||
} catch (err) {
|
||||
// ignoring errors
|
||||
}
|
||||
|
||||
//dc-section (subject is the only tag we want from dc)
|
||||
try {
|
||||
// search for sidecar and merge metadata
|
||||
const fullPathWithoutExt = path.join(path.parse(fullPath).dir, path.parse(fullPath).name);
|
||||
const sidecarPaths = [
|
||||
fullPath + '.xmp',
|
||||
fullPath + '.XMP',
|
||||
fullPathWithoutExt + '.xmp',
|
||||
fullPathWithoutExt + '.XMP',
|
||||
];
|
||||
|
||||
for (const sidecarPath of sidecarPaths) {
|
||||
if (fs.existsSync(sidecarPath)) {
|
||||
const sidecarData: any = await exifr.sidecar(sidecarPath, exifrOptions);
|
||||
if (sidecarData !== undefined) {
|
||||
MetadataLoader.mapMetadata(metadata, sidecarData);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.silly(LOG_TAG, 'Error loading sidecar metadata for : ' + fullPath);
|
||||
Logger.silly(err);
|
||||
}
|
||||
if (!metadata.creationDate) {
|
||||
// creationDate can be negative, when it was created before epoch (1970)
|
||||
metadata.creationDate = 0;
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(LOG_TAG, 'Error during reading photo: ' + fullPath);
|
||||
console.error(err);
|
||||
return MetadataLoader.EMPTY_METADATA;
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(LOG_TAG, 'Error during reading photo: ' + fullPath);
|
||||
console.error(err);
|
||||
return MetadataLoader.EMPTY_METADATA;
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private static mapMetadata(metadata: PhotoMetadata, exif: any) {
|
||||
//replace adobe xap-section with xmp to reuse parsing
|
||||
if (Object.hasOwn(exif, 'xap')) {
|
||||
exif['xmp'] = exif['xap'];
|
||||
delete exif['xap'];
|
||||
}
|
||||
const orientation = MetadataLoader.getOrientation(exif);
|
||||
MetadataLoader.mapImageDimensions(metadata, exif, orientation);
|
||||
MetadataLoader.mapKeywords(metadata, exif);
|
||||
MetadataLoader.mapTimestampAndOffset(metadata, exif);
|
||||
MetadataLoader.mapCameraData(metadata, exif);
|
||||
MetadataLoader.mapGPS(metadata, exif);
|
||||
MetadataLoader.mapToponyms(metadata, exif);
|
||||
MetadataLoader.mapRating(metadata, exif);
|
||||
if (Config.Faces.enabled) {
|
||||
MetadataLoader.mapFaces(metadata, exif, orientation);
|
||||
}
|
||||
|
||||
}
|
||||
private static getOrientation(exif: any): number {
|
||||
let orientation = 1; //Orientation 1 is normal
|
||||
if (exif.ifd0?.Orientation != undefined) {
|
||||
orientation = parseInt(exif.ifd0.Orientation as any, 10) as number;
|
||||
}
|
||||
return orientation;
|
||||
}
|
||||
|
||||
private static mapImageDimensions(metadata: PhotoMetadata, exif: any, orientation: number) {
|
||||
if (metadata.size.width <= 0) {
|
||||
metadata.size.width = exif.ifd0?.ImageWidth || exif.exif?.ExifImageWidth;
|
||||
}
|
||||
if (metadata.size.height <= 0) {
|
||||
metadata.size.height = exif.ifd0?.ImageHeight || exif.exif?.ExifImageHeight;
|
||||
}
|
||||
metadata.size.height = Math.max(metadata.size.height, 1); //ensure height dimension is positive
|
||||
metadata.size.width = Math.max(metadata.size.width, 1); //ensure width dimension is positive
|
||||
|
||||
//we need to switch width and height for images that are rotated sideways
|
||||
if (4 < orientation) { //Orientation is sideways (rotated 90% or 270%)
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
const height = metadata.size.width;
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
metadata.size.width = metadata.size.height;
|
||||
metadata.size.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
private static mapKeywords(metadata: PhotoMetadata, exif: any) {
|
||||
if (exif.dc &&
|
||||
exif.dc.subject &&
|
||||
exif.dc.subject.length > 0) {
|
||||
@ -334,127 +368,133 @@ export class MetadataLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//ifd0 section
|
||||
if (exif.ifd0) {
|
||||
if (exif.ifd0.ImageWidth && metadata.size.width <= 0) {
|
||||
metadata.size.width = exif.ifd0.ImageWidth;
|
||||
}
|
||||
if (exif.ifd0.ImageHeight && metadata.size.height <= 0) {
|
||||
metadata.size.height = exif.ifd0.ImageHeight;
|
||||
}
|
||||
if (exif.ifd0.Orientation) {
|
||||
orientation = parseInt(
|
||||
exif.ifd0.Orientation as any,
|
||||
10
|
||||
) as number;
|
||||
}
|
||||
if (exif.ifd0.Make && exif.ifd0.Make !== '') {
|
||||
metadata.cameraData = metadata.cameraData || {};
|
||||
metadata.cameraData.make = '' + exif.ifd0.Make;
|
||||
}
|
||||
if (exif.ifd0.Model && exif.ifd0.Model !== '') {
|
||||
metadata.cameraData = metadata.cameraData || {};
|
||||
metadata.cameraData.model = '' + exif.ifd0.Model;
|
||||
}
|
||||
//if (exif.ifd0.ModifyDate) {} //Deferred to the exif-section where the other timestamps are
|
||||
}
|
||||
|
||||
//exif section starting with the date sectino
|
||||
private static mapTimestampAndOffset(metadata: PhotoMetadata, exif: any) {
|
||||
metadata.creationDate = Utils.timestampToMS(exif?.photoshop?.DateCreated, null) ||
|
||||
Utils.timestampToMS(exif?.xmp?.CreateDate, null) ||
|
||||
Utils.timestampToMS(exif?.xmp?.ModifyDate, null) ||
|
||||
metadata.creationDate;
|
||||
|
||||
metadata.creationDateOffset = Utils.timestampToOffsetString(exif?.photoshop?.DateCreated) ||
|
||||
Utils.timestampToOffsetString(exif?.xmp?.CreateDate) ||
|
||||
metadata.creationDateOffset;
|
||||
if (exif.exif) {
|
||||
let offset = undefined;
|
||||
//Preceedence of dates: exif.DateTimeOriginal, exif.CreateDate, ifd0.ModifyDate, ihdr["Creation Time"], xmp.MetadataDate, file system date
|
||||
//Filesystem is the absolute last resort, and it's hard to write tests for, since file system dates are changed on e.g. git clone.
|
||||
if (exif.exif.DateTimeOriginal) {
|
||||
//DateTimeOriginal is when the camera shutter closed
|
||||
let offset = exif.exif.OffsetTimeOriginal; //OffsetTimeOriginal is the corresponding offset
|
||||
offset = exif.exif.OffsetTimeOriginal; //OffsetTimeOriginal is the corresponding offset
|
||||
if (!offset) { //Find offset among other options if possible
|
||||
offset = exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps);
|
||||
}
|
||||
metadata.creationDate = Utils.timestampToMS(exif.exif.DateTimeOriginal, offset);
|
||||
metadata.creationDateOffset = offset;
|
||||
} else if (exif.exif.CreateDate) { //using else if here, because DateTimeOriginal has preceedence
|
||||
//Create is when the camera wrote the file (typically within the same ms as shutter close)
|
||||
let offset = exif.exif.OffsetTimeDigitized; //OffsetTimeDigitized is the corresponding offset
|
||||
offset = exif.exif.OffsetTimeDigitized; //OffsetTimeDigitized is the corresponding offset
|
||||
if (!offset) { //Find offset among other options if possible
|
||||
offset = exif.exif.OffsetTimeOriginal || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps);
|
||||
}
|
||||
metadata.creationDate = Utils.timestampToMS(exif.exif.CreateDate, offset);
|
||||
metadata.creationDateOffset = offset;
|
||||
} else if (exif.ifd0?.ModifyDate) { //using else if here, because DateTimeOriginal and CreatDate have preceedence
|
||||
let offset = exif.exif.OffsetTime; //exif.Offsettime is the offset corresponding to ifd0.ModifyDate
|
||||
offset = exif.exif.OffsetTime; //exif.Offsettime is the offset corresponding to ifd0.ModifyDate
|
||||
if (!offset) { //Find offset among other options if possible
|
||||
offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps);
|
||||
}
|
||||
metadata.creationDate = Utils.timestampToMS(exif.ifd0.ModifyDate, offset);
|
||||
metadata.creationDateOffset = offset
|
||||
} else if (exif.ihdr && exif.ihdr["Creation Time"]) {// again else if (another fallback date if the good ones aren't there) {
|
||||
const any_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps);
|
||||
metadata.creationDate = Utils.timestampToMS(exif.ihdr["Creation Time"], any_offset);
|
||||
metadata.creationDateOffset = any_offset;
|
||||
offset = any_offset;
|
||||
} else if (exif.xmp?.MetadataDate) {// again else if (another fallback date if the good ones aren't there - metadata date is probably later than actual creation date, but much better than file time) {
|
||||
const any_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps);
|
||||
metadata.creationDate = Utils.timestampToMS(exif.xmp.MetadataDate, any_offset);
|
||||
metadata.creationDateOffset = any_offset;
|
||||
offset = any_offset;
|
||||
}
|
||||
if (exif.exif.LensModel && exif.exif.LensModel !== '') {
|
||||
metadata.creationDateOffset = offset || metadata.creationDateOffset;
|
||||
}
|
||||
}
|
||||
|
||||
private static mapCameraData(metadata: PhotoMetadata, exif: any) {
|
||||
metadata.cameraData = metadata.cameraData || {};
|
||||
if (exif.ifd0) {
|
||||
if (exif.ifd0.Make && exif.ifd0.Make !== '') {
|
||||
metadata.cameraData.make = '' + exif.ifd0.Make;
|
||||
}
|
||||
if (exif.ifd0.Model && exif.ifd0.Model !== '') {
|
||||
metadata.cameraData.model = '' + exif.ifd0.Model;
|
||||
}
|
||||
}
|
||||
if (exif.exif) {
|
||||
if (exif.exif.LensModel && exif.exif.LensModel !== '') {
|
||||
metadata.cameraData.lens = '' + exif.exif.LensModel;
|
||||
}
|
||||
if (Utils.isUInt32(exif.exif.ISO)) {
|
||||
metadata.cameraData = 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;
|
||||
if (Object.keys(metadata.cameraData).length === 0) {
|
||||
delete metadata.cameraData;
|
||||
}
|
||||
}
|
||||
|
||||
//gps section
|
||||
if (exif.gps) {
|
||||
private static mapGPS(metadata: PhotoMetadata, exif: any) {
|
||||
try {
|
||||
if (exif.gps || (exif.exif && exif.exif.GPSLatitude && exif.exif.GPSLongitude)) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.GPSData = metadata.positionData.GPSData || {};
|
||||
|
||||
if (Utils.isFloat32(exif.gps.longitude)) {
|
||||
metadata.positionData.GPSData.longitude = parseFloat(
|
||||
exif.gps.longitude.toFixed(6)
|
||||
);
|
||||
metadata.positionData.GPSData.longitude = Utils.isFloat32(exif.gps?.longitude) ? exif.gps.longitude : Utils.xmpExifGpsCoordinateToDecimalDegrees(exif.exif.GPSLongitude);
|
||||
metadata.positionData.GPSData.latitude = Utils.isFloat32(exif.gps?.latitude) ? exif.gps.latitude : Utils.xmpExifGpsCoordinateToDecimalDegrees(exif.exif.GPSLatitude);
|
||||
|
||||
if (metadata.positionData.GPSData.longitude !== undefined) {
|
||||
metadata.positionData.GPSData.longitude = parseFloat(metadata.positionData.GPSData.longitude.toFixed(6))
|
||||
}
|
||||
if (metadata.positionData.GPSData.latitude !== undefined) {
|
||||
metadata.positionData.GPSData.latitude = parseFloat(metadata.positionData.GPSData.latitude.toFixed(6))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(LOG_TAG, 'Error during reading of GPS data: ' + err);
|
||||
} finally {
|
||||
if (metadata.positionData?.GPSData &&
|
||||
(Object.keys(metadata.positionData.GPSData).length === 0 ||
|
||||
metadata.positionData.GPSData.longitude === undefined ||
|
||||
metadata.positionData.GPSData.latitude === undefined)) {
|
||||
delete metadata.positionData.GPSData;
|
||||
}
|
||||
if (metadata.positionData) {
|
||||
if (Object.keys(metadata.positionData).length === 0) {
|
||||
delete metadata.positionData;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
private static mapToponyms(metadata: PhotoMetadata, exif: any) {
|
||||
//Function to convert html code for special characters into their corresponding character (used in exif.photoshop-section)
|
||||
const unescape = (tag: string) => {
|
||||
return tag.replace(/&#([0-9]{1,3});/gi, function (match, numStr) {
|
||||
return String.fromCharCode(parseInt(numStr, 10));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
//photoshop section (sometimes has City, Country and State)
|
||||
//photoshop section sometimes has City, Country and State
|
||||
if (exif.photoshop) {
|
||||
if (!metadata.positionData?.country && exif.photoshop.Country) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
@ -469,32 +509,27 @@ export class MetadataLoader {
|
||||
metadata.positionData.city = unescape(exif.photoshop.City);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////
|
||||
metadata.size.height = Math.max(metadata.size.height, 1); //ensure height dimension is positive
|
||||
metadata.size.width = Math.max(metadata.size.width, 1); //ensure width dimension is positive
|
||||
|
||||
//Before moving on to the XMP section (particularly the regions (mwg-rs))
|
||||
//we need to switch width and height for images that are rotated sideways
|
||||
if (4 < orientation) { //Orientation is sideways (rotated 90% or 270%)
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
const height = metadata.size.width;
|
||||
// noinspection JSSuspiciousNameCombination
|
||||
metadata.size.width = metadata.size.height;
|
||||
metadata.size.height = height;
|
||||
}
|
||||
///////////////////////////////////////
|
||||
|
||||
//xmp section
|
||||
if (exif.xmp && exif.xmp.Rating) {
|
||||
metadata.rating = exif.xmp.Rating;
|
||||
if (metadata.rating < 0) {
|
||||
metadata.rating = 0;
|
||||
private static mapRating(metadata: PhotoMetadata, exif: any) {
|
||||
if (exif.xmp &&
|
||||
exif.xmp.Rating !== undefined) {
|
||||
const rting = Math.round(exif.xmp.Rating);
|
||||
if (rting <= 0) {
|
||||
//We map all ratings below 0 to 0. Lightroom supports value -1, but most other tools (including this) don't.
|
||||
//Rating 0 means "unrated" according to adobe's spec, so we delete the attribute in pigallery for the same effect
|
||||
delete metadata.rating;
|
||||
} else if (rting > 5) { //map all ratings above 5 to 5
|
||||
metadata.rating = 5;
|
||||
} else {
|
||||
metadata.rating = (rting as RatingTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static mapFaces(metadata: PhotoMetadata, exif: any, orientation: number) {
|
||||
//xmp."mwg-rs" section
|
||||
if (Config.Faces.enabled &&
|
||||
exif["mwg-rs"] &&
|
||||
if (exif["mwg-rs"] &&
|
||||
exif["mwg-rs"].Regions) {
|
||||
const faces: FaceRegion[] = [];
|
||||
const regionListVal = Array.isArray(exif["mwg-rs"].Regions.RegionList) ? exif["mwg-rs"].Regions.RegionList : [exif["mwg-rs"].Regions.RegionList];
|
||||
@ -599,113 +634,5 @@ export class MetadataLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignoring errors
|
||||
}
|
||||
|
||||
if (!metadata.creationDate) {
|
||||
// creationDate can be negative, when it was created before epoch (1970)
|
||||
metadata.creationDate = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
// search for sidecar and merge metadata
|
||||
const fullPathWithoutExt = path.join(path.parse(fullPath).dir, path.parse(fullPath).name);
|
||||
const sidecarPaths = [
|
||||
fullPath + '.xmp',
|
||||
fullPath + '.XMP',
|
||||
fullPathWithoutExt + '.xmp',
|
||||
fullPathWithoutExt + '.XMP',
|
||||
];
|
||||
|
||||
for (const sidecarPath of sidecarPaths) {
|
||||
if (fs.existsSync(sidecarPath)) {
|
||||
const sidecarData = await exifr.sidecar(sidecarPath);
|
||||
|
||||
if (sidecarData !== undefined) {
|
||||
if ((sidecarData as SideCar).dc !== undefined) {
|
||||
if ((sidecarData as SideCar).dc.subject !== undefined) {
|
||||
if (metadata.keywords === undefined) {
|
||||
metadata.keywords = [];
|
||||
}
|
||||
let keywords = (sidecarData as SideCar).dc.subject || [];
|
||||
if (typeof keywords === 'string') {
|
||||
keywords = [keywords];
|
||||
}
|
||||
for (const kw of keywords) {
|
||||
if (metadata.keywords.indexOf(kw) === -1) {
|
||||
metadata.keywords.push(kw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hasPhotoshopDate = false;
|
||||
if ((sidecarData as SideCar).photoshop !== undefined) {
|
||||
if ((sidecarData as SideCar).photoshop.DateCreated !== undefined) {
|
||||
const date = Utils.timestampToMS((sidecarData as SideCar).photoshop.DateCreated, null);
|
||||
if (date) {
|
||||
metadata.creationDate = date;
|
||||
hasPhotoshopDate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.hasOwn(sidecarData, 'xap')) {
|
||||
(sidecarData as any)['xmp'] = (sidecarData as any)['xap'];
|
||||
delete (sidecarData as any)['xap'];
|
||||
}
|
||||
if ((sidecarData as SideCar).xmp !== undefined) {
|
||||
if (
|
||||
(sidecarData as SideCar).xmp.Rating !== undefined &&
|
||||
(sidecarData as SideCar).xmp.Rating > 0
|
||||
) {
|
||||
metadata.rating = (sidecarData as SideCar).xmp.Rating;
|
||||
}
|
||||
if (
|
||||
!hasPhotoshopDate && (
|
||||
(sidecarData as SideCar).xmp.CreateDate !== undefined ||
|
||||
(sidecarData as SideCar).xmp.ModifyDate !== undefined
|
||||
)
|
||||
) {
|
||||
metadata.creationDate =
|
||||
Utils.timestampToMS((sidecarData as SideCar).xmp.CreateDate, null) ||
|
||||
Utils.timestampToMS((sidecarData as SideCar).xmp.ModifyDate, null) ||
|
||||
metadata.creationDate;
|
||||
}
|
||||
}
|
||||
if ((sidecarData as SideCar).exif !== undefined) {
|
||||
if (
|
||||
(sidecarData as SideCar).exif.GPSLatitude !== undefined &&
|
||||
(sidecarData as SideCar).exif.GPSLongitude !== undefined
|
||||
) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.GPSData = {};
|
||||
|
||||
metadata.positionData.GPSData.longitude = Utils.xmpExifGpsCoordinateToDecimalDegrees(
|
||||
(sidecarData as SideCar).exif.GPSLongitude
|
||||
);
|
||||
metadata.positionData.GPSData.latitude = Utils.xmpExifGpsCoordinateToDecimalDegrees(
|
||||
(sidecarData as SideCar).exif.GPSLatitude
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.silly(LOG_TAG, 'Error loading sidecar metadata for : ' + fullPath);
|
||||
Logger.silly(err);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Logger.error(LOG_TAG, 'Error during reading photo: ' + fullPath);
|
||||
console.error(err);
|
||||
return MetadataLoader.EMPTY_METADATA;
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(LOG_TAG, 'Error during reading photo: ' + fullPath);
|
||||
console.error(err);
|
||||
return MetadataLoader.EMPTY_METADATA;
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
@ -141,6 +141,22 @@ export class Utils {
|
||||
return Date.parse(formattedTimestamp);
|
||||
}
|
||||
|
||||
//function to extract offset string from timestamp string, returns undefined if timestamp does not contain offset
|
||||
static timestampToOffsetString(timestamp: string) {
|
||||
try {
|
||||
const idx = timestamp.indexOf("+");
|
||||
if (idx > 0) {
|
||||
return timestamp.substring(idx, timestamp.length);
|
||||
}
|
||||
if (timestamp.indexOf("Z") > 0) {
|
||||
return '+00:00';
|
||||
}
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
//function to calculate offset from exif.exif.gpsTimeStamp or exif.gps.GPSDateStamp + exif.gps.GPSTimestamp
|
||||
static getTimeOffsetByGPSStamp(timestamp: string, gpsTimeStamp: string, gps: any) {
|
||||
let UTCTimestamp = gpsTimeStamp;
|
||||
@ -383,11 +399,14 @@ export class Utils {
|
||||
}
|
||||
|
||||
public static xmpExifGpsCoordinateToDecimalDegrees(text: string): number {
|
||||
if (!text) {
|
||||
return undefined;
|
||||
}
|
||||
const parts = text.match(/^([0-9]+),([0-9.]+)([EWNS])$/);
|
||||
const degrees: number = parseInt(parts[1], 10);
|
||||
const minutes: number = parseFloat(parts[2]);
|
||||
const sign = (parts[3] === "N" || parts[3] === "E") ? 1 : -1;
|
||||
return sign * (degrees + (minutes / 60.0))
|
||||
return (sign * (degrees + (minutes / 60.0)))
|
||||
}
|
||||
|
||||
|
||||
|
@ -29,34 +29,6 @@ export interface MediaDimension {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface SideCar {
|
||||
exif?: SideCarExif;
|
||||
dc?: SideCarDc;
|
||||
xmp?: SideCarXmp;
|
||||
photoshop?: SideCarPhotoshop;
|
||||
}
|
||||
|
||||
export interface SideCarExif {
|
||||
GPSLatitude?: string;
|
||||
GPSLongitude?: string;
|
||||
}
|
||||
|
||||
export interface SideCarDc {
|
||||
subject?: string[];
|
||||
}
|
||||
|
||||
export interface SideCarXmp {
|
||||
Rating?: RatingTypes;
|
||||
CreateDate?: string;
|
||||
ModifyDate?: string;
|
||||
}
|
||||
|
||||
export interface SideCarPhotoshop {
|
||||
// Corresponds to Exif.Photo.DateTimeOriginal. No corresponding key exists in
|
||||
// the xmp namespace!
|
||||
DateCreated?: string;
|
||||
}
|
||||
|
||||
export const MediaDTOUtils = {
|
||||
hasPositionData: (media: MediaDTO): boolean => {
|
||||
return (
|
||||
|
@ -17,7 +17,6 @@
|
||||
},
|
||||
"keywords": [
|
||||
],
|
||||
"rating": 0,
|
||||
"faces": [
|
||||
{
|
||||
"box": {
|
||||
|
@ -16,7 +16,6 @@
|
||||
},
|
||||
"keywords": [
|
||||
],
|
||||
"rating": 0,
|
||||
"faces": [
|
||||
{
|
||||
"box": {
|
||||
|
@ -20,8 +20,8 @@
|
||||
],
|
||||
"positionData": {
|
||||
"GPSData": {
|
||||
"latitude": 50.08958748333333,
|
||||
"longitude": 14.397409516666666
|
||||
"latitude": 50.089587,
|
||||
"longitude": 14.39741
|
||||
}
|
||||
},
|
||||
"rating": 3
|
||||
|
@ -21,8 +21,8 @@
|
||||
],
|
||||
"positionData": {
|
||||
"GPSData": {
|
||||
"latitude": 50.08958748333333,
|
||||
"longitude": 14.397409516666666
|
||||
"latitude": 50.089587,
|
||||
"longitude": 14.39741
|
||||
}
|
||||
},
|
||||
"rating": 3
|
||||
|
@ -11,5 +11,11 @@
|
||||
"keywords": [
|
||||
"Travel"
|
||||
],
|
||||
"rating": 3
|
||||
"rating": 3,
|
||||
"positionData": {
|
||||
"GPSData": {
|
||||
"latitude": 50.089587,
|
||||
"longitude": 14.39741
|
||||
}
|
||||
}
|
||||
}
|
@ -11,5 +11,11 @@
|
||||
"keywords": [
|
||||
"Travel"
|
||||
],
|
||||
"rating": 3
|
||||
"rating": 3,
|
||||
"positionData": {
|
||||
"GPSData": {
|
||||
"latitude": 50.089587,
|
||||
"longitude": 14.39741
|
||||
}
|
||||
}
|
||||
}
|
@ -21,8 +21,8 @@
|
||||
],
|
||||
"positionData": {
|
||||
"GPSData": {
|
||||
"latitude": 50.08958748333333,
|
||||
"longitude": 14.397409516666666
|
||||
"latitude": 50.089587,
|
||||
"longitude": 14.39741
|
||||
}
|
||||
},
|
||||
"rating": 3
|
||||
|
BIN
test/backend/assets/sidecar/Chars.jpg
Normal file
BIN
test/backend/assets/sidecar/Chars.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
172
test/backend/assets/sidecar/Chars.jpg.xmp
Normal file
172
test/backend/assets/sidecar/Chars.jpg.xmp
Normal file
@ -0,0 +1,172 @@
|
||||
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
||||
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.69'>
|
||||
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:Iptc4xmpCore='http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'>
|
||||
<Iptc4xmpCore:CountryCode>SWE</Iptc4xmpCore:CountryCode>
|
||||
<Iptc4xmpCore:Location>Jönköping</Iptc4xmpCore:Location>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
<rdf:li>æÆøØåÅéÉüÜäÄöÖïÏñÑ</rdf:li>
|
||||
<rdf:li>abcdefghijklmnopqrstuvwxyz</rdf:li>
|
||||
<rdf:li>abcdefghijklmnopqrstuvwxyz</rdf:li>
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:MP='http://ns.microsoft.com/photo/1.2/'
|
||||
xmlns:MPRI='http://ns.microsoft.com/photo/1.2/t/RegionInfo#'
|
||||
xmlns:MPReg='http://ns.microsoft.com/photo/1.2/t/Region#'>
|
||||
<MP:RegionInfo rdf:parseType='Resource'>
|
||||
<MPRI:Regions>
|
||||
<rdf:Bag>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<MPReg:PersonDisplayName>æÆøØåÅéÉüÜäÄöÖïÏñÑ</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>0.294791666666667, 0.13287037037037, 0.107291666666667, 0.237962962962963</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<MPReg:PersonDisplayName>abcdefghijklmnopqrstuvwxyz</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>0.451041666666667, 0.13287037037037, 0.110416666666667, 0.24537037037037</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<MPReg:PersonDisplayName>abcdefghijklmnopqrstuvwxyz</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>0.605208333333333, 0.138425925925926, 0.110416666666667, 0.24537037037037</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
</rdf:Bag>
|
||||
</MPRI:Regions>
|
||||
</MP:RegionInfo>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:dc='http://purl.org/dc/elements/1.1/'>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>æÆøØåÅéÉüÜäÄöÖïÏñÑ</rdf:li>
|
||||
<rdf:li>abcdefghijklmnopqrstuvwxyz</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
<exif:ColorSpace>65535</exif:ColorSpace>
|
||||
<exif:ComponentsConfiguration>
|
||||
<rdf:Seq>
|
||||
<rdf:li>1</rdf:li>
|
||||
<rdf:li>2</rdf:li>
|
||||
<rdf:li>3</rdf:li>
|
||||
<rdf:li>0</rdf:li>
|
||||
</rdf:Seq>
|
||||
</exif:ComponentsConfiguration>
|
||||
<exif:DateTimeOriginal>2024-01-31T00:02:07+01:00</exif:DateTimeOriginal>
|
||||
<exif:ExifVersion>0232</exif:ExifVersion>
|
||||
<exif:FlashpixVersion>0100</exif:FlashpixVersion>
|
||||
<exif:GPSAltitude>102/1</exif:GPSAltitude>
|
||||
<exif:GPSAltitudeRef>0</exif:GPSAltitudeRef>
|
||||
<exif:GPSLatitude>57,46.8417528N</exif:GPSLatitude>
|
||||
<exif:GPSLongitude>14,9.7753146E</exif:GPSLongitude>
|
||||
<exif:GPSMapDatum>WGS-84</exif:GPSMapDatum>
|
||||
<exif:GPSTimeStamp>1899-12-30T01:00:14Z</exif:GPSTimeStamp>
|
||||
<exif:GPSVersionID>2.2.0.0</exif:GPSVersionID>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:mwg-rs='http://www.metadataworkinggroup.com/schemas/regions/'
|
||||
xmlns:stArea='http://ns.adobe.com/xmp/sType/Area#'
|
||||
xmlns:stDim='http://ns.adobe.com/xap/1.0/sType/Dimensions#'>
|
||||
<mwg-rs:Regions rdf:parseType='Resource'>
|
||||
<mwg-rs:AppliedToDimensions rdf:parseType='Resource'>
|
||||
<stDim:h>1080</stDim:h>
|
||||
<stDim:unit>pixel</stDim:unit>
|
||||
<stDim:w>1920</stDim:w>
|
||||
</mwg-rs:AppliedToDimensions>
|
||||
<mwg-rs:RegionList>
|
||||
<rdf:Bag>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<mwg-rs:Area rdf:parseType='Resource'>
|
||||
<stArea:h>0.237962962962963</stArea:h>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
<stArea:w>0.107291666666667</stArea:w>
|
||||
<stArea:x>0.3484375</stArea:x>
|
||||
<stArea:y>0.251851851851852</stArea:y>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>æÆøØåÅéÉüÜäÄöÖïÏñÑ</mwg-rs:Name>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<mwg-rs:Area rdf:parseType='Resource'>
|
||||
<stArea:h>0.24537037037037</stArea:h>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
<stArea:w>0.110416666666667</stArea:w>
|
||||
<stArea:x>0.50625</stArea:x>
|
||||
<stArea:y>0.255555555555556</stArea:y>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>abcdefghijklmnopqrstuvwxyz</mwg-rs:Name>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<mwg-rs:Area rdf:parseType='Resource'>
|
||||
<stArea:h>0.24537037037037</stArea:h>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
<stArea:w>0.110416666666667</stArea:w>
|
||||
<stArea:x>0.660416666666667</stArea:x>
|
||||
<stArea:y>0.261111111111111</stArea:y>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>abcdefghijklmnopqrstuvwxyz</mwg-rs:Name>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
</rdf:Bag>
|
||||
</mwg-rs:RegionList>
|
||||
</mwg-rs:Regions>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:pdf='http://ns.adobe.com/pdf/1.3/'>
|
||||
<pdf:Keywords>æÆøØåÅéÉüÜäÄöÖïÏñÑ, abcdefghijklmnopqrstuvwxyz</pdf:Keywords>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:photoshop='http://ns.adobe.com/photoshop/1.0/'>
|
||||
<photoshop:City>Jönköping</photoshop:City>
|
||||
<photoshop:Country>Sverige</photoshop:Country>
|
||||
<photoshop:State>Jönköping</photoshop:State>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
||||
<tiff:BitsPerSample>
|
||||
<rdf:Seq>
|
||||
<rdf:li>8</rdf:li>
|
||||
</rdf:Seq>
|
||||
</tiff:BitsPerSample>
|
||||
<tiff:ImageLength>1080</tiff:ImageLength>
|
||||
<tiff:ImageWidth>1920</tiff:ImageWidth>
|
||||
<tiff:ResolutionUnit>2</tiff:ResolutionUnit>
|
||||
<tiff:XResolution>300/1</tiff:XResolution>
|
||||
<tiff:YCbCrPositioning>1</tiff:YCbCrPositioning>
|
||||
<tiff:YCbCrSubSampling>
|
||||
<rdf:Seq>
|
||||
<rdf:li>1</rdf:li>
|
||||
<rdf:li>1</rdf:li>
|
||||
</rdf:Seq>
|
||||
</tiff:YCbCrSubSampling>
|
||||
<tiff:YResolution>300/1</tiff:YResolution>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2024-01-31T00:02:07+01:00</xmp:CreateDate>
|
||||
<xmp:CreatorTool>Tag That Photo</xmp:CreatorTool>
|
||||
<xmp:MetadataDate>2024-01-30T23:02:06Z</xmp:MetadataDate>
|
||||
<xmp:ModifyDate>2024-01-31T00:02:07+01:00</xmp:ModifyDate>
|
||||
<xmp:Rating>-1</xmp:Rating>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
<?xpacket end='w'?>
|
49
test/backend/assets/sidecar/Chars.json
Normal file
49
test/backend/assets/sidecar/Chars.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"size": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
},
|
||||
"creationDate": 1706655727000,
|
||||
"creationDateOffset": "+01:00",
|
||||
"fileSize": 101948,
|
||||
"positionData": {
|
||||
"GPSData": {
|
||||
"longitude": 14.162922,
|
||||
"latitude": 57.780696
|
||||
},
|
||||
"country": "Sverige",
|
||||
"state": "Jönköping",
|
||||
"city": "Jönköping"
|
||||
},
|
||||
"keywords": [
|
||||
],
|
||||
"faces": [
|
||||
{
|
||||
"box": {
|
||||
"width": 206,
|
||||
"height": 257,
|
||||
"left": 566,
|
||||
"top": 144
|
||||
},
|
||||
"name": "æÆøØåÅéÉüÜäÄöÖïÏñÑ"
|
||||
},
|
||||
{
|
||||
"name": "abcdefghijklmnopqrstuvwxyz",
|
||||
"box": {
|
||||
"width": 212,
|
||||
"height": 265,
|
||||
"left": 866,
|
||||
"top": 144
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "abcdefghijklmnopqrstuvwxyz",
|
||||
"box": {
|
||||
"width": 212,
|
||||
"height": 265,
|
||||
"left": 1162,
|
||||
"top": 150
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
BIN
test/backend/assets/sidecar/Chars_exiftool.jpg
Normal file
BIN
test/backend/assets/sidecar/Chars_exiftool.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
173
test/backend/assets/sidecar/Chars_exiftool.jpg.xmp
Normal file
173
test/backend/assets/sidecar/Chars_exiftool.jpg.xmp
Normal file
@ -0,0 +1,173 @@
|
||||
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
||||
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.69'>
|
||||
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:Iptc4xmpCore='http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'>
|
||||
<Iptc4xmpCore:CountryCode>SWE</Iptc4xmpCore:CountryCode>
|
||||
<Iptc4xmpCore:Location>Jönköping</Iptc4xmpCore:Location>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
<rdf:li>æÆøØåÅéÉüÜäÄöÖïÏñÑ</rdf:li>
|
||||
<rdf:li>abcdefghijklmnopqrstuvwxyz</rdf:li>
|
||||
<rdf:li>abcdefghijklmnopqrstuvwxyz</rdf:li>
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:MP='http://ns.microsoft.com/photo/1.2/'
|
||||
xmlns:MPRI='http://ns.microsoft.com/photo/1.2/t/RegionInfo#'
|
||||
xmlns:MPReg='http://ns.microsoft.com/photo/1.2/t/Region#'>
|
||||
<MP:RegionInfo rdf:parseType='Resource'>
|
||||
<MPRI:Regions>
|
||||
<rdf:Bag>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<MPReg:PersonDisplayName>æÆøØåÅéÉüÜäÄöÖïÏñÑ</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>0.294791666666667, 0.13287037037037, 0.107291666666667, 0.237962962962963</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<MPReg:PersonDisplayName>abcdefghijklmnopqrstuvwxyz</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>0.451041666666667, 0.13287037037037, 0.110416666666667, 0.24537037037037</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<MPReg:PersonDisplayName>abcdefghijklmnopqrstuvwxyz</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>0.605208333333333, 0.138425925925926, 0.110416666666667, 0.24537037037037</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
</rdf:Bag>
|
||||
</MPRI:Regions>
|
||||
</MP:RegionInfo>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:dc='http://purl.org/dc/elements/1.1/'>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>æÆøØåÅéÉüÜäÄöÖïÏñÑ</rdf:li>
|
||||
<rdf:li>abcdefghijklmnopqrstuvwxyz</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
<exif:ColorSpace>65535</exif:ColorSpace>
|
||||
<exif:ComponentsConfiguration>
|
||||
<rdf:Seq>
|
||||
<rdf:li>1</rdf:li>
|
||||
<rdf:li>2</rdf:li>
|
||||
<rdf:li>3</rdf:li>
|
||||
<rdf:li>0</rdf:li>
|
||||
</rdf:Seq>
|
||||
</exif:ComponentsConfiguration>
|
||||
<exif:DateTimeOriginal>2024-01-30T12:00:00+01:00</exif:DateTimeOriginal>
|
||||
<exif:ExifVersion>0232</exif:ExifVersion>
|
||||
<exif:FlashpixVersion>0100</exif:FlashpixVersion>
|
||||
<exif:GPSAltitude>102/1</exif:GPSAltitude>
|
||||
<exif:GPSAltitudeRef>0</exif:GPSAltitudeRef>
|
||||
<exif:GPSLatitude>57,46.8417528N</exif:GPSLatitude>
|
||||
<exif:GPSLongitude>14,9.7753146E</exif:GPSLongitude>
|
||||
<exif:GPSMapDatum>WGS-84</exif:GPSMapDatum>
|
||||
<exif:GPSTimeStamp>1899-12-30T01:00:14Z</exif:GPSTimeStamp>
|
||||
<exif:GPSVersionID>2.2.0.0</exif:GPSVersionID>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:mwg-rs='http://www.metadataworkinggroup.com/schemas/regions/'
|
||||
xmlns:stArea='http://ns.adobe.com/xmp/sType/Area#'
|
||||
xmlns:stDim='http://ns.adobe.com/xap/1.0/sType/Dimensions#'>
|
||||
<mwg-rs:Regions rdf:parseType='Resource'>
|
||||
<mwg-rs:AppliedToDimensions rdf:parseType='Resource'>
|
||||
<stDim:h>1080</stDim:h>
|
||||
<stDim:unit>pixel</stDim:unit>
|
||||
<stDim:w>1920</stDim:w>
|
||||
</mwg-rs:AppliedToDimensions>
|
||||
<mwg-rs:RegionList>
|
||||
<rdf:Bag>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<mwg-rs:Area rdf:parseType='Resource'>
|
||||
<stArea:h>0.237962962962963</stArea:h>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
<stArea:w>0.107291666666667</stArea:w>
|
||||
<stArea:x>0.3484375</stArea:x>
|
||||
<stArea:y>0.251851851851852</stArea:y>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>æÆøØåÅéÉüÜäÄöÖïÏñÑ</mwg-rs:Name>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<mwg-rs:Area rdf:parseType='Resource'>
|
||||
<stArea:h>0.24537037037037</stArea:h>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
<stArea:w>0.110416666666667</stArea:w>
|
||||
<stArea:x>0.50625</stArea:x>
|
||||
<stArea:y>0.255555555555556</stArea:y>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>abcdefghijklmnopqrstuvwxyz</mwg-rs:Name>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
<rdf:li rdf:parseType='Resource'>
|
||||
<mwg-rs:Area rdf:parseType='Resource'>
|
||||
<stArea:h>0.24537037037037</stArea:h>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
<stArea:w>0.110416666666667</stArea:w>
|
||||
<stArea:x>0.660416666666667</stArea:x>
|
||||
<stArea:y>0.261111111111111</stArea:y>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>abcdefghijklmnopqrstuvwxyz</mwg-rs:Name>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
</rdf:Bag>
|
||||
</mwg-rs:RegionList>
|
||||
</mwg-rs:Regions>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:pdf='http://ns.adobe.com/pdf/1.3/'>
|
||||
<pdf:Keywords>æÆøØåÅéÉüÜäÄöÖïÏñÑ, abcdefghijklmnopqrstuvwxyz</pdf:Keywords>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:photoshop='http://ns.adobe.com/photoshop/1.0/'>
|
||||
<photoshop:City>Jönköping</photoshop:City>
|
||||
<photoshop:Country>Sverige</photoshop:Country>
|
||||
<photoshop:State>Jönköping</photoshop:State>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
||||
<tiff:BitsPerSample>
|
||||
<rdf:Seq>
|
||||
<rdf:li>8</rdf:li>
|
||||
</rdf:Seq>
|
||||
</tiff:BitsPerSample>
|
||||
<tiff:ImageLength>1080</tiff:ImageLength>
|
||||
<tiff:ImageWidth>1920</tiff:ImageWidth>
|
||||
<tiff:Orientation>1</tiff:Orientation>
|
||||
<tiff:ResolutionUnit>2</tiff:ResolutionUnit>
|
||||
<tiff:XResolution>300/1</tiff:XResolution>
|
||||
<tiff:YCbCrPositioning>1</tiff:YCbCrPositioning>
|
||||
<tiff:YCbCrSubSampling>
|
||||
<rdf:Seq>
|
||||
<rdf:li>1</rdf:li>
|
||||
<rdf:li>1</rdf:li>
|
||||
</rdf:Seq>
|
||||
</tiff:YCbCrSubSampling>
|
||||
<tiff:YResolution>300/1</tiff:YResolution>
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2024-01-30T12:00:00+01:00</xmp:CreateDate>
|
||||
<xmp:CreatorTool>Tag That Photo</xmp:CreatorTool>
|
||||
<xmp:MetadataDate>2024-01-30T23:02:06Z</xmp:MetadataDate>
|
||||
<xmp:ModifyDate>2024-01-30T12:00:00+01:00</xmp:ModifyDate>
|
||||
<xmp:Rating>-1</xmp:Rating>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
<?xpacket end='w'?>
|
49
test/backend/assets/sidecar/Chars_exiftool.json
Normal file
49
test/backend/assets/sidecar/Chars_exiftool.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"size": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
},
|
||||
"creationDate": 1706612400000,
|
||||
"creationDateOffset": "+01:00",
|
||||
"fileSize": 101948,
|
||||
"positionData": {
|
||||
"GPSData": {
|
||||
"longitude": 14.162922,
|
||||
"latitude": 57.780696
|
||||
},
|
||||
"country": "Sverige",
|
||||
"state": "Jönköping",
|
||||
"city": "Jönköping"
|
||||
},
|
||||
"keywords": [
|
||||
],
|
||||
"faces": [
|
||||
{
|
||||
"box": {
|
||||
"width": 206,
|
||||
"height": 257,
|
||||
"left": 566,
|
||||
"top": 144
|
||||
},
|
||||
"name": "æÆøØåÅéÉüÜäÄöÖïÏñÑ"
|
||||
},
|
||||
{
|
||||
"name": "abcdefghijklmnopqrstuvwxyz",
|
||||
"box": {
|
||||
"width": 212,
|
||||
"height": 265,
|
||||
"left": 866,
|
||||
"top": 144
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "abcdefghijklmnopqrstuvwxyz",
|
||||
"box": {
|
||||
"width": 212,
|
||||
"height": 265,
|
||||
"left": 1162,
|
||||
"top": 150
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
"bitRate": 1794127,
|
||||
"duration": 290,
|
||||
"creationDate": 1542482851000,
|
||||
"creationDateOffset": "+01:00",
|
||||
"fileSize": 65073,
|
||||
"fps": 40000,
|
||||
"keywords": [
|
||||
|
@ -6,6 +6,7 @@
|
||||
"bitRate": 1794127,
|
||||
"duration": 290,
|
||||
"creationDate": 1542482851000,
|
||||
"creationDateOffset": "+01:00",
|
||||
"fileSize": 65073,
|
||||
"fps": 40000,
|
||||
"keywords": [
|
||||
|
@ -6,6 +6,7 @@
|
||||
"bitRate": 1794127,
|
||||
"duration": 290,
|
||||
"creationDate": 1542482851000,
|
||||
"creationDateOffset": "+01:00",
|
||||
"fileSize": 65073,
|
||||
"fps": 40000,
|
||||
"keywords": [
|
||||
|
@ -4,6 +4,7 @@
|
||||
"height": 5
|
||||
},
|
||||
"creationDate": 1542482851000,
|
||||
"creationDateOffset": "+01:00",
|
||||
"fileSize": 1430,
|
||||
"keywords": [
|
||||
"first",
|
||||
|
@ -4,6 +4,7 @@
|
||||
"height": 5
|
||||
},
|
||||
"creationDate": 1542482851000,
|
||||
"creationDateOffset": "+01:00",
|
||||
"fileSize": 1430,
|
||||
"keywords": [
|
||||
"first",
|
||||
|
@ -4,6 +4,7 @@
|
||||
"height": 5
|
||||
},
|
||||
"creationDate": 1542482851000,
|
||||
"creationDateOffset": "+01:00",
|
||||
"fileSize": 1430,
|
||||
"keywords": [
|
||||
"first"
|
||||
|
BIN
test/tmp/sqlite.db-journal
Normal file
BIN
test/tmp/sqlite.db-journal
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user