From bda5fef910b0ab1259898de07920456513216654 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Thu, 20 Dec 2018 23:02:49 +0100 Subject: [PATCH] adding tests for metadata loading --- backend/model/threading/DiskMangerWorker.ts | 168 +---------------- backend/model/threading/MetadataLoader.ts | 170 ++++++++++++++++++ common/entities/PhotoDTO.ts | 8 +- .../unit/assets/test image öüóőúéáű-.,.jpg | Bin 62392 -> 62786 bytes test/backend/unit/assets/test_png.png | Bin 0 -> 2110 bytes .../model/threading/DiskMangerWorker.spec.ts | 6 +- .../model/threading/MetaDataLoader.spec.ts | 55 ++++++ 7 files changed, 237 insertions(+), 170 deletions(-) create mode 100644 backend/model/threading/MetadataLoader.ts create mode 100644 test/backend/unit/assets/test_png.png create mode 100644 test/backend/unit/model/threading/MetaDataLoader.spec.ts diff --git a/backend/model/threading/DiskMangerWorker.ts b/backend/model/threading/DiskMangerWorker.ts index 819407b..353eaa8 100644 --- a/backend/model/threading/DiskMangerWorker.ts +++ b/backend/model/threading/DiskMangerWorker.ts @@ -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({ 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({ 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 { - return new Promise((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 { - return new Promise((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 = (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}); - } - }); - } - ); - } } diff --git a/backend/model/threading/MetadataLoader.ts b/backend/model/threading/MetadataLoader.ts new file mode 100644 index 0000000..1aa25e6 --- /dev/null +++ b/backend/model/threading/MetadataLoader.ts @@ -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 { + return new Promise((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 { + return new Promise((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 = (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}); + } + }); + } + ); + } +} diff --git a/common/entities/PhotoDTO.ts b/common/entities/PhotoDTO.ts index b7710ea..0eb994b 100644 --- a/common/entities/PhotoDTO.ts +++ b/common/entities/PhotoDTO.ts @@ -12,10 +12,10 @@ export interface PhotoDTO extends MediaDTO { } export interface PhotoMetadata extends MediaMetadata { - caption: string; - keywords: Array; - cameraData: CameraMetadata; - positionData: PositionMetaData; + caption?: string; + keywords?: string[]; + cameraData?: CameraMetadata; + positionData?: PositionMetaData; orientation: OrientationTypes; size: MediaDimension; creationDate: number; diff --git a/test/backend/unit/assets/test image öüóőúéáű-.,.jpg b/test/backend/unit/assets/test image öüóőúéáű-.,.jpg index b6347849e9d7f69bdfd18350e81f3173283db1a1..4f838c4a9b754d25c1845de955fb5a9effee0d4c 100644 GIT binary patch delta 804 zcmdn-ocYiz<_Y4dRjw78X$%a$z6@Fn3=A9$0*uTI%s>_+0~?UwV_*WZd4UqQ82Q0$ zpeV-+MgcHe3CMoOCk?sL;Pha$2D90L z?3ngU1{SEUlnzxGTX@Ty*+5q?Fi0^lh}g`T2WHy<*V7Pm{|y-EDtOV1cfbu4zUUqxj9xj ziFxU^N)@^(scDI&IVDPV$l-0H4^p3#X$N5dHQMoVadL9n=p#%7vQkoulZ!IJF@$1+ zH-$DJn>%@_+0~?UwXJ7)dfh>+gi~?Y` z5|Dj@Q3$Gr5vWFrffcNtf#VgUG@Sj3Q3I+5Xd0s-Sg!_9?=MCpFk6Iyfm4am+78TN z2Xf5XGZ|PQ265VTsKVI7eRJjk9mBw2!@wXS_DY5UsFs5V$lnI!2W@=A!@=g9n3tc& zFj+oLezG=a3h$AoxiD7z%Ss{WIod5{-~n5D^s1gR*Lh{!rn zDNvR{CC!O(Z@Kr}-@W&{_xF7- zFC;h+ZRTi(LZQ$+E+-6G9ra_22{N|?9UMj$17%ns3w3dL#{wc4CHe9FP^k7|^EJ>I zkxk{?SS1Q&4eN)&w8&YFC>>z`7&u%ef>odbLiq`02@r-S0h6FG2o$D&G6?ygP=@&rA+Me>)&88Vj&Y7P2m&xayFDK z<{no-;m3m`5|5`O(uG*xeHb4#192dMU=X909Fr;;Y9@BwF9X@@)p#ss-2_fyV&5he z!wq0msIJx>45_&6BS}P zES5|cmPMhOCxGk=p%vAiW`y4TB>~OqD=Vn8#sa5e-f#7Bbiz3P7e2*mM$! z0091EI)~%$??(ej6bhR}r_eXCJgE`}rHRlcR{T4b^`}?{TLFQvOc5cI9b;lcK(Q3Y z1^Y)}(nvT84j|~uxse^D5T`-H0EJ9~S+55}{Jxk(mLJ)V5z4`|9`-K0lsyX|L118Q`qgKMAHE_TpX`#hn$;PZOU>4I7RyfYu~Z1hoEnL4oU zRL^Fe|0vb@49?-54Ro36>Jn=kpglPB>>Z7a`Ymri^S|I327zi@Igx}vTwcVZ$^ zotbHpk&*F2r^_7};I^|8WHQV0^701{9-vmK)6>&d&d!!pDm6hFeuz z^${Aa>F*D0ZfP+%bm&mW&6`cBAP8Prn4jNcWMtH>dp^VaZRz2M>h$!UXV1La!nm2J-x%u z?(Wb~_|Mg2V_~pDVeL3KK5kQf?i{F6eXYe@4tcn+(DL0b@{Xm#v-$a&J)`&TFMb%UCqr~Jlx&6QGqEx7Zen@d3tzw*t#Q=nf;Nf zvNCphbxn93Jng@bGXQSD;^d`X z_lCJMDm9O1I=Cho9vR_9HFb44O;y`exqCLgIwls^`*^ zgvn4tiPaR!t~4j8@A8xSfd6P|AC!Ms>g%w5dxPlv(b4+b5u&3<&pNJ-)cH2PY>EmH z_~ez8Sj98h6StbY$y93Z)RcpEXZGMA?`T|ZFtE*v*-xhSdP-ujdV|IBt zEJnDBd8B;tc%-fud)Uy&$0w&LN8Ws@cn7rw;pWVY;M{F%PfzR6(9jMx*60%u$)9{`O1dz{p5$ zeZ5P3pDl7BPf_rkz`$`@Zf@z~jr+d)_F=Ht4-n?L32(SxWj$Z94-O7)UT$q`qp-f6 znwbfVdh$yA=Jl^%YmYuY>2#v^zp5)P&c1cy*^dha#hsn>u{WB4qANYm(`YoBEiSX} lA^I-IFj>N-vuzHoLfG^D0X74!7}XP$pB=OQaU`+vPvPL2Qo literal 0 HcmV?d00001 diff --git a/test/backend/unit/model/threading/DiskMangerWorker.spec.ts b/test/backend/unit/model/threading/DiskMangerWorker.spec.ts index a4f3682..9c541d9 100644 --- a/test/backend/unit/model/threading/DiskMangerWorker.spec.ts +++ b/test/backend/unit/model/threading/DiskMangerWorker.spec.ts @@ -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((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((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: 'óüöúőűáé ÓÜÖÚŐŰÁ' }); diff --git a/test/backend/unit/model/threading/MetaDataLoader.spec.ts b/test/backend/unit/model/threading/MetaDataLoader.spec.ts new file mode 100644 index 0000000..92d82bd --- /dev/null +++ b/test/backend/unit/model/threading/MetaDataLoader.spec.ts @@ -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', 'űáéúőóüö ŰÁÉÚŐÓÜÖ'] + })); + }); + +});