pigallery2/src/backend/model/fileprocessing/PhotoProcessing.ts
Patrik J. Braun c3c94c1709 replacing flat thumbnail path to shadow original folder hierarchy
preparing tmp folder clean-up task
2019-12-24 12:44:38 +01:00

184 lines
6.1 KiB
TypeScript

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<RendererInput, void> = 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 = <RendererInput>{
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 = <RendererInput>{
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 = <RendererInput>{
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;
}
}