diff --git a/README.md b/README.md index 91dee556..1409d228 100755 --- a/README.md +++ b/README.md @@ -315,6 +315,8 @@ Rotate the output image by either an explicit angle or auto-orient based on the Use this method without `angle` to determine the angle from EXIF data. Mirroring is supported and may infer the use of a `flip` operation. +Method order is important when both rotating and extracting regions, for example `rotate(x).extract(y)` will produce a different result to `extract(y).rotate(x)`. + #### flip() Flip the image about the vertical Y axis. This always occurs after rotation, if any. diff --git a/index.js b/index.js index 9dd53743..b3f238c3 100755 --- a/index.js +++ b/index.js @@ -37,6 +37,7 @@ var Sharp = function(input) { canvas: 'c', gravity: 0, angle: 0, + rotateBeforePreExtract: false, flip: false, flop: false, withoutEnlargement: false, @@ -136,6 +137,10 @@ Sharp.prototype.extract = function(topOffset, leftOffset, width, height) { ['topOffset', 'leftOffset', 'width', 'height'].forEach(function(name, index) { this.options[name + suffix] = values[index]; }.bind(this)); + // Ensure existing rotation occurs before pre-resize extraction + if (suffix === 'Pre' && this.options.angle !== 0) { + this.options.rotateBeforePreExtract = true; + } return this; }; diff --git a/package.json b/package.json index 9258e179..45c7fff4 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharp", - "version": "0.8.4", + "version": "0.9.0", "author": "Lovell Fuller ", "contributors": [ "Pierre Inglebert ", @@ -37,10 +37,10 @@ "bluebird": "^2.6.4", "color": "^0.7.3", "nan": "^1.5.1", - "semver": "^4.1.0" + "semver": "^4.2.0" }, "devDependencies": { - "mocha": "^2.0.1", + "mocha": "^2.1.0", "mocha-jshint": "^0.0.9", "istanbul": "^0.3.5", "coveralls": "^2.11.2" diff --git a/src/resize.cc b/src/resize.cc index 0431d365..2e7fd85f 100755 --- a/src/resize.cc +++ b/src/resize.cc @@ -58,6 +58,7 @@ struct ResizeBaton { double gamma; bool greyscale; int angle; + bool rotateBeforePreExtract; bool flip; bool flop; bool progressive; @@ -146,6 +147,25 @@ class ResizeWorker : public NanAsyncWorker { } vips_object_local(hook, image); + // Calculate angle of rotation + Angle rotation; + bool flip; + std::tie(rotation, flip) = CalculateRotationAndFlip(baton->angle, image); + if (flip && !baton->flip) { + // Add flip operation due to EXIF mirroring + baton->flip = TRUE; + } + + // Rotate pre-extract + if (baton->rotateBeforePreExtract && rotation != Angle::D0) { + VipsImage *rotated; + if (vips_rot(image, &rotated, static_cast(rotation), NULL)) { + return Error(baton, hook); + } + vips_object_local(hook, rotated); + image = rotated; + } + // Pre extraction if (baton->topOffsetPre != -1) { VipsImage *extractedPre; @@ -156,24 +176,15 @@ class ResizeWorker : public NanAsyncWorker { image = extractedPre; } - // Get input image width and height + // Get pre-resize image width and height int inputWidth = image->Xsize; int inputHeight = image->Ysize; - - // Calculate angle of rotation, to be carried out later - Angle rotation; - bool flip; - std::tie(rotation, flip) = CalculateRotationAndFlip(baton->angle, image); if (rotation == Angle::D90 || rotation == Angle::D270) { // 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; - } // Get window size of interpolator, used for determining shrink vs affine int interpolatorWindowSize = InterpolatorWindowSize(baton->interpolator.c_str()); @@ -392,7 +403,7 @@ class ResizeWorker : public NanAsyncWorker { } // Rotate - if (rotation != Angle::D0) { + if (!baton->rotateBeforePreExtract && rotation != Angle::D0) { VipsImage *rotated; if (vips_rot(image, &rotated, static_cast(rotation), NULL)) { return Error(baton, hook); @@ -918,6 +929,7 @@ NAN_METHOD(resize) { baton->gamma = options->Get(NanNew("gamma"))->NumberValue(); baton->greyscale = options->Get(NanNew("greyscale"))->BooleanValue(); baton->angle = options->Get(NanNew("angle"))->Int32Value(); + baton->rotateBeforePreExtract = options->Get(NanNew("rotateBeforePreExtract"))->BooleanValue(); baton->flip = options->Get(NanNew("flip"))->BooleanValue(); baton->flop = options->Get(NanNew("flop"))->BooleanValue(); // Output options diff --git a/test/unit/extract.js b/test/unit/extract.js index 366bd823..035a2883 100755 --- a/test/unit/extract.js +++ b/test/unit/extract.js @@ -92,4 +92,28 @@ describe('Partial image extraction', function() { }); }); + it('Extract then rotate', function(done) { + sharp(fixtures.inputJpg) + .extract(10, 10, 100, 100) + .rotate(90) + .toFile(fixtures.path('output.extract.extract-then-rotate.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + done(); + }); + }); + + it('Rotate then extract', function(done) { + sharp(fixtures.inputJpg) + .rotate(90) + .extract(10, 10, 100, 100) + .toFile(fixtures.path('output.extract.rotate-then-extract.jpg'), function(err, info) { + if (err) throw err; + assert.strictEqual(100, info.width); + assert.strictEqual(100, info.height); + done(); + }); + }); + });