1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-25 02:04:15 +02:00

implementing sharp for hardware accelerated thumbnail generation

This commit is contained in:
Braun Patrik 2017-05-28 12:33:18 +02:00
parent ffd01c5765
commit 4c6eeac71b
10 changed files with 136 additions and 50 deletions

View File

@ -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

View File

@ -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);
}
}

View File

@ -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: {

View 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));
}
});
};

View File

@ -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));
});
}

View File

@ -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) {

View File

@ -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);

View File

@ -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;
}

View File

@ -9,6 +9,7 @@ export enum ErrorCodes{
GENERAL_ERROR,
THUMBNAIL_GENERATION_ERROR,
SERVER_ERROR,
USER_MANAGEMENT_DISABLED

View File

@ -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"
}
}