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
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.

View File

@ -13,9 +13,9 @@
'<!(node -e "require(\'nan\')")'
],
'cflags': ['-fexceptions', '-Wall', '-O3'],
'cflags_cc': ['-fexceptions', '-Wall', '-O3'],
'cflags_cc': ['-std=c++0x', '-fexceptions', '-Wall', '-O3'],
'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,
height: -1,
canvas: 'c',
gravity: 0,
angle: 0,
withoutEnlargement: 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';
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;
};

View File

@ -3,6 +3,7 @@
#include <math.h>
#include <string>
#include <string.h>
#include <tuple>
#include <vips/vips.h>
#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<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 {
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<String>("gravity"))->Int32Value();
baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();

View File

@ -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();