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`.
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()

View File

@ -31,6 +31,8 @@ var Sharp = function(input) {
canvas: 'c',
gravity: 0,
angle: 0,
flip: false,
flop: false,
withoutEnlargement: false,
interpolator: 'bilinear',
// operations
@ -168,6 +170,22 @@ Sharp.prototype.rotate = function(angle) {
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
This is equivalent to GraphicsMagick's ">" geometry option:

View File

@ -43,12 +43,14 @@ struct resize_baton {
bool sharpen;
double gamma;
bool greyscale;
int angle;
bool flip;
bool flop;
bool progressive;
bool without_enlargement;
VipsAccess access_method;
int quality;
int compression_level;
int angle;
std::string err;
bool with_metadata;
@ -65,6 +67,8 @@ struct resize_baton {
sharpen(false),
gamma(0.0),
greyscale(false),
flip(false),
flop(false),
progressive(false),
without_enlargement(false),
with_metadata(false) {}
@ -125,15 +129,16 @@ typedef enum {
} 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:
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
*/
static Angle
sharp_calc_rotation(int const angle, VipsImage const *input) {
static std::tuple<Angle, bool>
sharp_calc_rotation_and_flip(int const angle, VipsImage const *input) {
Angle rotate = ANGLE_0;
bool flip = FALSE;
if (angle == -1) {
const char *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;
} else if (exif[0] == 0x38) { // "8"
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 {
@ -154,7 +170,7 @@ sharp_calc_rotation(int const angle, VipsImage const *input) {
rotate = ANGLE_270;
}
}
return rotate;
return std::make_tuple(rotate, flip);
}
/*
@ -443,13 +459,19 @@ class ResizeWorker : public NanAsyncWorker {
int inputHeight = image->Ysize;
// 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) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = inputWidth;
inputWidth = inputHeight;
inputHeight = swap;
}
if (flip && !baton->flip) {
// Add flip operation due to EXIF mirroring
baton->flip = TRUE;
}
// Scaling calculations
double factor;
@ -645,6 +667,28 @@ class ResizeWorker : public NanAsyncWorker {
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
if (image->Xsize != baton->width || image->Ysize != baton->height) {
if (baton->canvas == EMBED) {
@ -960,6 +1004,8 @@ NAN_METHOD(resize) {
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
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
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
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/
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
inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg

View File

@ -19,7 +19,9 @@ describe('Rotation', function() {
});
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)
.resize(320)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
@ -30,9 +32,24 @@ describe('Rotation', function() {
});
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)
.rotate()
.resize(320)
.toFile(fixtures.path('output.exif.8.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('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(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
@ -61,4 +78,57 @@ describe('Rotation', function() {
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();
});
});
});