diff --git a/README.md b/README.md index 949277b8..74eb87a6 100755 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ sharp('input.jpg').resize(300, 200).toFile('output.jpg', function(err) { ``` ```javascript -var transformer = sharp().resize(300, 200); +var transformer = sharp().resize(300, 200).crop(sharp.gravity.north); readableStream.pipe(transformer).pipe(writableStream); // Read image data from readableStream, resize and write image data to writableStream ``` @@ -183,10 +183,14 @@ Scale output to `width` x `height`. By default, the resized image is cropped to `height` is the Number of pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width. -#### crop() +#### crop([gravity]) Crop the resized image to the exact size specified, the default behaviour. +`gravity`, if present, is an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north`. + +Possible values are `north`, `east`, `south`, `west`, `center` and `centre`. The default gravity is `center`/`centre`. + #### max() Preserving aspect ratio, resize the image to the maximum width or height specified. diff --git a/binding.gyp b/binding.gyp index 2167b80a..dd890fdd 100755 --- a/binding.gyp +++ b/binding.gyp @@ -13,9 +13,9 @@ '= 0 && gravity <= 4) { + this.options.gravity = gravity; + } else { + throw new Error('Unsupported crop gravity ' + gravity); + } + } return this; }; diff --git a/src/sharp.cc b/src/sharp.cc index 1c73cca5..a1a46a47 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "nan.h" @@ -20,6 +21,7 @@ struct resize_baton { int width; int height; bool crop; + int gravity; bool max; VipsExtend extend; bool sharpen; @@ -36,6 +38,7 @@ struct resize_baton { buffer_in_len(0), buffer_out_len(0), crop(false), + gravity(0), max(false), sharpen(false), progressive(false), @@ -94,7 +97,8 @@ static void resize_error(resize_baton *baton, VipsImage *unref) { 2. Use input image EXIF Orientation header (does not support mirroring) 3. Otherwise default to zero, i.e. no rotation */ -static VipsAngle calc_rotation(int const angle, VipsImage const *input) { +static VipsAngle +sharp_calc_rotation(int const angle, VipsImage const *input) { VipsAngle rotate = VIPS_ANGLE_0; if (angle == -1) { const char *exif; @@ -119,6 +123,36 @@ static VipsAngle calc_rotation(int const angle, VipsImage const *input) { return rotate; } +/* + Calculate the (left, top) coordinates of the output image + within the input image, applying the given gravity. +*/ +static std::tuple +sharp_calc_crop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const gravity) { + int left = 0; + int top = 0; + switch (gravity) { + case 1: // North + left = (inWidth - outWidth + 1) / 2; + break; + case 2: // East + left = inWidth - outWidth; + top = (inHeight - outHeight + 1) / 2; + break; + case 3: // South + left = (inWidth - outWidth + 1) / 2; + top = inHeight - outHeight; + break; + case 4: // West + top = (inHeight - outHeight + 1) / 2; + break; + default: // Centre + left = (inWidth - outWidth + 1) / 2; + top = (inHeight - outHeight + 1) / 2; + } + return std::make_tuple(left, top); +} + class ResizeWorker : public NanAsyncWorker { public: ResizeWorker(NanCallback *callback, resize_baton *baton) @@ -189,7 +223,7 @@ class ResizeWorker : public NanAsyncWorker { int inputHeight = in->Ysize; // Calculate angle of rotation, to be carried out later - VipsAngle rotation = calc_rotation(baton->angle, in); + VipsAngle rotation = sharp_calc_rotation(baton->angle, in); if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) { // Swap input output width and height when rotating by 90 or 270 degrees int swap = inputWidth; @@ -337,10 +371,11 @@ class ResizeWorker : public NanAsyncWorker { if (rotated->Xsize != baton->width || rotated->Ysize != baton->height) { if (baton->crop || baton->max) { // Crop/max + int left; + int top; + std::tie(left, top) = sharp_calc_crop(rotated->Xsize, rotated->Ysize, baton->width, baton->height, baton->gravity); int width = std::min(rotated->Xsize, baton->width); int height = std::min(rotated->Ysize, baton->height); - int left = (rotated->Xsize - width + 1) / 2; - int top = (rotated->Ysize - height + 1) / 2; if (vips_extract_area(rotated, &canvased, left, top, width, height, NULL)) { return resize_error(baton, rotated); } @@ -507,6 +542,7 @@ NAN_METHOD(resize) { baton->max = true; } // Other options + baton->gravity = options->Get(NanNew("gravity"))->Int32Value(); baton->sharpen = options->Get(NanNew("sharpen"))->BooleanValue(); baton->interpolator = *String::Utf8Value(options->Get(NanNew("interpolator"))->ToString()); baton->progressive = options->Get(NanNew("progressive"))->BooleanValue(); diff --git a/tests/unit.js b/tests/unit.js index 9ec7e2dd..39229db8 100755 --- a/tests/unit.js +++ b/tests/unit.js @@ -344,6 +344,60 @@ async.series([ var pipeline = sharp().resize(320, 240); readable.pipe(pipeline).pipe(writable) }, + // Crop, gravity=north + function(done) { + sharp(inputJpg).resize(320, 80).crop(sharp.gravity.north).toFile(path.join(fixturesPath, 'output.gravity-north.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(80, info.height); + done(); + }); + }, + // Crop, gravity=east + function(done) { + sharp(inputJpg).resize(80, 320).crop(sharp.gravity.east).toFile(path.join(fixturesPath, 'output.gravity-east.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual(80, info.width); + assert.strictEqual(320, info.height); + done(); + }); + }, + // Crop, gravity=south + function(done) { + sharp(inputJpg).resize(320, 80).crop(sharp.gravity.south).toFile(path.join(fixturesPath, 'output.gravity-south.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(80, info.height); + done(); + }); + }, + // Crop, gravity=west + function(done) { + sharp(inputJpg).resize(80, 320).crop(sharp.gravity.west).toFile(path.join(fixturesPath, 'output.gravity-west.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual(80, info.width); + assert.strictEqual(320, info.height); + done(); + }); + }, + // Crop, gravity=center + function(done) { + sharp(inputJpg).resize(320, 80).crop(sharp.gravity.center).toFile(path.join(fixturesPath, 'output.gravity-center.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(80, info.height); + done(); + }); + }, + // Crop, gravity=centre + function(done) { + sharp(inputJpg).resize(80, 320).crop(sharp.gravity.centre).toFile(path.join(fixturesPath, 'output.gravity-centre.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual(80, info.width); + assert.strictEqual(320, info.height); + done(); + }); + }, // Verify internal counters function(done) { var counters = sharp.counters();