import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import * as crypto from 'crypto'; import {ProjectPath} from '../../ProjectPath'; import {Config} from '../../../common/config/private/Config'; import {ThumbnailTH} from '../threading/ThreadPool'; import {PhotoWorker, RendererInput, ThumbnailSourceType} from '../threading/PhotoWorker'; import {ITaskExecuter, TaskExecuter} from '../threading/TaskExecuter'; import {ServerConfig} from '../../../common/config/private/IPrivateConfig'; import {FaceRegion, PhotoDTO} from '../../../common/entities/PhotoDTO'; export class PhotoProcessing { private static initDone = false; private static taskQue: ITaskExecuter = null; public static init() { if (this.initDone === true) { return; } if (Config.Server.Threading.enabled === true) { if (Config.Server.Threading.thumbnailThreads > 0) { Config.Client.Media.Thumbnail.concurrentThumbnailGenerations = Config.Server.Threading.thumbnailThreads; } else { Config.Client.Media.Thumbnail.concurrentThumbnailGenerations = Math.max(1, os.cpus().length - 1); } } else { Config.Client.Media.Thumbnail.concurrentThumbnailGenerations = 1; } if (Config.Server.Threading.enabled === true && Config.Server.Media.photoProcessingLibrary === ServerConfig.PhotoProcessingLib.Jimp) { this.taskQue = new ThumbnailTH(Config.Client.Media.Thumbnail.concurrentThumbnailGenerations); } else { this.taskQue = new TaskExecuter(Config.Client.Media.Thumbnail.concurrentThumbnailGenerations, (input => PhotoWorker.render(input, Config.Server.Media.photoProcessingLibrary))); } this.initDone = true; } public static async generatePersonThumbnail(photo: PhotoDTO) { // load parameters if (!photo.metadata.faces || photo.metadata.faces.length !== 1) { throw new Error('Photo does not contain a face'); } // load parameters const mediaPath = path.join(ProjectPath.ImageFolder, photo.directory.path, photo.directory.name, photo.name); const size: number = Config.Client.Media.Thumbnail.personThumbnailSize; // generate thumbnail path const thPath = PhotoProcessing.generatePersonThumbnailPath(mediaPath, photo.metadata.faces[0], size); // check if thumbnail already exist if (fs.existsSync(thPath) === true) { return null; } const margin = { x: Math.round(photo.metadata.faces[0].box.width * (Config.Server.Media.Thumbnail.personFaceMargin)), y: Math.round(photo.metadata.faces[0].box.height * (Config.Server.Media.Thumbnail.personFaceMargin)) }; // run on other thread const input = { type: ThumbnailSourceType.Photo, mediaPath: mediaPath, size: size, outPath: thPath, makeSquare: false, cut: { left: Math.round(Math.max(0, photo.metadata.faces[0].box.left - margin.x / 2)), top: Math.round(Math.max(0, photo.metadata.faces[0].box.top - margin.y / 2)), width: photo.metadata.faces[0].box.width + margin.x, height: photo.metadata.faces[0].box.height + margin.y }, qualityPriority: Config.Server.Media.Thumbnail.qualityPriority }; input.cut.width = Math.min(input.cut.width, photo.metadata.size.width - input.cut.left); input.cut.height = Math.min(input.cut.height, photo.metadata.size.height - input.cut.top); await PhotoProcessing.taskQue.execute(input); return thPath; } public static generateThumbnailPath(mediaPath: string, size: number): string { const extension = path.extname(mediaPath); const file = path.basename(mediaPath, extension); return path.join(ProjectPath.TranscodedFolder, ProjectPath.getRelativePathToImages(path.dirname(mediaPath)), file + '_' + size + '.jpg'); } public static generatePersonThumbnailPath(mediaPath: string, faceRegion: FaceRegion, size: number): string { return path.join(ProjectPath.FacesFolder, crypto.createHash('md5').update(mediaPath + '_' + faceRegion.name + '_' + faceRegion.box.left + '_' + faceRegion.box.top) .digest('hex') + '_' + size + '.jpg'); } public static generateConvertedFilePath(photoPath: string): string { const extension = path.extname(photoPath); const file = path.basename(photoPath, extension); const postfix = Config.Server.Media.Photo.Converting.resolution; return path.join(ProjectPath.TranscodedFolder, ProjectPath.getRelativePathToImages(path.dirname(photoPath)), file + '_' + postfix + '.jpg'); } public static async convertPhoto(mediaPath: string, size: number) { // generate thumbnail path const outPath = PhotoProcessing.generateConvertedFilePath(mediaPath); // check if file already exist if (fs.existsSync(outPath) === true) { return outPath; } // run on other thread const input = { type: ThumbnailSourceType.Photo, mediaPath: mediaPath, size: size, outPath: outPath, makeSquare: false, qualityPriority: Config.Server.Media.Thumbnail.qualityPriority }; const outDir = path.dirname(input.outPath); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, {recursive: true}); } await this.taskQue.execute(input); return outPath; } public static async generateThumbnail(mediaPath: string, size: number, sourceType: ThumbnailSourceType, makeSquare: boolean) { // generate thumbnail path const outPath = PhotoProcessing.generateThumbnailPath(mediaPath, size); // check if thumbnail already exist if (fs.existsSync(outPath) === true) { return outPath; } // run on other thread const input = { type: sourceType, mediaPath: mediaPath, size: size, outPath: outPath, makeSquare: makeSquare, qualityPriority: Config.Server.Media.Thumbnail.qualityPriority }; const outDir = path.dirname(input.outPath); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, {recursive: true}); } await this.taskQue.execute(input); return outPath; } }