mirror of
https://github.com/bpatrik/pigallery2.git
synced 2024-11-24 08:42:24 +02:00
implementing sharp for hardware accelerated thumbnail generation
This commit is contained in:
parent
ffd01c5765
commit
4c6eeac71b
@ -24,7 +24,7 @@ Feature list:
|
||||
* prioritizes thumbnail generation (generating thumbnail first for the visible photos)
|
||||
* saving generated thumbnails to TEMP folder for reuse
|
||||
* supporting several core CPU
|
||||
* supporting hardware acceleration - `In progress`
|
||||
* supporting hardware acceleration ([sharp](https://github.com/lovell/sharp) as optional and JS-based [Jimp](https://github.com/oliver-moran/jimp) as fallback)
|
||||
* Custom lightbox for full screen photo viewing
|
||||
* keyboard support for navigation - `In progress`
|
||||
* showing low-res thumbnail while full image loads
|
||||
|
@ -17,7 +17,7 @@ class ProjectPathClass {
|
||||
constructor() {
|
||||
this.Root = path.join(__dirname, "/../");
|
||||
this.ImageFolder = this.isAbsolutePath(Config.Server.imagesFolder) ? Config.Server.imagesFolder : path.join(this.Root, Config.Server.imagesFolder);
|
||||
this.ThumbnailFolder = this.isAbsolutePath(Config.Server.thumbnailFolder) ? Config.Server.thumbnailFolder : path.join(this.Root, Config.Server.thumbnailFolder);
|
||||
this.ThumbnailFolder = this.isAbsolutePath(Config.Server.thumbnail.folder) ? Config.Server.thumbnail.folder : path.join(this.Root, Config.Server.thumbnail.folder);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,10 @@ export let Config = new ConfigClass();
|
||||
Config.Server = {
|
||||
port: 80,
|
||||
imagesFolder: "demo/images",
|
||||
thumbnailFolder: "demo/TEMP",
|
||||
thumbnail: {
|
||||
folder: "demo/TEMP",
|
||||
hardwareAcceleration: true
|
||||
},
|
||||
database: {
|
||||
type: DatabaseType.mysql,
|
||||
mysql: {
|
||||
|
91
backend/middlewares/thumbnail/THRenderers.ts
Normal file
91
backend/middlewares/thumbnail/THRenderers.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import {Metadata, SharpInstance} from "@types/sharp";
|
||||
|
||||
export interface RendererInput {
|
||||
imagePath: string;
|
||||
size: number;
|
||||
makeSquare: boolean;
|
||||
thPath: string;
|
||||
__dirname: string;
|
||||
}
|
||||
|
||||
export const softwareRenderer = (input: RendererInput, done) => {
|
||||
|
||||
//generate thumbnail
|
||||
const Jimp = require("jimp");
|
||||
Jimp.read(input.imagePath).then((image) => {
|
||||
/**
|
||||
* newWidth * newHeight = size*size
|
||||
* newHeight/newWidth = height/width
|
||||
*
|
||||
* newHeight = (height/width)*newWidth
|
||||
* newWidth * newWidth = (size*size) / (height/width)
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
const ratio = image.bitmap.height / image.bitmap.width;
|
||||
if (input.makeSquare == false) {
|
||||
let newWidth = Math.sqrt((input.size * input.size) / ratio);
|
||||
|
||||
image.resize(newWidth, Jimp.AUTO, Jimp.RESIZE_BEZIER);
|
||||
} else {
|
||||
image.resize(input.size / Math.min(ratio, 1), Jimp.AUTO, Jimp.RESIZE_BEZIER);
|
||||
image.crop(0, 0, input.size, input.size);
|
||||
}
|
||||
image.quality(60); // set JPEG quality
|
||||
image.write(input.thPath, () => { // save
|
||||
return done();
|
||||
});
|
||||
}).catch(function (err) {
|
||||
const Error = require(input.__dirname + "/../../../common/entities/Error").Error;
|
||||
const ErrorCodes = require(input.__dirname + "/../../../common/entities/Error").ErrorCodes;
|
||||
return done(new Error(ErrorCodes.GENERAL_ERROR, err));
|
||||
});
|
||||
};
|
||||
|
||||
export const hardwareRenderer = (input: RendererInput, done) => {
|
||||
|
||||
//generate thumbnail
|
||||
const sharp = require("sharp");
|
||||
|
||||
const image: SharpInstance = sharp(input.imagePath);
|
||||
image
|
||||
.metadata()
|
||||
.then((metadata: Metadata) => {
|
||||
/**
|
||||
* newWidth * newHeight = size*size
|
||||
* newHeight/newWidth = height/width
|
||||
*
|
||||
* newHeight = (height/width)*newWidth
|
||||
* newWidth * newWidth = (size*size) / (height/width)
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
try {
|
||||
const ratio = metadata.height / metadata.width;
|
||||
if (input.makeSquare == false) {
|
||||
const newWidth = Math.round(Math.sqrt((input.size * input.size) / ratio));
|
||||
console.log(image
|
||||
.resize(newWidth));
|
||||
|
||||
} else {
|
||||
image
|
||||
.resize(input.size, input.size)
|
||||
.crop(sharp.strategy.center);
|
||||
}
|
||||
image
|
||||
.jpeg()
|
||||
.toFile(input.thPath).then(() => {
|
||||
return done();
|
||||
}).catch(function (err) {
|
||||
const Error = require(input.__dirname + "/../../../common/entities/Error").Error;
|
||||
const ErrorCodes = require(input.__dirname + "/../../../common/entities/Error").ErrorCodes;
|
||||
return done(new Error(ErrorCodes.GENERAL_ERROR, err));
|
||||
});
|
||||
} catch (err) {
|
||||
const Error = require(input.__dirname + "/../../../common/entities/Error").Error;
|
||||
const ErrorCodes = require(input.__dirname + "/../../../common/entities/Error").ErrorCodes;
|
||||
return done(new Error(ErrorCodes.GENERAL_ERROR, err));
|
||||
}
|
||||
});
|
||||
|
||||
};
|
@ -1,15 +1,16 @@
|
||||
///<reference path="customtypings/jimp.d.ts"/>
|
||||
///<reference path="../customtypings/jimp.d.ts"/>
|
||||
import * as path from "path";
|
||||
import * as crypto from "crypto";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import {Error, ErrorCodes} from "../../common/entities/Error";
|
||||
import {Config} from "../config/Config";
|
||||
import {ContentWrapper} from "../../common/entities/ConentWrapper";
|
||||
import {DirectoryDTO} from "../../common/entities/DirectoryDTO";
|
||||
import {ProjectPath} from "../ProjectPath";
|
||||
import {PhotoDTO} from "../../common/entities/PhotoDTO";
|
||||
import {Error, ErrorCodes} from "../../../common/entities/Error";
|
||||
import {Config} from "../../config/Config";
|
||||
import {ContentWrapper} from "../../../common/entities/ConentWrapper";
|
||||
import {DirectoryDTO} from "../../../common/entities/DirectoryDTO";
|
||||
import {ProjectPath} from "../../ProjectPath";
|
||||
import {PhotoDTO} from "../../../common/entities/PhotoDTO";
|
||||
import {hardwareRenderer, softwareRenderer} from "./THRenderers";
|
||||
|
||||
|
||||
Config.Client.concurrentThumbnailGenerations = Math.max(1, os.cpus().length - 1);
|
||||
@ -17,41 +18,11 @@ Config.Client.concurrentThumbnailGenerations = Math.max(1, os.cpus().length - 1)
|
||||
const Pool = require('threads').Pool;
|
||||
const pool = new Pool(Config.Client.concurrentThumbnailGenerations);
|
||||
|
||||
pool.run(
|
||||
(input: {imagePath: string, size: number, makeSquare: boolean, thPath: string}, done) => {
|
||||
|
||||
//generate thumbnail
|
||||
let Jimp = require("jimp");
|
||||
Jimp.read(input.imagePath).then((image) => {
|
||||
/**
|
||||
* newWidth * newHeight = size*size
|
||||
* newHeight/newWidth = height/width
|
||||
*
|
||||
* newHeight = (height/width)*newWidth
|
||||
* newWidth * newWidth = (size*size) / (height/width)
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
if (input.makeSquare == false) {
|
||||
let ratio = image.bitmap.height / image.bitmap.width;
|
||||
let newWidth = Math.sqrt((input.size * input.size) / ratio);
|
||||
|
||||
image.resize(newWidth, Jimp.AUTO, Jimp.RESIZE_BEZIER);
|
||||
} else {
|
||||
let ratio = image.bitmap.height / image.bitmap.width;
|
||||
image.resize(input.size / Math.min(ratio, 1), Jimp.AUTO, Jimp.RESIZE_BEZIER);
|
||||
image.crop(0, 0, input.size, input.size);
|
||||
}
|
||||
image.quality(60); // set JPEG quality
|
||||
image.write(input.thPath, () => { // save
|
||||
return done();
|
||||
});
|
||||
}).catch(function (err) {
|
||||
return done(new Error(ErrorCodes.GENERAL_ERROR));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (Config.Server.thumbnail.hardwareAcceleration == true) {
|
||||
pool.run(hardwareRenderer);
|
||||
} else {
|
||||
pool.run(softwareRenderer);
|
||||
}
|
||||
|
||||
export class ThumbnailGeneratorMWs {
|
||||
|
||||
@ -159,11 +130,12 @@ export class ThumbnailGeneratorMWs {
|
||||
}
|
||||
|
||||
//run on other thread
|
||||
pool.send({imagePath: imagePath, size: size, thPath: thPath, makeSquare: makeSquare})
|
||||
pool.send({imagePath: imagePath, size: size, thPath: thPath, makeSquare: makeSquare, __dirname: __dirname})
|
||||
.on('done', (out) => {
|
||||
return next(out);
|
||||
}).on('error', (job, error) => {
|
||||
return next(new Error(ErrorCodes.GENERAL_ERROR, error));
|
||||
}).on('error', (error) => {
|
||||
console.log(error);
|
||||
return next(new Error(ErrorCodes.THUMBNAIL_GENERATION_ERROR, error));
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {AuthenticationMWs} from "../middlewares/user/AuthenticationMWs";
|
||||
import {GalleryMWs} from "../middlewares/GalleryMWs";
|
||||
import {RenderingMWs} from "../middlewares/RenderingMWs";
|
||||
import {ThumbnailGeneratorMWs} from "../middlewares/ThumbnailGeneratorMWs";
|
||||
import {ThumbnailGeneratorMWs} from "../middlewares/thumbnail/ThumbnailGeneratorMWs";
|
||||
|
||||
export class GalleryRouter {
|
||||
constructor(private app: any) {
|
||||
|
@ -63,6 +63,17 @@ export class Server {
|
||||
ObjectManagerRepository.InitMemoryManagers();
|
||||
});
|
||||
|
||||
if (Config.Server.thumbnail.hardwareAcceleration == true) {
|
||||
try {
|
||||
const sharp = require.resolve("sharp");
|
||||
} catch (err) {
|
||||
console.error("Thumbnail hardware acceleration is not possible." +
|
||||
" 'Sharp' node module is not found." +
|
||||
" Falling back to JS based thumbnail generation");
|
||||
Config.Server.thumbnail.hardwareAcceleration = false;
|
||||
}
|
||||
}
|
||||
|
||||
new PublicRouter(this.app);
|
||||
|
||||
new UserRouter(this.app);
|
||||
|
@ -12,11 +12,15 @@ interface DataBaseConfig {
|
||||
type: DatabaseType;
|
||||
mysql?: MySQLConfig;
|
||||
}
|
||||
interface ThumbnailConfig {
|
||||
folder: string;
|
||||
hardwareAcceleration: boolean;
|
||||
}
|
||||
|
||||
interface ServerConfig {
|
||||
port: number;
|
||||
imagesFolder: string;
|
||||
thumbnailFolder: string;
|
||||
thumbnail: ThumbnailConfig;
|
||||
database: DataBaseConfig;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ export enum ErrorCodes{
|
||||
|
||||
|
||||
GENERAL_ERROR,
|
||||
THUMBNAIL_GENERATION_ERROR,
|
||||
SERVER_ERROR,
|
||||
|
||||
USER_MANAGEMENT_DISABLED
|
||||
|
@ -59,6 +59,7 @@
|
||||
"@types/jasmine": "^2.5.47",
|
||||
"@types/node": "^7.0.22",
|
||||
"@types/optimist": "0.0.29",
|
||||
"@types/sharp": "^0.17.1",
|
||||
"chai": "^4.0.0",
|
||||
"jasmine-core": "^2.6.2",
|
||||
"karma": "^1.7.0",
|
||||
@ -76,5 +77,8 @@
|
||||
"ts-helpers": "^1.1.2",
|
||||
"tslint": "^5.3.2",
|
||||
"typescript": "^2.3.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sharp": "^0.17.3"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user