mirror of
https://github.com/xuthus83/pigallery2.git
synced 2025-01-14 14:43:17 +08:00
adding tests for metadata loading
This commit is contained in:
parent
741024e962
commit
bda5fef910
@ -1,22 +1,15 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {DirectoryDTO} from '../../../common/entities/DirectoryDTO';
|
||||
import {PhotoDTO, PhotoMetadata} from '../../../common/entities/PhotoDTO';
|
||||
import {Logger} from '../../Logger';
|
||||
import {IptcParser} from 'ts-node-iptc';
|
||||
import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser';
|
||||
import {FfprobeData} from 'fluent-ffmpeg';
|
||||
import {PhotoDTO} from '../../../common/entities/PhotoDTO';
|
||||
import {ProjectPath} from '../../ProjectPath';
|
||||
import {Config} from '../../../common/config/private/Config';
|
||||
import {VideoDTO, VideoMetadata} from '../../../common/entities/VideoDTO';
|
||||
import {FFmpegFactory} from '../FFmpegFactory';
|
||||
import {VideoDTO} from '../../../common/entities/VideoDTO';
|
||||
import {FileDTO} from '../../../common/entities/FileDTO';
|
||||
import * as sizeOf from 'image-size';
|
||||
import {MetadataLoader} from './MetadataLoader';
|
||||
|
||||
const LOG_TAG = '[DiskManagerTask]';
|
||||
|
||||
const ffmpeg = FFmpegFactory.get();
|
||||
|
||||
export class DiskMangerWorker {
|
||||
|
||||
private static readonly SupportedEXT = {
|
||||
@ -98,7 +91,7 @@ export class DiskMangerWorker {
|
||||
directory.media.push(<PhotoDTO>{
|
||||
name: file,
|
||||
directory: null,
|
||||
metadata: await DiskMangerWorker.loadPhotoMetadata(fullFilePath)
|
||||
metadata: await MetadataLoader.loadPhotoMetadata(fullFilePath)
|
||||
});
|
||||
|
||||
if (maxPhotos != null && directory.media.length > maxPhotos) {
|
||||
@ -109,7 +102,7 @@ export class DiskMangerWorker {
|
||||
directory.media.push(<VideoDTO>{
|
||||
name: file,
|
||||
directory: null,
|
||||
metadata: await DiskMangerWorker.loadVideoMetadata(fullFilePath)
|
||||
metadata: await MetadataLoader.loadVideoMetadata(fullFilePath)
|
||||
});
|
||||
|
||||
} else if (photosOnly === false && Config.Client.MetaFile.enabled === true &&
|
||||
@ -132,155 +125,4 @@ export class DiskMangerWorker {
|
||||
|
||||
}
|
||||
|
||||
public static loadVideoMetadata(fullPath: string): Promise<VideoMetadata> {
|
||||
return new Promise<VideoMetadata>((resolve, reject) => {
|
||||
const metadata: VideoMetadata = {
|
||||
size: {
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
bitRate: 0,
|
||||
duration: 0,
|
||||
creationDate: 0,
|
||||
fileSize: 0
|
||||
};
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
metadata.fileSize = stat.size;
|
||||
} catch (err) {
|
||||
}
|
||||
ffmpeg(fullPath).ffprobe((err: any, data: FfprobeData) => {
|
||||
if (!!err || data === null) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (!data.streams[0]) {
|
||||
return resolve(metadata);
|
||||
}
|
||||
|
||||
try {
|
||||
for (let i = 0; i < data.streams.length; i++) {
|
||||
if (data.streams[i].width) {
|
||||
metadata.size.width = data.streams[i].width;
|
||||
metadata.size.height = data.streams[i].height;
|
||||
|
||||
metadata.duration = Math.floor(data.streams[i].duration * 1000);
|
||||
metadata.bitRate = parseInt(data.streams[i].bit_rate, 10) || null;
|
||||
metadata.creationDate = Date.parse(data.streams[i].tags.creation_time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
return resolve(metadata);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
|
||||
return new Promise<PhotoMetadata>((resolve, reject) => {
|
||||
const fd = fs.openSync(fullPath, 'r');
|
||||
|
||||
const data = Buffer.allocUnsafe(Config.Server.photoMetadataSize);
|
||||
fs.read(fd, data, 0, Config.Server.photoMetadataSize, 0, (err) => {
|
||||
if (err) {
|
||||
fs.closeSync(fd);
|
||||
return reject({file: fullPath, error: err});
|
||||
}
|
||||
const metadata: PhotoMetadata = {
|
||||
keywords: [],
|
||||
cameraData: {},
|
||||
positionData: null,
|
||||
size: {width: 1, height: 1},
|
||||
caption: null,
|
||||
orientation: OrientationTypes.TOP_LEFT,
|
||||
creationDate: 0,
|
||||
fileSize: 0
|
||||
};
|
||||
try {
|
||||
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
metadata.fileSize = stat.size;
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
try {
|
||||
const exif = ExifParserFactory.create(data).parse();
|
||||
metadata.cameraData = {
|
||||
ISO: exif.tags.ISO,
|
||||
model: exif.tags.Model,
|
||||
make: exif.tags.Make,
|
||||
fStop: exif.tags.FNumber,
|
||||
exposure: exif.tags.ExposureTime,
|
||||
focalLength: exif.tags.FocalLength,
|
||||
lens: exif.tags.LensModel,
|
||||
};
|
||||
if (!isNaN(exif.tags.GPSLatitude) || exif.tags.GPSLongitude || exif.tags.GPSAltitude) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.GPSData = {
|
||||
latitude: exif.tags.GPSLatitude,
|
||||
longitude: exif.tags.GPSLongitude,
|
||||
altitude: exif.tags.GPSAltitude
|
||||
};
|
||||
}
|
||||
|
||||
if (exif.tags.CreateDate || exif.tags.DateTimeOriginal || exif.tags.ModifyDate) {
|
||||
metadata.creationDate = exif.tags.CreateDate || exif.tags.DateTimeOriginal || exif.tags.ModifyDate;
|
||||
}
|
||||
|
||||
if (exif.tags.Orientation) {
|
||||
metadata.orientation = exif.tags.Orientation;
|
||||
}
|
||||
|
||||
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 {
|
||||
const info = sizeOf(fullPath);
|
||||
metadata.size = {width: info.width, height: info.height};
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.debug(LOG_TAG, 'Error parsing exif', fullPath, err);
|
||||
try {
|
||||
const info = sizeOf(fullPath);
|
||||
metadata.size = {width: info.width, height: info.height};
|
||||
} catch (e) {
|
||||
metadata.size = {width: 1, height: 1};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const iptcData = IptcParser.parse(data);
|
||||
if (iptcData.country_or_primary_location_name || iptcData.province_or_state || iptcData.city) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.country = iptcData.country_or_primary_location_name;
|
||||
metadata.positionData.state = iptcData.province_or_state;
|
||||
metadata.positionData.city = iptcData.city;
|
||||
}
|
||||
if (iptcData.caption) {
|
||||
metadata.caption = iptcData.caption.replace(/\0/g, '').trim();
|
||||
}
|
||||
metadata.keywords = iptcData.keywords || [];
|
||||
metadata.creationDate = <number>(iptcData.date_time ? iptcData.date_time.getTime() : metadata.creationDate);
|
||||
|
||||
} catch (err) {
|
||||
// Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err);
|
||||
}
|
||||
|
||||
metadata.creationDate = metadata.creationDate || 0;
|
||||
|
||||
fs.closeSync(fd);
|
||||
return resolve(metadata);
|
||||
} catch (err) {
|
||||
fs.closeSync(fd);
|
||||
return reject({file: fullPath, error: err});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
170
backend/model/threading/MetadataLoader.ts
Normal file
170
backend/model/threading/MetadataLoader.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import {VideoMetadata} from '../../../common/entities/VideoDTO';
|
||||
import {PhotoMetadata} from '../../../common/entities/PhotoDTO';
|
||||
import {Config} from '../../../common/config/private/Config';
|
||||
import {Logger} from '../../Logger';
|
||||
import * as fs from 'fs';
|
||||
import * as sizeOf from 'image-size';
|
||||
import {OrientationTypes, ExifParserFactory} from 'ts-exif-parser';
|
||||
import {IptcParser} from 'ts-node-iptc';
|
||||
import {FFmpegFactory} from '../FFmpegFactory';
|
||||
import {FfprobeData} from 'fluent-ffmpeg';
|
||||
|
||||
const LOG_TAG = '[MetadataLoader]';
|
||||
const ffmpeg = FFmpegFactory.get();
|
||||
|
||||
export class MetadataLoader {
|
||||
|
||||
public static loadVideoMetadata(fullPath: string): Promise<VideoMetadata> {
|
||||
return new Promise<VideoMetadata>((resolve, reject) => {
|
||||
const metadata: VideoMetadata = {
|
||||
size: {
|
||||
width: 1,
|
||||
height: 1
|
||||
},
|
||||
bitRate: 0,
|
||||
duration: 0,
|
||||
creationDate: 0,
|
||||
fileSize: 0
|
||||
};
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
metadata.fileSize = stat.size;
|
||||
} catch (err) {
|
||||
}
|
||||
ffmpeg(fullPath).ffprobe((err: any, data: FfprobeData) => {
|
||||
if (!!err || data === null) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (!data.streams[0]) {
|
||||
return resolve(metadata);
|
||||
}
|
||||
|
||||
try {
|
||||
for (let i = 0; i < data.streams.length; i++) {
|
||||
if (data.streams[i].width) {
|
||||
metadata.size.width = data.streams[i].width;
|
||||
metadata.size.height = data.streams[i].height;
|
||||
|
||||
metadata.duration = Math.floor(data.streams[i].duration * 1000);
|
||||
metadata.bitRate = parseInt(data.streams[i].bit_rate, 10) || null;
|
||||
metadata.creationDate = Date.parse(data.streams[i].tags.creation_time);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
return resolve(metadata);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
|
||||
return new Promise<PhotoMetadata>((resolve, reject) => {
|
||||
const fd = fs.openSync(fullPath, 'r');
|
||||
|
||||
const data = Buffer.allocUnsafe(Config.Server.photoMetadataSize);
|
||||
fs.read(fd, data, 0, Config.Server.photoMetadataSize, 0, (err) => {
|
||||
if (err) {
|
||||
fs.closeSync(fd);
|
||||
return reject({file: fullPath, error: err});
|
||||
}
|
||||
const metadata: PhotoMetadata = {
|
||||
size: {width: 1, height: 1},
|
||||
orientation: OrientationTypes.TOP_LEFT,
|
||||
creationDate: 0,
|
||||
fileSize: 0
|
||||
};
|
||||
try {
|
||||
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
metadata.fileSize = stat.size;
|
||||
metadata.creationDate = stat.ctime.getTime();
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
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) {
|
||||
metadata.cameraData = {
|
||||
ISO: exif.tags.ISO,
|
||||
model: exif.tags.Model,
|
||||
make: exif.tags.Make,
|
||||
fStop: exif.tags.FNumber,
|
||||
exposure: exif.tags.ExposureTime,
|
||||
focalLength: exif.tags.FocalLength,
|
||||
lens: exif.tags.LensModel,
|
||||
};
|
||||
}
|
||||
if (!isNaN(exif.tags.GPSLatitude) || exif.tags.GPSLongitude || exif.tags.GPSAltitude) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.GPSData = {
|
||||
latitude: exif.tags.GPSLatitude,
|
||||
longitude: exif.tags.GPSLongitude,
|
||||
altitude: exif.tags.GPSAltitude
|
||||
};
|
||||
}
|
||||
|
||||
if (exif.tags.CreateDate || exif.tags.DateTimeOriginal || exif.tags.ModifyDate) {
|
||||
metadata.creationDate = exif.tags.CreateDate || exif.tags.DateTimeOriginal || exif.tags.ModifyDate;
|
||||
}
|
||||
|
||||
if (exif.tags.Orientation) {
|
||||
metadata.orientation = exif.tags.Orientation;
|
||||
}
|
||||
|
||||
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 {
|
||||
const info = sizeOf(fullPath);
|
||||
metadata.size = {width: info.width, height: info.height};
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.debug(LOG_TAG, 'Error parsing exif', fullPath, err);
|
||||
try {
|
||||
const info = sizeOf(fullPath);
|
||||
metadata.size = {width: info.width, height: info.height};
|
||||
} catch (e) {
|
||||
metadata.size = {width: 1, height: 1};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const iptcData = IptcParser.parse(data);
|
||||
if (iptcData.country_or_primary_location_name || iptcData.province_or_state || iptcData.city) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.country = iptcData.country_or_primary_location_name.replace(/\0/g, '').trim();
|
||||
metadata.positionData.state = iptcData.province_or_state.replace(/\0/g, '').trim();
|
||||
metadata.positionData.city = iptcData.city.replace(/\0/g, '').trim();
|
||||
}
|
||||
if (iptcData.caption) {
|
||||
metadata.caption = iptcData.caption.replace(/\0/g, '').trim();
|
||||
}
|
||||
metadata.keywords = iptcData.keywords || [];
|
||||
metadata.creationDate = <number>(iptcData.date_time ? iptcData.date_time.getTime() : metadata.creationDate);
|
||||
|
||||
} catch (err) {
|
||||
// Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err);
|
||||
}
|
||||
|
||||
metadata.creationDate = metadata.creationDate || 0;
|
||||
|
||||
fs.closeSync(fd);
|
||||
return resolve(metadata);
|
||||
} catch (err) {
|
||||
fs.closeSync(fd);
|
||||
return reject({file: fullPath, error: err});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -12,10 +12,10 @@ export interface PhotoDTO extends MediaDTO {
|
||||
}
|
||||
|
||||
export interface PhotoMetadata extends MediaMetadata {
|
||||
caption: string;
|
||||
keywords: Array<string>;
|
||||
cameraData: CameraMetadata;
|
||||
positionData: PositionMetaData;
|
||||
caption?: string;
|
||||
keywords?: string[];
|
||||
cameraData?: CameraMetadata;
|
||||
positionData?: PositionMetaData;
|
||||
orientation: OrientationTypes;
|
||||
size: MediaDimension;
|
||||
creationDate: number;
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
BIN
test/backend/unit/assets/test_png.png
Normal file
BIN
test/backend/unit/assets/test_png.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -11,10 +11,10 @@ describe('DiskMangerWorker', () => {
|
||||
Config.Server.imagesFolder = path.join(__dirname, '/../../assets');
|
||||
ProjectPath.ImageFolder = path.join(__dirname, '/../../assets');
|
||||
const dir = await DiskMangerWorker.scanDirectory('/');
|
||||
expect(dir.media.length).to.be.equals(1);
|
||||
expect(dir.media.length).to.be.equals(2);
|
||||
expect(dir.media[0].name).to.be.equals('test image öüóőúéáű-.,.jpg');
|
||||
expect((<PhotoDTO>dir.media[0]).metadata.keywords).to.deep.equals(['Berkley', 'USA', 'űáéúőóüö ŰÁÉÚŐÓÜÖ']);
|
||||
expect(dir.media[0].metadata.fileSize).to.deep.equals(62392);
|
||||
expect(dir.media[0].metadata.fileSize).to.deep.equals(62786);
|
||||
expect(dir.media[0].metadata.size).to.deep.equals({width: 140, height: 93});
|
||||
expect((<PhotoDTO>dir.media[0]).metadata.cameraData).to.deep.equals({
|
||||
ISO: 3200,
|
||||
@ -32,7 +32,7 @@ describe('DiskMangerWorker', () => {
|
||||
longitude: -122.25678,
|
||||
altitude: 102.4498997995992
|
||||
},
|
||||
country: 'mmóüöúőűáé ÓÜÖÚŐŰÁÉmm-.,|\\mm\u0000',
|
||||
country: 'mmóüöúőűáé ÓÜÖÚŐŰÁÉmm-.,|\\mm',
|
||||
state: 'óüöúőűáé ÓÜÖÚŐŰÁ',
|
||||
city: 'óüöúőűáé ÓÜÖÚŐŰÁ'
|
||||
});
|
||||
|
55
test/backend/unit/model/threading/MetaDataLoader.spec.ts
Normal file
55
test/backend/unit/model/threading/MetaDataLoader.spec.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import {expect} from 'chai';
|
||||
import {MetadataLoader} from '../../../../../backend/model/threading/MetadataLoader';
|
||||
import {Utils} from '../../../../../common/Utils';
|
||||
import * as path from 'path';
|
||||
|
||||
describe('MetadataLoader', () => {
|
||||
|
||||
it('should load png', async () => {
|
||||
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../assets/test_png.png'));
|
||||
expect(Utils.clone(data)).to.be.deep.equal(Utils.clone({
|
||||
creationDate: 1545342192328,
|
||||
fileSize: 2110,
|
||||
orientation: 1,
|
||||
size: {
|
||||
height: 26,
|
||||
width: 26
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it('should load jpg', async () => {
|
||||
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../assets/test image öüóőúéáű-.,.jpg'));
|
||||
expect(Utils.clone(data)).to.be.deep.equal(Utils.clone({
|
||||
size: {width: 140, height: 93},
|
||||
orientation: 1,
|
||||
caption: 'Test caption',
|
||||
creationDate: 1434018566000,
|
||||
fileSize: 62786,
|
||||
cameraData:
|
||||
{
|
||||
ISO: 3200,
|
||||
model: 'óüöúőűáé ÓÜÖÚŐŰÁÉ',
|
||||
make: 'Canon',
|
||||
fStop: 5.6,
|
||||
exposure: 0.00125,
|
||||
focalLength: 85,
|
||||
lens: 'EF-S15-85mm f/3.5-5.6 IS USM'
|
||||
},
|
||||
positionData:
|
||||
{
|
||||
GPSData:
|
||||
{
|
||||
latitude: 37.871093333333334,
|
||||
longitude: -122.25678,
|
||||
altitude: 102.4498997995992
|
||||
},
|
||||
country: 'mmóüöúőűáé ÓÜÖÚŐŰÁÉmm-.,|\\mm',
|
||||
state: 'óüöúőűáé ÓÜÖÚŐŰÁ',
|
||||
city: 'óüöúőűáé ÓÜÖÚŐŰÁ'
|
||||
},
|
||||
keywords: ['Berkley', 'USA', 'űáéúőóüö ŰÁÉÚŐÓÜÖ']
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user