From f8338e7c4fb461f9d4edf7daf1ea734205053bc6 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Sat, 10 May 2014 19:45:12 +0100 Subject: [PATCH] Add quality and compressionLevel options for output image. #24 --- README.md | 16 ++- index.js | 278 +++++++++++++++++++++++++++----------------------- src/sharp.cc | 20 ++-- tests/unit.js | 161 ++++++++++++++++------------- 4 files changed, 264 insertions(+), 211 deletions(-) diff --git a/README.md b/README.md index efacb871..b9a3773d 100755 --- a/README.md +++ b/README.md @@ -94,11 +94,11 @@ sharp('input.jpg').resize(null, 200).progressive().toBuffer(function(err, buffer ``` ```javascript -sharp('input.png').resize(300).sharpen().webp(function(err, buffer) { +sharp('input.png').resize(300).sharpen().quality(90).webp(function(err, buffer) { if (err) { throw err; } - // buffer contains sharpened WebP image data (converted from PNG), 300 pixels wide + // buffer contains 300 pixels wide, sharpened, 90% quality WebP image data }); ``` @@ -159,6 +159,18 @@ Perform a mild sharpen of the resultant image. This typically reduces performanc Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed. +### quality(quality) + +The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`. + +`quality` is a Number between 1 and 100. + +### compressionLevel(compressionLevel) + +An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`. + +`compressionLevel` is a Number between -1 and 9. + ### sequentialRead() An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems. diff --git a/index.js b/index.js index 00e8067d..05b1228b 100755 --- a/index.js +++ b/index.js @@ -1,128 +1,150 @@ -/*jslint node: true */ -'use strict'; - -var sharp = require('./build/Release/sharp'); - -var Sharp = function(input) { - if (!(this instanceof Sharp)) { - return new Sharp(input); - } - this.options = { - width: -1, - height: -1, - canvas: 'c', - sharpen: false, - progressive: false, - sequentialRead: false, - output: '__jpeg' - }; - if (typeof input === 'string') { - this.options.inFile = input; - } else if (typeof input ==='object' && input instanceof Buffer) { - this.options.inBuffer = input; - } else { - throw 'Unsupported input ' + typeof input; - } - return this; -}; -module.exports = Sharp; - -Sharp.prototype.crop = function() { - this.options.canvas = 'c'; - return this; -}; - -Sharp.prototype.embedWhite = function() { - this.options.canvas = 'w'; - return this; -}; - -Sharp.prototype.embedBlack = function() { - this.options.canvas = 'b'; - return this; -}; - -Sharp.prototype.sharpen = function(sharpen) { - this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true; - return this; -}; - -Sharp.prototype.progressive = function(progressive) { - this.options.progressive = (typeof progressive === 'boolean') ? progressive : true; - return this; -}; - -Sharp.prototype.sequentialRead = function(sequentialRead) { - this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true; - return this; -}; - -Sharp.prototype.resize = function(width, height) { - if (!width) { - this.options.width = -1; - } else { - if (!Number.isNaN(width)) { - this.options.width = width; - } else { - throw 'Invalid width ' + width; - } - } - if (!height) { - this.options.height = -1; - } else { - if (!Number.isNaN(height)) { - this.options.height = height; - } else { - throw 'Invalid height ' + height; - } - } - return this; -}; - -Sharp.prototype.write = function(output, callback) { - if (!output || output.length === 0) { - throw 'Invalid output'; - } else { - this._sharp(output, callback); - } - return this; -}; - -Sharp.prototype.toBuffer = function(callback) { - return this._sharp('__input', callback); -}; - -Sharp.prototype.jpeg = function(callback) { - return this._sharp('__jpeg', callback); -}; - -Sharp.prototype.png = function(callback) { - return this._sharp('__png', callback); -}; - -Sharp.prototype.webp = function(callback) { - return this._sharp('__webp', callback); -}; - -Sharp.prototype._sharp = function(output, callback) { - sharp.resize( - this.options.inFile, - this.options.inBuffer, - output, - this.options.width, - this.options.height, - this.options.canvas, - this.options.sharpen, - this.options.progressive, - this.options.sequentialRead, - callback - ); - return this; -}; - -module.exports.cache = function(limit) { - if (Number.isNaN(limit)) { - limit = null; - } - return sharp.cache(limit); -}; +/*jslint node: true */ +'use strict'; + +var sharp = require('./build/Release/sharp'); + +var Sharp = function(input) { + if (!(this instanceof Sharp)) { + return new Sharp(input); + } + this.options = { + width: -1, + height: -1, + canvas: 'c', + sharpen: false, + progressive: false, + sequentialRead: false, + quality: 80, + compressionLevel: 6, + output: '__jpeg' + }; + if (typeof input === 'string') { + this.options.inFile = input; + } else if (typeof input ==='object' && input instanceof Buffer) { + this.options.inBuffer = input; + } else { + throw 'Unsupported input ' + typeof input; + } + return this; +}; +module.exports = Sharp; + +Sharp.prototype.crop = function() { + this.options.canvas = 'c'; + return this; +}; + +Sharp.prototype.embedWhite = function() { + this.options.canvas = 'w'; + return this; +}; + +Sharp.prototype.embedBlack = function() { + this.options.canvas = 'b'; + return this; +}; + +Sharp.prototype.sharpen = function(sharpen) { + this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true; + return this; +}; + +Sharp.prototype.progressive = function(progressive) { + this.options.progressive = (typeof progressive === 'boolean') ? progressive : true; + return this; +}; + +Sharp.prototype.sequentialRead = function(sequentialRead) { + this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true; + return this; +}; + +Sharp.prototype.quality = function(quality) { + if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) { + this.options.quality = quality; + } else { + throw 'Invalid quality (1 to 100) ' + quality; + } + return this; +}; + +Sharp.prototype.compressionLevel = function(compressionLevel) { + if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) { + this.options.compressionLevel = compressionLevel; + } else { + throw 'Invalid compressionLevel (-1 to 9) ' + compressionLevel; + } + return this; +}; + +Sharp.prototype.resize = function(width, height) { + if (!width) { + this.options.width = -1; + } else { + if (!Number.isNaN(width)) { + this.options.width = width; + } else { + throw 'Invalid width ' + width; + } + } + if (!height) { + this.options.height = -1; + } else { + if (!Number.isNaN(height)) { + this.options.height = height; + } else { + throw 'Invalid height ' + height; + } + } + return this; +}; + +Sharp.prototype.write = function(output, callback) { + if (!output || output.length === 0) { + throw 'Invalid output'; + } else { + this._sharp(output, callback); + } + return this; +}; + +Sharp.prototype.toBuffer = function(callback) { + return this._sharp('__input', callback); +}; + +Sharp.prototype.jpeg = function(callback) { + return this._sharp('__jpeg', callback); +}; + +Sharp.prototype.png = function(callback) { + return this._sharp('__png', callback); +}; + +Sharp.prototype.webp = function(callback) { + return this._sharp('__webp', callback); +}; + +Sharp.prototype._sharp = function(output, callback) { + sharp.resize( + this.options.inFile, + this.options.inBuffer, + output, + this.options.width, + this.options.height, + this.options.canvas, + this.options.sharpen, + this.options.progressive, + this.options.sequentialRead, + this.options.quality, + this.options.compressionLevel, + callback + ); + return this; +}; + +module.exports.cache = function(limit) { + if (Number.isNaN(limit)) { + limit = null; + } + return sharp.cache(limit); +}; diff --git a/src/sharp.cc b/src/sharp.cc index 098578b1..1b4b437c 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -24,6 +24,8 @@ struct resize_baton { bool sharpen; bool progessive; VipsAccess access_method; + int quality; + int compressionLevel; std::string err; resize_baton(): buffer_in_len(0), buffer_out_len(0) {} @@ -259,37 +261,37 @@ class ResizeWorker : public NanAsyncWorker { // Output if (baton->file_out == "__jpeg" || (baton->file_out == "__input" && inputImageType == JPEG)) { // Write JPEG to buffer - if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) { + if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) { return resize_error(baton, sharpened); } } else if (baton->file_out == "__png" || (baton->file_out == "__input" && inputImageType == PNG)) { // Write PNG to buffer - if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) { + if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progessive, NULL)) { return resize_error(baton, sharpened); } } else if (baton->file_out == "__webp" || (baton->file_out == "__input" && inputImageType == WEBP)) { // Write WEBP to buffer - if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, NULL)) { + if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, NULL)) { return resize_error(baton, sharpened); } } else if (is_jpeg(baton->file_out)) { // Write JPEG to file - if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) { + if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) { return resize_error(baton, sharpened); } } else if (is_png(baton->file_out)) { // Write PNG to file - if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) { + if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progessive, NULL)) { return resize_error(baton, sharpened); } } else if (is_webp(baton->file_out)) { // Write WEBP to file - if (vips_webpsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, NULL)) { + if (vips_webpsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, NULL)) { return resize_error(baton, sharpened); } } else if (is_tiff(baton->file_out)) { // Write TIFF to file - if (vips_tiffsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", 80, NULL)) { + if (vips_tiffsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) { return resize_error(baton, sharpened); } } else { @@ -345,8 +347,10 @@ NAN_METHOD(resize) { baton->sharpen = args[6]->BooleanValue(); baton->progessive = args[7]->BooleanValue(); baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; + baton->quality = args[9]->Int32Value(); + baton->compressionLevel = args[10]->Int32Value(); - NanCallback *callback = new NanCallback(args[9].As()); + NanCallback *callback = new NanCallback(args[11].As()); NanAsyncQueueWorker(new ResizeWorker(callback, baton)); NanReturnUndefined(); diff --git a/tests/unit.js b/tests/unit.js index 05222755..d4d27919 100755 --- a/tests/unit.js +++ b/tests/unit.js @@ -1,73 +1,88 @@ -var sharp = require("../index"); -var path = require("path"); -var imagemagick = require("imagemagick"); -var assert = require("assert"); -var async = require("async"); - -var fixturesPath = path.join(__dirname, "fixtures"); - -var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/ -var outputJpg = path.join(fixturesPath, "output.jpg"); - -async.series([ - // Resize with exact crop - function(done) { - sharp(inputJpg).resize(320, 240).write(outputJpg, function(err) { - if (err) throw err; - imagemagick.identify(outputJpg, function(err, features) { - if (err) throw err; - assert.strictEqual(320, features.width); - assert.strictEqual(240, features.height); - done(); - }); - }); - }, - // Resize to fixed width - function(done) { - sharp(inputJpg).resize(320).write(outputJpg, function(err) { - if (err) throw err; - imagemagick.identify(outputJpg, function(err, features) { - if (err) throw err; - assert.strictEqual(320, features.width); - assert.strictEqual(261, features.height); - done(); - }); - }); - }, - // Resize to fixed height - function(done) { - sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) { - if (err) throw err; - imagemagick.identify(outputJpg, function(err, features) { - if (err) throw err; - assert.strictEqual(391, features.width); - assert.strictEqual(320, features.height); - done(); - }); - }); - }, - // Identity transform - function(done) { - sharp(inputJpg).write(outputJpg, function(err) { - if (err) throw err; - imagemagick.identify(outputJpg, function(err, features) { - if (err) throw err; - assert.strictEqual(2725, features.width); - assert.strictEqual(2225, features.height); - done(); - }); - }); - }, - // Upscale - function(done) { - sharp(inputJpg).resize(3000).write(outputJpg, function(err) { - if (err) throw err; - imagemagick.identify(outputJpg, function(err, features) { - if (err) throw err; - assert.strictEqual(3000, features.width); - assert.strictEqual(2449, features.height); - done(); - }); - }); - } -]); +var sharp = require("../index"); +var path = require("path"); +var imagemagick = require("imagemagick"); +var assert = require("assert"); +var async = require("async"); + +var fixturesPath = path.join(__dirname, "fixtures"); + +var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/ +var outputJpg = path.join(fixturesPath, "output.jpg"); + +async.series([ + // Resize with exact crop + function(done) { + sharp(inputJpg).resize(320, 240).write(outputJpg, function(err) { + if (err) throw err; + imagemagick.identify(outputJpg, function(err, features) { + if (err) throw err; + assert.strictEqual(320, features.width); + assert.strictEqual(240, features.height); + done(); + }); + }); + }, + // Resize to fixed width + function(done) { + sharp(inputJpg).resize(320).write(outputJpg, function(err) { + if (err) throw err; + imagemagick.identify(outputJpg, function(err, features) { + if (err) throw err; + assert.strictEqual(320, features.width); + assert.strictEqual(261, features.height); + done(); + }); + }); + }, + // Resize to fixed height + function(done) { + sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) { + if (err) throw err; + imagemagick.identify(outputJpg, function(err, features) { + if (err) throw err; + assert.strictEqual(391, features.width); + assert.strictEqual(320, features.height); + done(); + }); + }); + }, + // Identity transform + function(done) { + sharp(inputJpg).write(outputJpg, function(err) { + if (err) throw err; + imagemagick.identify(outputJpg, function(err, features) { + if (err) throw err; + assert.strictEqual(2725, features.width); + assert.strictEqual(2225, features.height); + done(); + }); + }); + }, + // Upscale + function(done) { + sharp(inputJpg).resize(3000).write(outputJpg, function(err) { + if (err) throw err; + imagemagick.identify(outputJpg, function(err, features) { + if (err) throw err; + assert.strictEqual(3000, features.width); + assert.strictEqual(2449, features.height); + done(); + }); + }); + }, + // Quality + function(done) { + sharp(inputJpg).resize(320, 240).quality(70).jpeg(function(err, buffer70) { + if (err) throw err; + sharp(inputJpg).resize(320, 240).jpeg(function(err, buffer80) { + if (err) throw err; + sharp(inputJpg).resize(320, 240).quality(90).jpeg(function(err, buffer90) { + assert(buffer70.length < buffer80.length); + assert(buffer80.length < buffer90.length); + done(); + }); + }); + }); + } + +]);