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) * prioritizes thumbnail generation (generating thumbnail first for the visible photos)
* saving generated thumbnails to TEMP folder for reuse * saving generated thumbnails to TEMP folder for reuse
* supporting several core CPU * 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 * Custom lightbox for full screen photo viewing
* keyboard support for navigation - `In progress` * keyboard support for navigation - `In progress`
* showing low-res thumbnail while full image loads * showing low-res thumbnail while full image loads

View File

@ -17,7 +17,7 @@ class ProjectPathClass {
constructor() { constructor() {
this.Root = path.join(__dirname, "/../"); this.Root = path.join(__dirname, "/../");
this.ImageFolder = this.isAbsolutePath(Config.Server.imagesFolder) ? Config.Server.imagesFolder : path.join(this.Root, Config.Server.imagesFolder); 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 = { Config.Server = {
port: 80, port: 80,
imagesFolder: "demo/images", imagesFolder: "demo/images",
thumbnailFolder: "demo/TEMP", thumbnail: {
folder: "demo/TEMP",
hardwareAcceleration: true
},
database: { database: {
type: DatabaseType.mysql, type: DatabaseType.mysql,
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 path from "path";
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as fs from "fs"; import * as fs from "fs";
import * as os from "os"; import * as os from "os";
import {NextFunction, Request, Response} from "express"; import {NextFunction, Request, Response} from "express";
import {Error, ErrorCodes} from "../../common/entities/Error"; import {Error, ErrorCodes} from "../../../common/entities/Error";
import {Config} from "../config/Config"; import {Config} from "../../config/Config";
import {ContentWrapper} from "../../common/entities/ConentWrapper"; import {ContentWrapper} from "../../../common/entities/ConentWrapper";
import {DirectoryDTO} from "../../common/entities/DirectoryDTO"; import {DirectoryDTO} from "../../../common/entities/DirectoryDTO";
import {ProjectPath} from "../ProjectPath"; import {ProjectPath} from "../../ProjectPath";
import {PhotoDTO} from "../../common/entities/PhotoDTO"; import {PhotoDTO} from "../../../common/entities/PhotoDTO";
import {hardwareRenderer, softwareRenderer} from "./THRenderers";
Config.Client.concurrentThumbnailGenerations = Math.max(1, os.cpus().length - 1); 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 = require('threads').Pool;
const pool = new Pool(Config.Client.concurrentThumbnailGenerations); const pool = new Pool(Config.Client.concurrentThumbnailGenerations);
pool.run( if (Config.Server.thumbnail.hardwareAcceleration == true) {
(input: {imagePath: string, size: number, makeSquare: boolean, thPath: string}, done) => { pool.run(hardwareRenderer);
//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 { } else {
let ratio = image.bitmap.height / image.bitmap.width; pool.run(softwareRenderer);
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));
});
}
);
export class ThumbnailGeneratorMWs { export class ThumbnailGeneratorMWs {
@ -159,11 +130,12 @@ export class ThumbnailGeneratorMWs {
} }
//run on other thread //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) => { .on('done', (out) => {
return next(out); return next(out);
}).on('error', (job, error) => { }).on('error', (error) => {
return next(new Error(ErrorCodes.GENERAL_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 {AuthenticationMWs} from "../middlewares/user/AuthenticationMWs";
import {GalleryMWs} from "../middlewares/GalleryMWs"; import {GalleryMWs} from "../middlewares/GalleryMWs";
import {RenderingMWs} from "../middlewares/RenderingMWs"; import {RenderingMWs} from "../middlewares/RenderingMWs";
import {ThumbnailGeneratorMWs} from "../middlewares/ThumbnailGeneratorMWs"; import {ThumbnailGeneratorMWs} from "../middlewares/thumbnail/ThumbnailGeneratorMWs";
export class GalleryRouter { export class GalleryRouter {
constructor(private app: any) { constructor(private app: any) {

View File

@ -63,6 +63,17 @@ export class Server {
ObjectManagerRepository.InitMemoryManagers(); 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 PublicRouter(this.app);
new UserRouter(this.app); new UserRouter(this.app);

View File

@ -12,11 +12,15 @@ interface DataBaseConfig {
type: DatabaseType; type: DatabaseType;
mysql?: MySQLConfig; mysql?: MySQLConfig;
} }
interface ThumbnailConfig {
folder: string;
hardwareAcceleration: boolean;
}
interface ServerConfig { interface ServerConfig {
port: number; port: number;
imagesFolder: string; imagesFolder: string;
thumbnailFolder: string; thumbnail: ThumbnailConfig;
database: DataBaseConfig; database: DataBaseConfig;
} }

View File

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

View File

@ -59,6 +59,7 @@
"@types/jasmine": "^2.5.47", "@types/jasmine": "^2.5.47",
"@types/node": "^7.0.22", "@types/node": "^7.0.22",
"@types/optimist": "0.0.29", "@types/optimist": "0.0.29",
"@types/sharp": "^0.17.1",
"chai": "^4.0.0", "chai": "^4.0.0",
"jasmine-core": "^2.6.2", "jasmine-core": "^2.6.2",
"karma": "^1.7.0", "karma": "^1.7.0",
@ -76,5 +77,8 @@
"ts-helpers": "^1.1.2", "ts-helpers": "^1.1.2",
"tslint": "^5.3.2", "tslint": "^5.3.2",
"typescript": "^2.3.3" "typescript": "^2.3.3"
},
"optionalDependencies": {
"sharp": "^0.17.3"
} }
} }