Skip to content

Commit

Permalink
Add support for mirroring #62
Browse files Browse the repository at this point in the history
  • Loading branch information
lovell committed Oct 21, 2014
1 parent f214673 commit db6dc64
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 24 deletions.
10 changes: 9 additions & 1 deletion README.md
Expand Up @@ -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()

Expand Down
18 changes: 18 additions & 0 deletions index.js
Expand Up @@ -31,6 +31,8 @@ var Sharp = function(input) {
canvas: 'c',
gravity: 0,
angle: 0,
flip: false,
flop: false,
withoutEnlargement: false,
interpolator: 'bilinear',
// operations
Expand Down Expand Up @@ -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:
Expand Down
60 changes: 53 additions & 7 deletions src/sharp.cc
Expand Up @@ -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;

Expand All @@ -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) {}
Expand Down Expand Up @@ -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)) {
Expand All @@ -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 {
Expand All @@ -154,7 +170,7 @@ sharp_calc_rotation(int const angle, VipsImage const *input) {
rotate = ANGLE_270;
}
}
return rotate;
return std::make_tuple(rotate, flip);
}

/*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
Binary file added test/fixtures/Landscape_5.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/fixtures/index.js
Expand Up @@ -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

Expand Down
102 changes: 86 additions & 16 deletions test/unit/rotate.js
Expand Up @@ -19,25 +19,42 @@ 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) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(426, info.height);
done();
});
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);
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) {
sharp(fixtures.inputJpgWithExif).rotate().resize(320).toBuffer(function(err, data, 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);
done();
});
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('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) {
Expand All @@ -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();
});
});

});

0 comments on commit db6dc64

Please sign in to comment.