Add gravity support to crop #45

This commit is contained in:
Lovell Fuller 2014-08-21 11:01:25 +01:00
parent 8ef0851a49
commit 5fe945fca8
5 changed files with 115 additions and 9 deletions

View File

@ -95,7 +95,7 @@ sharp('input.jpg').resize(300, 200).toFile('output.jpg', function(err) {
``` ```
```javascript ```javascript
var transformer = sharp().resize(300, 200); var transformer = sharp().resize(300, 200).crop(sharp.gravity.north);
readableStream.pipe(transformer).pipe(writableStream); readableStream.pipe(transformer).pipe(writableStream);
// Read image data from readableStream, resize and write image data to 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. `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. 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() #### max()
Preserving aspect ratio, resize the image to the maximum width or height specified. Preserving aspect ratio, resize the image to the maximum width or height specified.

View File

@ -13,9 +13,9 @@
'<!(node -e "require(\'nan\')")' '<!(node -e "require(\'nan\')")'
], ],
'cflags': ['-fexceptions', '-Wall', '-O3'], 'cflags': ['-fexceptions', '-Wall', '-O3'],
'cflags_cc': ['-fexceptions', '-Wall', '-O3'], 'cflags_cc': ['-std=c++0x', '-fexceptions', '-Wall', '-O3'],
'xcode_settings': { 'xcode_settings': {
'OTHER_CFLAGS': ['-fexceptions', '-Wall', '-O3'] 'OTHER_CFLAGS': ['-std=c++11', '-fexceptions', '-Wall', '-O3']
} }
}] }]
} }

View File

@ -15,6 +15,7 @@ var Sharp = function(input) {
width: -1, width: -1,
height: -1, height: -1,
canvas: 'c', canvas: 'c',
gravity: 0,
angle: 0, angle: 0,
withoutEnlargement: false, withoutEnlargement: false,
sharpen: false, sharpen: false,
@ -72,8 +73,19 @@ Sharp.prototype._write = function(chunk, encoding, callback) {
} }
}; };
Sharp.prototype.crop = function() { // Crop this part of the resized image (Center/Centre, North, East, South, West)
module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'south': 3, 'west': 4};
Sharp.prototype.crop = function(gravity) {
this.options.canvas = 'c'; this.options.canvas = 'c';
if (typeof gravity !== 'undefined') {
// Is this a supported gravity?
if (!Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
this.options.gravity = gravity;
} else {
throw new Error('Unsupported crop gravity ' + gravity);
}
}
return this; return this;
}; };

View File

@ -3,6 +3,7 @@
#include <math.h> #include <math.h>
#include <string> #include <string>
#include <string.h> #include <string.h>
#include <tuple>
#include <vips/vips.h> #include <vips/vips.h>
#include "nan.h" #include "nan.h"
@ -20,6 +21,7 @@ struct resize_baton {
int width; int width;
int height; int height;
bool crop; bool crop;
int gravity;
bool max; bool max;
VipsExtend extend; VipsExtend extend;
bool sharpen; bool sharpen;
@ -36,6 +38,7 @@ struct resize_baton {
buffer_in_len(0), buffer_in_len(0),
buffer_out_len(0), buffer_out_len(0),
crop(false), crop(false),
gravity(0),
max(false), max(false),
sharpen(false), sharpen(false),
progressive(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) 2. Use input image EXIF Orientation header (does not support mirroring)
3. Otherwise default to zero, i.e. no rotation 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; VipsAngle rotate = VIPS_ANGLE_0;
if (angle == -1) { if (angle == -1) {
const char *exif; const char *exif;
@ -119,6 +123,36 @@ static VipsAngle calc_rotation(int const angle, VipsImage const *input) {
return rotate; return rotate;
} }
/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given gravity.
*/
static std::tuple<int, int>
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 { class ResizeWorker : public NanAsyncWorker {
public: public:
ResizeWorker(NanCallback *callback, resize_baton *baton) ResizeWorker(NanCallback *callback, resize_baton *baton)
@ -189,7 +223,7 @@ class ResizeWorker : public NanAsyncWorker {
int inputHeight = in->Ysize; int inputHeight = in->Ysize;
// Calculate angle of rotation, to be carried out later // 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) { if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees // Swap input output width and height when rotating by 90 or 270 degrees
int swap = inputWidth; int swap = inputWidth;
@ -337,10 +371,11 @@ class ResizeWorker : public NanAsyncWorker {
if (rotated->Xsize != baton->width || rotated->Ysize != baton->height) { if (rotated->Xsize != baton->width || rotated->Ysize != baton->height) {
if (baton->crop || baton->max) { if (baton->crop || baton->max) {
// Crop/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 width = std::min(rotated->Xsize, baton->width);
int height = std::min(rotated->Ysize, baton->height); 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)) { if (vips_extract_area(rotated, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, rotated); return resize_error(baton, rotated);
} }
@ -507,6 +542,7 @@ NAN_METHOD(resize) {
baton->max = true; baton->max = true;
} }
// Other options // Other options
baton->gravity = options->Get(NanNew<String>("gravity"))->Int32Value();
baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue(); baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString()); baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue(); baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();

View File

@ -344,6 +344,60 @@ async.series([
var pipeline = sharp().resize(320, 240); var pipeline = sharp().resize(320, 240);
readable.pipe(pipeline).pipe(writable) 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 // Verify internal counters
function(done) { function(done) {
var counters = sharp.counters(); var counters = sharp.counters();