Add support for mirroring #62

This commit is contained in:
Lovell Fuller 2014-10-21 14:47:08 +01:00
parent f214673c3c
commit db6dc6431b
6 changed files with 167 additions and 24 deletions

View File

@ -321,7 +321,15 @@ Rotate the output image by either an explicit angle or auto-orient based on the
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`. `angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
Use this method without `angle` to determine the angle from EXIF data. Mirroring is currently unsupported. Use this method without `angle` to determine the angle from EXIF data. Mirroring is supported and may infer the use of a `flip` operation.
#### flip()
Flip the image about the vertical Y axis. This always occurs after rotation, if any.
#### flop()
Flop the image about the horizontal X axis. This always occurs after rotation, if any.
#### withoutEnlargement() #### withoutEnlargement()

View File

@ -31,6 +31,8 @@ var Sharp = function(input) {
canvas: 'c', canvas: 'c',
gravity: 0, gravity: 0,
angle: 0, angle: 0,
flip: false,
flop: false,
withoutEnlargement: false, withoutEnlargement: false,
interpolator: 'bilinear', interpolator: 'bilinear',
// operations // operations
@ -168,6 +170,22 @@ Sharp.prototype.rotate = function(angle) {
return this; return this;
}; };
/*
Flip the image vertically, about the Y axis
*/
Sharp.prototype.flip = function(flip) {
this.options.flip = (typeof flip === 'boolean') ? flip : true;
return this;
};
/*
Flop the image horizontally, about the X axis
*/
Sharp.prototype.flop = function(flop) {
this.options.flop = (typeof flop === 'boolean') ? flop : true;
return this;
};
/* /*
Do not enlarge the output if the input width *or* height are already less than the required dimensions 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: This is equivalent to GraphicsMagick's ">" geometry option:

View File

@ -43,12 +43,14 @@ struct resize_baton {
bool sharpen; bool sharpen;
double gamma; double gamma;
bool greyscale; bool greyscale;
int angle;
bool flip;
bool flop;
bool progressive; bool progressive;
bool without_enlargement; bool without_enlargement;
VipsAccess access_method; VipsAccess access_method;
int quality; int quality;
int compression_level; int compression_level;
int angle;
std::string err; std::string err;
bool with_metadata; bool with_metadata;
@ -65,6 +67,8 @@ struct resize_baton {
sharpen(false), sharpen(false),
gamma(0.0), gamma(0.0),
greyscale(false), greyscale(false),
flip(false),
flop(false),
progressive(false), progressive(false),
without_enlargement(false), without_enlargement(false),
with_metadata(false) {} with_metadata(false) {}
@ -125,15 +129,16 @@ typedef enum {
} Angle; } Angle;
/* /*
Calculate the angle of rotation for the output image. Calculate the angle of rotation and need-to-flip for the output image.
In order of priority: In order of priority:
1. Use explicitly requested angle (supports 90, 180, 270) 1. Use explicitly requested angle (supports 90, 180, 270)
2. Use input image EXIF Orientation header (does not support mirroring) 2. Use input image EXIF Orientation header - supports mirroring
3. Otherwise default to zero, i.e. no rotation 3. Otherwise default to zero, i.e. no rotation
*/ */
static Angle static std::tuple<Angle, bool>
sharp_calc_rotation(int const angle, VipsImage const *input) { sharp_calc_rotation_and_flip(int const angle, VipsImage const *input) {
Angle rotate = ANGLE_0; Angle rotate = ANGLE_0;
bool flip = FALSE;
if (angle == -1) { if (angle == -1) {
const char *exif; const char *exif;
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) { if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
@ -143,6 +148,17 @@ sharp_calc_rotation(int const angle, VipsImage const *input) {
rotate = ANGLE_180; rotate = ANGLE_180;
} else if (exif[0] == 0x38) { // "8" } else if (exif[0] == 0x38) { // "8"
rotate = ANGLE_270; rotate = ANGLE_270;
} else if (exif[0] == 0x32) { // "2" (flip 1)
flip = TRUE;
} else if (exif[0] == 0x37) { // "7" (flip 6)
rotate = ANGLE_90;
flip = TRUE;
} else if (exif[0] == 0x34) { // "4" (flip 3)
rotate = ANGLE_180;
flip = TRUE;
} else if (exif[0] == 0x35) { // "5" (flip 8)
rotate = ANGLE_270;
flip = TRUE;
} }
} }
} else { } else {
@ -154,7 +170,7 @@ sharp_calc_rotation(int const angle, VipsImage const *input) {
rotate = ANGLE_270; rotate = ANGLE_270;
} }
} }
return rotate; return std::make_tuple(rotate, flip);
} }
/* /*
@ -443,13 +459,19 @@ class ResizeWorker : public NanAsyncWorker {
int inputHeight = image->Ysize; int inputHeight = image->Ysize;
// Calculate angle of rotation, to be carried out later // Calculate angle of rotation, to be carried out later
Angle rotation = sharp_calc_rotation(baton->angle, image); Angle rotation;
bool flip;
std::tie(rotation, flip) = sharp_calc_rotation_and_flip(baton->angle, image);
if (rotation == ANGLE_90 || rotation == ANGLE_270) { if (rotation == ANGLE_90 || rotation == 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;
inputWidth = inputHeight; inputWidth = inputHeight;
inputHeight = swap; inputHeight = swap;
} }
if (flip && !baton->flip) {
// Add flip operation due to EXIF mirroring
baton->flip = TRUE;
}
// Scaling calculations // Scaling calculations
double factor; double factor;
@ -645,6 +667,28 @@ class ResizeWorker : public NanAsyncWorker {
image = rotated; image = rotated;
} }
// Flip (mirror about Y axis)
if (baton->flip) {
VipsImage *flipped = vips_image_new();
vips_object_local(hook, flipped);
if (vips_flip(image, &flipped, VIPS_DIRECTION_VERTICAL, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = flipped;
}
// Flop (mirror about X axis)
if (baton->flop) {
VipsImage *flopped = vips_image_new();
vips_object_local(hook, flopped);
if (vips_flip(image, &flopped, VIPS_DIRECTION_HORIZONTAL, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = flopped;
}
// Crop/embed // Crop/embed
if (image->Xsize != baton->width || image->Ysize != baton->height) { if (image->Xsize != baton->width || image->Ysize != baton->height) {
if (baton->canvas == EMBED) { if (baton->canvas == EMBED) {
@ -960,6 +1004,8 @@ NAN_METHOD(resize) {
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue(); baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue(); baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value(); baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue();
baton->flop = options->Get(NanNew<String>("flop"))->BooleanValue();
// Output options // Output options
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue(); baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value(); baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();

BIN
test/fixtures/Landscape_5.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -10,6 +10,7 @@ module.exports = {
inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/ inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
inputJpgWithExif: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg inputJpgWithExif: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
inputJpgWithExifMirroring: getPath('Landscape_5.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg
inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html
inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg

View File

@ -19,25 +19,42 @@ describe('Rotation', function() {
}); });
it('Input image has Orientation EXIF tag but do not rotate output', function(done) { it('Input image has Orientation EXIF tag but do not rotate output', function(done) {
sharp(fixtures.inputJpgWithExif).resize(320).toBuffer(function(err, data, info) { sharp(fixtures.inputJpgWithExif)
if (err) throw err; .resize(320)
assert.strictEqual(true, data.length > 0); .toBuffer(function(err, data, info) {
assert.strictEqual('jpeg', info.format); if (err) throw err;
assert.strictEqual(320, info.width); assert.strictEqual(true, data.length > 0);
assert.strictEqual(426, info.height); assert.strictEqual('jpeg', info.format);
done(); assert.strictEqual(320, info.width);
}); assert.strictEqual(426, info.height);
done();
});
}); });
it('Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate', function(done) { it('Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate', function(done) {
sharp(fixtures.inputJpgWithExif).rotate().resize(320).toBuffer(function(err, data, info) { sharp(fixtures.inputJpgWithExif)
if (err) throw err; .rotate()
assert.strictEqual(true, data.length > 0); .resize(320)
assert.strictEqual('jpeg', info.format); .toFile(fixtures.path('output.exif.8.jpg'), function(err, info) {
assert.strictEqual(320, info.width); if (err) throw err;
assert.strictEqual(240, info.height); assert.strictEqual('jpeg', info.format);
done(); assert.strictEqual(320, info.width);
}); assert.strictEqual(240, info.height);
done();
});
});
it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', function(done) {
sharp(fixtures.inputJpgWithExifMirroring)
.rotate()
.resize(320)
.toFile(fixtures.path('output.exif.5.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
}); });
it('Attempt to auto-rotate using image that has no EXIF', function(done) { it('Attempt to auto-rotate using image that has no EXIF', function(done) {
@ -61,4 +78,57 @@ describe('Rotation', function() {
done(); done();
}); });
it('Flip - vertical', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flip()
.toFile(fixtures.path('output.flip.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Flop - horizontal', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flop()
.toFile(fixtures.path('output.flop.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Flip and flop', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flop()
.toFile(fixtures.path('output.flip.flop.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Neither flip nor flop', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flip(false)
.flop(false)
.toFile(fixtures.path('output.flip.flop.nope.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
}); });