diff --git a/lib/constructor.js b/lib/constructor.js index 76ea64ea..32596649 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -117,6 +117,7 @@ const Sharp = function (input, options) { height: -1, canvas: 'crop', crop: 0, + useExifOrientation: false, angle: 0, rotateBeforePreExtract: false, flip: false, diff --git a/lib/operation.js b/lib/operation.js index 6297d0d7..c0b19539 100644 --- a/lib/operation.js +++ b/lib/operation.js @@ -6,7 +6,10 @@ const is = require('./is'); * Rotate the output image by either an explicit angle * or auto-orient based on the EXIF `Orientation` tag. * - * Use this method without angle to determine the angle from EXIF data. + * If an angle is provided, it is converted to a valid 90/180/270deg rotation. + * For example, `-450` will produce a 270deg rotation. + * + * If no angle is provided, it is determined the from EXIF data. * Mirroring is supported and may infer the use of a flip operation. * * The use of `rotate` implies the removal of the EXIF `Orientation` tag, if any. @@ -25,17 +28,17 @@ const is = require('./is'); * }); * readableStream.pipe(pipeline); * - * @param {Number} [angle=auto] 0, 90, 180 or 270. + * @param {Number} [angle=auto] angle of rotation, must be a multiple of 90. * @returns {Sharp} * @throws {Error} Invalid parameters */ function rotate (angle) { if (!is.defined(angle)) { - this.options.angle = -1; - } else if (is.integer(angle) && is.inArray(angle, [0, 90, 180, 270])) { + this.options.useExifOrientation = true; + } else if (is.integer(angle) && !(angle % 90)) { this.options.angle = angle; } else { - throw new Error('Unsupported angle (0, 90, 180, 270) ' + angle); + throw new Error('Unsupported angle: angle must be a positive/negative multiple of 90 ' + angle); } return this; } @@ -81,7 +84,7 @@ function extract (options) { } }, this); // Ensure existing rotation occurs before pre-resize extraction - if (suffix === 'Pre' && this.options.angle !== 0) { + if (suffix === 'Pre' && ((this.options.angle % 360) !== 0 || this.options.useExifOrientation === true)) { this.options.rotateBeforePreExtract = true; } return this; diff --git a/package.json b/package.json index df99219c..a984a89e 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "Alice Monday ", "Kristo Jorgenson ", "YvesBos ", - "Guy Maliar " + "Guy Maliar ", + "Nicolas Coden " ], "scripts": { "clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*", diff --git a/src/pipeline.cc b/src/pipeline.cc index 9ce53379..3739df42 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -81,16 +81,12 @@ class PipelineWorker : public Nan::AsyncWorker { // Calculate angle of rotation VipsAngle rotation; - bool flip; - bool flop; - std::tie(rotation, flip, flop) = CalculateRotationAndFlip(baton->angle, image); - if (flip && !baton->flip) { - // Add flip operation due to EXIF mirroring - baton->flip = TRUE; - } - if (flop && !baton->flop) { - // Add flip operation due to EXIF mirroring - baton->flop = TRUE; + if (baton->useExifOrientation) { + // Rotate and flip image according to Exif orientation + // (ignore the requested rotation and flip) + std::tie(rotation, baton->flip, baton->flop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image)); + } else { + rotation = CalculateAngleRotation(baton->angle); } // Rotate pre-extract @@ -1065,39 +1061,43 @@ class PipelineWorker : public Nan::AsyncWorker { std::vector> buffersToPersist; /* - 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 - supports mirroring - 3. Otherwise default to zero, i.e. no rotation + Calculate the angle of rotation and need-to-flip for the given Exif orientation + By default, returns zero, i.e. no rotation. */ std::tuple - CalculateRotationAndFlip(int const angle, vips::VImage image) { + CalculateExifRotationAndFlip(int const exifOrientation) { VipsAngle rotate = VIPS_ANGLE_D0; bool flip = FALSE; bool flop = FALSE; - if (angle == -1) { - switch (sharp::ExifOrientation(image)) { - case 6: rotate = VIPS_ANGLE_D90; break; - case 3: rotate = VIPS_ANGLE_D180; break; - case 8: rotate = VIPS_ANGLE_D270; break; - case 2: flop = TRUE; break; // flop 1 - case 7: flip = TRUE; rotate = VIPS_ANGLE_D90; break; // flip 6 - case 4: flop = TRUE; rotate = VIPS_ANGLE_D180; break; // flop 3 - case 5: flip = TRUE; rotate = VIPS_ANGLE_D270; break; // flip 8 - } - } else { - if (angle == 90) { - rotate = VIPS_ANGLE_D90; - } else if (angle == 180) { - rotate = VIPS_ANGLE_D180; - } else if (angle == 270) { - rotate = VIPS_ANGLE_D270; - } + switch (exifOrientation) { + case 6: rotate = VIPS_ANGLE_D90; break; + case 3: rotate = VIPS_ANGLE_D180; break; + case 8: rotate = VIPS_ANGLE_D270; break; + case 2: flop = TRUE; break; // flop 1 + case 7: flip = TRUE; rotate = VIPS_ANGLE_D90; break; // flip 6 + case 4: flop = TRUE; rotate = VIPS_ANGLE_D180; break; // flop 3 + case 5: flip = TRUE; rotate = VIPS_ANGLE_D270; break; // flip 8 } return std::make_tuple(rotate, flip, flop); } + /* + Calculate the rotation for the given angle. + Supports any positive or negative angle that is a multiple of 90. + */ + VipsAngle + CalculateAngleRotation(int angle) { + angle = angle % 360; + if (angle < 0) + angle = 360 + angle; + switch (angle) { + case 90: return VIPS_ANGLE_D90; + case 180: return VIPS_ANGLE_D180; + case 270: return VIPS_ANGLE_D270; + } + return VIPS_ANGLE_D0; + } + /* Assemble the suffix argument to dzsave, which is the format (by extname) alongisde comma-separated arguments to the corresponding `formatsave` vips @@ -1222,6 +1222,7 @@ NAN_METHOD(pipeline) { baton->gamma = AttrTo(options, "gamma"); baton->greyscale = AttrTo(options, "greyscale"); baton->normalise = AttrTo(options, "normalise"); + baton->useExifOrientation = AttrTo(options, "useExifOrientation"); baton->angle = AttrTo(options, "angle"); baton->rotateBeforePreExtract = AttrTo(options, "rotateBeforePreExtract"); baton->flip = AttrTo(options, "flip"); diff --git a/src/pipeline.h b/src/pipeline.h index da3be3d9..47e828e6 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -81,6 +81,7 @@ struct PipelineBaton { double gamma; bool greyscale; bool normalise; + bool useExifOrientation; int angle; bool rotateBeforePreExtract; bool flip; @@ -158,6 +159,7 @@ struct PipelineBaton { gamma(0.0), greyscale(false), normalise(false), + useExifOrientation(false), angle(0), flip(false), flop(false), diff --git a/test/fixtures/320x240.jpg b/test/fixtures/320x240.jpg new file mode 100644 index 00000000..338a76e0 Binary files /dev/null and b/test/fixtures/320x240.jpg differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 5c092be0..d45d8a97 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -64,6 +64,7 @@ module.exports = { inputJpgWithCorruptHeader: getPath('corrupt-header.jpg'), inputJpgWithLowContrast: getPath('low-contrast.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/ inputJpgLarge: getPath('giant-image.jpg'), + inputJpg320x240: getPath('320x240.jpg'), // http://www.andrewault.net/2010/01/26/create-a-test-pattern-video-with-perl/ inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png inputPngWithTransparency: getPath('blackbug.png'), // public domain diff --git a/test/unit/rotate.js b/test/unit/rotate.js index 13502940..f3f38a75 100644 --- a/test/unit/rotate.js +++ b/test/unit/rotate.js @@ -34,6 +34,28 @@ describe('Rotation', function () { }); }); + [-3690, -450, -90, 90, 450, 3690].forEach(function (angle) { + it('Rotate by any 90-multiple angle (' + angle + 'deg)', function (done) { + sharp(fixtures.inputJpg320x240).rotate(angle).toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(240, info.width); + assert.strictEqual(320, info.height); + done(); + }); + }); + }); + + [-3780, -540, 0, 180, 540, 3780].forEach(function (angle) { + it('Rotate by any 180-multiple angle (' + angle + 'deg)', function (done) { + sharp(fixtures.inputJpg320x240).rotate(angle).toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + done(); + }); + }); + }); + it('Rotate by 270 degrees, square output ignoring aspect ratio', function (done) { sharp(fixtures.inputJpg) .resize(240, 240)