diff --git a/backend/model/interfaces/IPersonManager.ts b/backend/model/interfaces/IPersonManager.ts index dd23adf9..2526a6c0 100644 --- a/backend/model/interfaces/IPersonManager.ts +++ b/backend/model/interfaces/IPersonManager.ts @@ -1,7 +1,10 @@ import {PersonEntry} from '../sql/enitites/PersonEntry'; +import {MediaDTO} from '../../../common/entities/MediaDTO'; export interface IPersonManager { get(name: string): Promise; saveAll(names: string[]): Promise; + + keywordsToPerson(media: MediaDTO[]): Promise; } diff --git a/backend/model/sql/IndexingManager.ts b/backend/model/sql/IndexingManager.ts index 1c32db43..9f54f21d 100644 --- a/backend/model/sql/IndexingManager.ts +++ b/backend/model/sql/IndexingManager.ts @@ -5,7 +5,7 @@ import {DiskManager} from '../DiskManger'; import {PhotoEntity} from './enitites/PhotoEntity'; import {Utils} from '../../../common/Utils'; import {FaceRegion, PhotoMetadata} from '../../../common/entities/PhotoDTO'; -import {Connection, Repository} from 'typeorm'; +import {Connection, Repository} from 'typeorm'; import {MediaEntity} from './enitites/MediaEntity'; import {MediaDTO} from '../../../common/entities/MediaDTO'; import {VideoEntity} from './enitites/VideoEntity'; @@ -14,10 +14,11 @@ import {FileDTO} from '../../../common/entities/FileDTO'; import {NotificationManager} from '../NotifocationManager'; import {FaceRegionEntry} from './enitites/FaceRegionEntry'; import {ObjectManagerRepository} from '../ObjectManagerRepository'; +import {IIndexingManager} from '../interfaces/IIndexingManager'; const LOG_TAG = '[IndexingManager]'; -export class IndexingManager { +export class IndexingManager implements IIndexingManager { private savingQueue: DirectoryDTO[] = []; private isSaving = false; diff --git a/backend/model/sql/PersonManager.ts b/backend/model/sql/PersonManager.ts index e13c8f43..a5fcf97c 100644 --- a/backend/model/sql/PersonManager.ts +++ b/backend/model/sql/PersonManager.ts @@ -1,6 +1,8 @@ import {IPersonManager} from '../interfaces/IPersonManager'; -import {PersonEntry} from './enitites/PersonEntry'; import {SQLConnection} from './SQLConnection'; +import {PersonEntry} from './enitites/PersonEntry'; +import {MediaDTO} from '../../../common/entities/MediaDTO'; +import {PhotoDTO} from '../../../common/entities/PhotoDTO'; const LOG_TAG = '[PersonManager]'; @@ -8,6 +10,38 @@ export class PersonManager implements IPersonManager { persons: PersonEntry[] = []; + async loadAll(): Promise { + const connection = await SQLConnection.getConnection(); + const personRepository = connection.getRepository(PersonEntry); + this.persons = await personRepository.find(); + + } + + // TODO dead code, remove it + async keywordsToPerson(media: MediaDTO[]) { + await this.loadAll(); + const personFilter = (keyword: string) => this.persons.find(p => p.name.toLowerCase() === keyword.toLowerCase()); + (media).forEach(m => { + if (!m.metadata.keywords || m.metadata.keywords.length === 0) { + return; + } + + const personKeywords = m.metadata.keywords.filter(k => personFilter(k)); + if (personKeywords.length === 0) { + return; + } + // remove persons + m.metadata.keywords = m.metadata.keywords.filter(k => !personFilter(k)); + m.metadata.faces = m.metadata.faces || []; + personKeywords.forEach((pk: string) => { + m.metadata.faces.push({ + name: pk + }); + }); + + }); + } + async get(name: string): Promise { let person = this.persons.find(p => p.name === name); @@ -28,7 +62,7 @@ export class PersonManager implements IPersonManager { const toSave: { name: string }[] = []; const connection = await SQLConnection.getConnection(); const personRepository = connection.getRepository(PersonEntry); - this.persons = await personRepository.find(); + await this.loadAll(); for (let i = 0; i < names.length; i++) { @@ -42,7 +76,7 @@ export class PersonManager implements IPersonManager { for (let i = 0; i < toSave.length / 200; i++) { await personRepository.insert(toSave.slice(i * 200, (i + 1) * 200)); } - this.persons = await personRepository.find(); + this.persons = await personRepository.find(); } } diff --git a/backend/model/threading/MetadataLoader.ts b/backend/model/threading/MetadataLoader.ts index 43f26021..5e1b8bfe 100644 --- a/backend/model/threading/MetadataLoader.ts +++ b/backend/model/threading/MetadataLoader.ts @@ -187,48 +187,48 @@ export class MetadataLoader { metadata.creationDate = metadata.creationDate || 0; + if (Config.Client.Faces.enabled) { + try { - try { - - const ret = ExifReader.load(data); - const faces: FaceRegion[] = []; - if (ret.Regions && ret.Regions.value.RegionList && ret.Regions.value.RegionList.value) { - for (let i = 0; i < ret.Regions.value.RegionList.value.length; i++) { - if (!ret.Regions.value.RegionList.value[i].value || - !ret.Regions.value.RegionList.value[i].value['rdf:Description'] || - !ret.Regions.value.RegionList.value[i].value['rdf:Description'].value || - !ret.Regions.value.RegionList.value[i].value['rdf:Description'].value['mwg-rs:Area']) { - continue; + const ret = ExifReader.load(data); + const faces: FaceRegion[] = []; + if (ret.Regions && ret.Regions.value.RegionList && ret.Regions.value.RegionList.value) { + for (let i = 0; i < ret.Regions.value.RegionList.value.length; i++) { + if (!ret.Regions.value.RegionList.value[i].value || + !ret.Regions.value.RegionList.value[i].value['rdf:Description'] || + !ret.Regions.value.RegionList.value[i].value['rdf:Description'].value || + !ret.Regions.value.RegionList.value[i].value['rdf:Description'].value['mwg-rs:Area']) { + continue; + } + const region = ret.Regions.value.RegionList.value[i].value['rdf:Description']; + const regionBox = ret.Regions.value.RegionList.value[i].value['rdf:Description'].value['mwg-rs:Area'].attributes; + if (region.attributes['mwg-rs:Type'] !== 'Face' || + !region.attributes['mwg-rs:Name']) { + continue; + } + const name = region.attributes['mwg-rs:Name']; + const box = { + width: Math.round(regionBox['stArea:w'] * metadata.size.width), + height: Math.round(regionBox['stArea:h'] * metadata.size.height), + x: Math.round(regionBox['stArea:x'] * metadata.size.width), + y: Math.round(regionBox['stArea:y'] * metadata.size.height) + }; + faces.push({name: name, box: box}); } - const region = ret.Regions.value.RegionList.value[i].value['rdf:Description']; - const regionBox = ret.Regions.value.RegionList.value[i].value['rdf:Description'].value['mwg-rs:Area'].attributes; - if (region.attributes['mwg-rs:Type'] !== 'Face' || - !region.attributes['mwg-rs:Name']) { - continue; - } - const name = region.attributes['mwg-rs:Name']; - const box = { - width: Math.round(regionBox['stArea:w'] * metadata.size.width), - height: Math.round(regionBox['stArea:h'] * metadata.size.height), - x: Math.round(regionBox['stArea:x'] * metadata.size.width), - y: Math.round(regionBox['stArea:y'] * metadata.size.height) - }; - faces.push({name: name, box: box}); } + if (Config.Client.Faces.keywordsToPersons && faces.length > 0) { + metadata.faces = faces; // save faces + // remove faces from keywords + metadata.faces.forEach(f => { + const index = metadata.keywords.indexOf(f.name); + if (index !== -1) { + metadata.keywords.splice(index, 1); + } + }); + } + } catch (err) { } - if (faces.length > 0) { - metadata.faces = faces; // save faces - // remove faces from keywords - metadata.faces.forEach(f => { - const index = metadata.keywords.indexOf(f.name); - if (index !== -1) { - metadata.keywords.splice(index, 1); - } - }); - } - } catch (err) { } - return resolve(metadata); } catch (err) { return reject({file: fullPath, error: err}); diff --git a/backend/model/threading/ThumbnailWorker.ts b/backend/model/threading/ThumbnailWorker.ts index 80456b14..e42e744e 100644 --- a/backend/model/threading/ThumbnailWorker.ts +++ b/backend/model/threading/ThumbnailWorker.ts @@ -169,7 +169,7 @@ export class ImageRendererFactory { return async (input: RendererInput): Promise => { Logger.silly('[SharpThRenderer] rendering thumbnail:' + input.mediaPath); - const image: Sharp = sharp(input.mediaPath); + const image: Sharp = sharp(input.mediaPath, {failOnError: false}); const metadata: Metadata = await image.metadata(); /** diff --git a/common/config/public/ConfigClass.ts b/common/config/public/ConfigClass.ts index cce00613..73d32706 100644 --- a/common/config/public/ConfigClass.ts +++ b/common/config/public/ConfigClass.ts @@ -65,6 +65,10 @@ export module ClientConfig { enabled: boolean; } + export interface FacesConfig { + enabled: boolean; + keywordsToPersons: boolean; + } export interface Config { applicationTitle: string; @@ -81,6 +85,7 @@ export module ClientConfig { languages: string[]; Video: VideoConfig; MetaFile: MetaFileConfig; + Faces: FacesConfig; } } @@ -138,6 +143,10 @@ export class PublicConfigClass { showItemCount: true } }, + Faces: { + enabled: true, + keywordsToPersons: true + }, authenticationRequired: true, unAuthenticatedUserRole: UserRoles.Admin, publicUrl: '', diff --git a/common/entities/PhotoDTO.ts b/common/entities/PhotoDTO.ts index 7f605b3e..a6d5e98b 100644 --- a/common/entities/PhotoDTO.ts +++ b/common/entities/PhotoDTO.ts @@ -20,7 +20,7 @@ export interface FaceRegionBox { export interface FaceRegion { name: string; - box: FaceRegionBox; + box?: FaceRegionBox; // some faces don t have region ass they are coming from keywords } export interface PhotoMetadata extends MediaMetadata { diff --git a/frontend/app/gallery/FixOrientationPipe.ts b/frontend/app/gallery/FixOrientationPipe.ts index 59cfd9c9..f1fd89c0 100644 --- a/frontend/app/gallery/FixOrientationPipe.ts +++ b/frontend/app/gallery/FixOrientationPipe.ts @@ -35,6 +35,15 @@ export class FixOrientationPipe implements PipeTransform { } // transform context before drawing image + + // transform function parameters: + // a Horizontal scaling + // b Horizontal skewing + // c Vertical skewing + // d Vertical scaling + // e Horizontal moving + // f Vertical moving + switch (orientation) { case OrientationTypes.TOP_RIGHT: // 2 ctx.transform(-1, 0, 0, 1, width, 0); diff --git a/frontend/app/settings/settings.service.ts b/frontend/app/settings/settings.service.ts index 8a3b37ab..2f19fe12 100644 --- a/frontend/app/settings/settings.service.ts +++ b/frontend/app/settings/settings.service.ts @@ -59,6 +59,10 @@ export class SettingsService { showItemCount: true } }, + Faces: { + enabled: true, + keywordsToPersons: true + }, urlBase: '', publicUrl: '', applicationTitle: '', diff --git a/test/backend/unit/model/sql/PersonManager.ts b/test/backend/unit/model/sql/PersonManager.ts new file mode 100644 index 00000000..53f0f757 --- /dev/null +++ b/test/backend/unit/model/sql/PersonManager.ts @@ -0,0 +1,48 @@ +import {expect} from 'chai'; +import {PersonManager} from '../../../../../backend/model/sql/PersonManager'; +import {FaceRegion, PhotoDTO} from '../../../../../common/entities/PhotoDTO'; + + +// to help WebStorm to handle the test cases +declare let describe: any; +declare const after: any; +declare const it: any; + + +describe('PersonManager', () => { + + it('should upgrade keywords to person', async () => { + const pm = new PersonManager(); + pm.loadAll = () => Promise.resolve(); + pm.persons = [{name: 'Han Solo', id: 0, faces: []}, + {name: 'Anakin', id: 2, faces: []}]; + + const p_noFaces = { + metadata: { + keywords: ['Han Solo', 'just a keyword'] + } + }; + + const p_wFace = { + metadata: { + keywords: ['Han Solo', 'Anakin'], + faces: [{name: 'Obivan'}] + } + }; + + const cmp = (a: FaceRegion, b: FaceRegion) => { + return a.name.localeCompare(b.name); + }; + + await pm.keywordsToPerson([p_noFaces]); + expect(p_noFaces.metadata.keywords).to.be.deep.equal(['just a keyword']); + expect(p_noFaces.metadata.faces.sort(cmp)).to.eql([{name: 'Han Solo'}].sort(cmp)); + + await pm.keywordsToPerson([p_wFace]); + expect(p_wFace.metadata.keywords).to.be.deep.equal([]); + expect(p_wFace.metadata.faces.sort(cmp)).to.be + .eql([{name: 'Han Solo'}, {name: 'Obivan'}, {name: 'Anakin'}].sort(cmp)); + + }); + +});