diff --git a/README.md b/README.md index 20c6a09f..dbefdb72 100755 --- a/README.md +++ b/README.md @@ -175,6 +175,12 @@ Rotate the output image by either an explicit angle or auto-orient based on the Use this method without `angle` to determine the angle from EXIF data. +### withoutEnlargement() + +Do not enlarge the output image if the input image width *or* height are already less than the required dimensions. + +This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification". + ### sharpen() Perform a mild sharpen of the resultant image. This typically reduces performance by 30%. diff --git a/index.js b/index.js index bccadf6c..b6c3310e 100755 --- a/index.js +++ b/index.js @@ -12,6 +12,7 @@ var Sharp = function(input) { height: -1, canvas: 'c', angle: 0, + withoutEnlargement: false, sharpen: false, progressive: false, sequentialRead: false, @@ -20,9 +21,9 @@ var Sharp = function(input) { output: '__jpeg' }; if (typeof input === 'string') { - this.options.inFile = input; + this.options.fileIn = input; } else if (typeof input ==='object' && input instanceof Buffer) { - this.options.inBuffer = input; + this.options.bufferIn = input; } else { throw 'Unsupported input ' + typeof input; } @@ -65,6 +66,16 @@ Sharp.prototype.rotate = function(angle) { return this; }; +/* + Do not enlarge the output if the input width *or* height are already less than the required dimensions + This is equivalent to GraphicsMagick's ">" geometry option: + "change the dimensions of the image only if its width or height exceeds the geometry specification" +*/ +Sharp.prototype.withoutEnlargement = function(withoutEnlargement) { + this.options.withoutEnlargement = (typeof withoutEnlargement === 'boolean') ? withoutEnlargement : true; + return this; +}; + Sharp.prototype.sharpen = function(sharpen) { this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true; return this; @@ -127,7 +138,7 @@ Sharp.prototype.toFile = function(output, callback) { if (!output || output.length === 0) { callback('Invalid output'); } else { - if (this.options.inFile === output) { + if (this.options.fileIn === output) { callback('Cannot use same file for input and output'); } else { this._sharp(output, callback); @@ -157,18 +168,8 @@ Sharp.prototype.webp = function(callback) { Sharp.prototype._sharp = function(output, callback) { sharp.resize( - this.options.inFile, - this.options.inBuffer, + this.options, 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, - this.options.angle, callback ); return this; diff --git a/src/sharp.cc b/src/sharp.cc index e70a6133..459d786f 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -24,13 +24,21 @@ struct resize_baton { VipsExtend extend; bool sharpen; bool progressive; + bool without_enlargement; VipsAccess access_method; int quality; int compressionLevel; int angle; std::string err; - resize_baton(): buffer_in_len(0), buffer_out_len(0), crop(false), max(false), sharpen(false), progressive(false) {} + resize_baton(): + buffer_in_len(0), + buffer_out_len(0), + crop(false), + max(false), + sharpen(false), + progressive(false), + without_enlargement(false) {} }; typedef enum { @@ -213,6 +221,17 @@ class ResizeWorker : public NanAsyncWorker { } double residual = static_cast(shrink) / factor; + // Do not enlarge the output if the input width *or* height are already less than the required dimensions + if (baton->without_enlargement) { + if (inputWidth < baton->width || inputHeight < baton->height) { + factor = 1; + shrink = 1; + residual = 0; + baton->width = inputWidth; + baton->height = inputHeight; + } + } + // Try to use libjpeg shrink-on-load int shrink_on_load = 1; if (inputImageType == JPEG) { @@ -403,20 +422,29 @@ class ResizeWorker : public NanAsyncWorker { resize_baton* baton; }; +/* + resize(options, output, callback) +*/ NAN_METHOD(resize) { NanScope(); + // V8 objects are converted to non-V8 types held in the baton struct resize_baton *baton = new resize_baton; - baton->file_in = *String::Utf8Value(args[0]->ToString()); - if (args[1]->IsObject()) { - Local buffer = args[1]->ToObject(); + Local options = args[0]->ToObject(); + + // Input filename + baton->file_in = *String::Utf8Value(options->Get(NanNew("fileIn"))->ToString()); + // Input Buffer object + if (options->Get(NanNew("bufferIn"))->IsObject()) { + Local buffer = options->Get(NanNew("bufferIn"))->ToObject(); baton->buffer_in_len = Buffer::Length(buffer); baton->buffer_in = Buffer::Data(buffer); } - baton->file_out = *String::Utf8Value(args[2]->ToString()); - baton->width = args[3]->Int32Value(); - baton->height = args[4]->Int32Value(); - Local canvas = args[5]->ToString(); + // Output image dimensions + baton->width = options->Get(NanNew("width"))->Int32Value(); + baton->height = options->Get(NanNew("height"))->Int32Value(); + // Canvas options + Local canvas = options->Get(NanNew("canvas"))->ToString(); if (canvas->Equals(NanNew("c"))) { baton->crop = true; } else if (canvas->Equals(NanNew("w"))) { @@ -426,15 +454,19 @@ NAN_METHOD(resize) { } else if (canvas->Equals(NanNew("m"))) { baton->max = true; } - baton->sharpen = args[6]->BooleanValue(); - baton->progressive = 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(); - baton->angle = args[11]->Int32Value(); - - NanCallback *callback = new NanCallback(args[12].As()); + // Other options + baton->sharpen = options->Get(NanNew("sharpen"))->BooleanValue(); + baton->progressive = options->Get(NanNew("progressive"))->BooleanValue(); + baton->without_enlargement = options->Get(NanNew("withoutEnlargement"))->BooleanValue(); + baton->access_method = options->Get(NanNew("sequentialRead"))->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; + baton->quality = options->Get(NanNew("quality"))->Int32Value(); + baton->compressionLevel = options->Get(NanNew("compressionLevel"))->Int32Value(); + baton->angle = options->Get(NanNew("angle"))->Int32Value(); + // Output filename or __format for Buffer + baton->file_out = *String::Utf8Value(args[1]->ToString()); + // Join queue for worker thread + NanCallback *callback = new NanCallback(args[2].As()); NanAsyncQueueWorker(new ResizeWorker(callback, baton)); NanReturnUndefined(); } diff --git a/tests/unit.js b/tests/unit.js index ac237f1e..8db8c823 100755 --- a/tests/unit.js +++ b/tests/unit.js @@ -212,6 +212,30 @@ async.series([ } catch (e) {} assert(!isValid); done(); + }, + // Do not enlarge the output if the input width is already less than the output width + function(done) { + sharp(inputJpg).resize(2800).withoutEnlargement().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(); + }); + }); + }, + // Do not enlarge the output if the input height is already less than the output height + function(done) { + sharp(inputJpg).resize(null, 2300).withoutEnlargement().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(); + }); + }); } ]);