From 4c6eeac71b1ec17f0d0ca0a791b580840308c00b Mon Sep 17 00:00:00 2001 From: Braun Patrik Date: Sun, 28 May 2017 12:33:18 +0200 Subject: [PATCH] implementing sharp for hardware accelerated thumbnail generation --- README.md | 2 +- backend/ProjectPath.ts | 2 +- backend/config/Config.ts | 5 +- backend/middlewares/thumbnail/THRenderers.ts | 91 +++++++++++++++++++ .../{ => thumbnail}/ThumbnailGeneratorMWs.ts | 62 ++++--------- backend/routes/GalleryRouter.ts | 2 +- backend/server.ts | 11 +++ common/config/Config.ts | 6 +- common/entities/Error.ts | 1 + package.json | 4 + 10 files changed, 136 insertions(+), 50 deletions(-) create mode 100644 backend/middlewares/thumbnail/THRenderers.ts rename backend/middlewares/{ => thumbnail}/ThumbnailGeneratorMWs.ts (70%) diff --git a/README.md b/README.md index 641204c..41c0137 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/backend/ProjectPath.ts b/backend/ProjectPath.ts index 25d4dd0..e65324f 100644 --- a/backend/ProjectPath.ts +++ b/backend/ProjectPath.ts @@ -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); } } diff --git a/backend/config/Config.ts b/backend/config/Config.ts index a4c717c..f14bf9e 100644 --- a/backend/config/Config.ts +++ b/backend/config/Config.ts @@ -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: { diff --git a/backend/middlewares/thumbnail/THRenderers.ts b/backend/middlewares/thumbnail/THRenderers.ts new file mode 100644 index 0000000..5a3189d --- /dev/null +++ b/backend/middlewares/thumbnail/THRenderers.ts @@ -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)); + } + }); + +}; \ No newline at end of file diff --git a/backend/middlewares/ThumbnailGeneratorMWs.ts b/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts similarity index 70% rename from backend/middlewares/ThumbnailGeneratorMWs.ts rename to backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts index d7685d9..c68e760 100644 --- a/backend/middlewares/ThumbnailGeneratorMWs.ts +++ b/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts @@ -1,15 +1,16 @@ -/// +/// 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)); }); } diff --git a/backend/routes/GalleryRouter.ts b/backend/routes/GalleryRouter.ts index 9c28668..7868294 100644 --- a/backend/routes/GalleryRouter.ts +++ b/backend/routes/GalleryRouter.ts @@ -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) { diff --git a/backend/server.ts b/backend/server.ts index e38b187..d7c53b4 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -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); diff --git a/common/config/Config.ts b/common/config/Config.ts index d17b609..613cbbb 100644 --- a/common/config/Config.ts +++ b/common/config/Config.ts @@ -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; } diff --git a/common/entities/Error.ts b/common/entities/Error.ts index 7ac5976..03436e9 100644 --- a/common/entities/Error.ts +++ b/common/entities/Error.ts @@ -9,6 +9,7 @@ export enum ErrorCodes{ GENERAL_ERROR, + THUMBNAIL_GENERATION_ERROR, SERVER_ERROR, USER_MANAGEMENT_DISABLED diff --git a/package.json b/package.json index bd4c5e5..8d8fb7e 100644 --- a/package.json +++ b/package.json @@ -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" } }