mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 18:40:16 +02:00
Add support for any rotation angle (#791)
Allow to provide any positive or negative multiple of 90 to `.rotate(...)`. Negative angles and angles above 360 are converted to valid 0/90/180/270 rotations (0 rotations are still ignored). Changes: - [Node] Add `useExifOrientation` internal variable to know if the Exif orientation must be used instead of the provided angle. This allows to save a negative angle in the `angle` option, because the `-1` special case is not needed. - [Node] Change check for planed-rotation in extract, to prepare a rotation before extraction: check with both `angle` and `useExifOrientation` options. I think this check contains a bit too much logics on rotation options. Maybe we could move this condition to a dedicated function. - [C++] Separate `CalculateRotationAndFlip` into two generic functions: - `CalculateExifRotationAndFlip`: Calculate the angle of rotation and need-to-flip for the given Exif orientation. - `CalculateAngleRotation`: Calculate the rotation for the given angle. One or the other function is used to calculate the rotation, depending on wether the Exif orientation tag or the provided angle must be used. - Add unit tests for `-3690`, `-450`, `-90`, `90`, `450`, `3690` and `-3780`, `-540`, `0`, `180`, `540`, `3780` rotations - Add `320x240` fixture image for tests. Unrelated changes (squashed): - Add ncoden to the list of contributors
This commit is contained in:
parent
d15fb1ab1b
commit
99810c0311
@ -117,6 +117,7 @@ const Sharp = function (input, options) {
|
|||||||
height: -1,
|
height: -1,
|
||||||
canvas: 'crop',
|
canvas: 'crop',
|
||||||
crop: 0,
|
crop: 0,
|
||||||
|
useExifOrientation: false,
|
||||||
angle: 0,
|
angle: 0,
|
||||||
rotateBeforePreExtract: false,
|
rotateBeforePreExtract: false,
|
||||||
flip: false,
|
flip: false,
|
||||||
|
@ -6,7 +6,10 @@ const is = require('./is');
|
|||||||
* Rotate the output image by either an explicit angle
|
* Rotate the output image by either an explicit angle
|
||||||
* or auto-orient based on the EXIF `Orientation` tag.
|
* 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.
|
* 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.
|
* 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);
|
* 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}
|
* @returns {Sharp}
|
||||||
* @throws {Error} Invalid parameters
|
* @throws {Error} Invalid parameters
|
||||||
*/
|
*/
|
||||||
function rotate (angle) {
|
function rotate (angle) {
|
||||||
if (!is.defined(angle)) {
|
if (!is.defined(angle)) {
|
||||||
this.options.angle = -1;
|
this.options.useExifOrientation = true;
|
||||||
} else if (is.integer(angle) && is.inArray(angle, [0, 90, 180, 270])) {
|
} else if (is.integer(angle) && !(angle % 90)) {
|
||||||
this.options.angle = angle;
|
this.options.angle = angle;
|
||||||
} else {
|
} 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;
|
return this;
|
||||||
}
|
}
|
||||||
@ -81,7 +84,7 @@ function extract (options) {
|
|||||||
}
|
}
|
||||||
}, this);
|
}, this);
|
||||||
// Ensure existing rotation occurs before pre-resize extraction
|
// 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;
|
this.options.rotateBeforePreExtract = true;
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
@ -36,7 +36,8 @@
|
|||||||
"Alice Monday <alice0meta@gmail.com>",
|
"Alice Monday <alice0meta@gmail.com>",
|
||||||
"Kristo Jorgenson <kristo.jorgenson@gmail.com>",
|
"Kristo Jorgenson <kristo.jorgenson@gmail.com>",
|
||||||
"YvesBos <yves_bos@outlook.com>",
|
"YvesBos <yves_bos@outlook.com>",
|
||||||
"Guy Maliar <guy@tailorbrands.com>"
|
"Guy Maliar <guy@tailorbrands.com>",
|
||||||
|
"Nicolas Coden <nicolas@ncoden.fr>"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*",
|
"clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*",
|
||||||
|
@ -81,16 +81,12 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
|
|
||||||
// Calculate angle of rotation
|
// Calculate angle of rotation
|
||||||
VipsAngle rotation;
|
VipsAngle rotation;
|
||||||
bool flip;
|
if (baton->useExifOrientation) {
|
||||||
bool flop;
|
// Rotate and flip image according to Exif orientation
|
||||||
std::tie(rotation, flip, flop) = CalculateRotationAndFlip(baton->angle, image);
|
// (ignore the requested rotation and flip)
|
||||||
if (flip && !baton->flip) {
|
std::tie(rotation, baton->flip, baton->flop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
|
||||||
// Add flip operation due to EXIF mirroring
|
} else {
|
||||||
baton->flip = TRUE;
|
rotation = CalculateAngleRotation(baton->angle);
|
||||||
}
|
|
||||||
if (flop && !baton->flop) {
|
|
||||||
// Add flip operation due to EXIF mirroring
|
|
||||||
baton->flop = TRUE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rotate pre-extract
|
// Rotate pre-extract
|
||||||
@ -1065,39 +1061,43 @@ class PipelineWorker : public Nan::AsyncWorker {
|
|||||||
std::vector<v8::Local<v8::Object>> buffersToPersist;
|
std::vector<v8::Local<v8::Object>> buffersToPersist;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Calculate the angle of rotation and need-to-flip for the output image.
|
Calculate the angle of rotation and need-to-flip for the given Exif orientation
|
||||||
In order of priority:
|
By default, returns zero, i.e. no rotation.
|
||||||
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
|
|
||||||
*/
|
*/
|
||||||
std::tuple<VipsAngle, bool, bool>
|
std::tuple<VipsAngle, bool, bool>
|
||||||
CalculateRotationAndFlip(int const angle, vips::VImage image) {
|
CalculateExifRotationAndFlip(int const exifOrientation) {
|
||||||
VipsAngle rotate = VIPS_ANGLE_D0;
|
VipsAngle rotate = VIPS_ANGLE_D0;
|
||||||
bool flip = FALSE;
|
bool flip = FALSE;
|
||||||
bool flop = FALSE;
|
bool flop = FALSE;
|
||||||
if (angle == -1) {
|
switch (exifOrientation) {
|
||||||
switch (sharp::ExifOrientation(image)) {
|
case 6: rotate = VIPS_ANGLE_D90; break;
|
||||||
case 6: rotate = VIPS_ANGLE_D90; break;
|
case 3: rotate = VIPS_ANGLE_D180; break;
|
||||||
case 3: rotate = VIPS_ANGLE_D180; break;
|
case 8: rotate = VIPS_ANGLE_D270; break;
|
||||||
case 8: rotate = VIPS_ANGLE_D270; break;
|
case 2: flop = TRUE; break; // flop 1
|
||||||
case 2: flop = TRUE; break; // flop 1
|
case 7: flip = TRUE; rotate = VIPS_ANGLE_D90; break; // flip 6
|
||||||
case 7: flip = TRUE; rotate = VIPS_ANGLE_D90; break; // flip 6
|
case 4: flop = TRUE; rotate = VIPS_ANGLE_D180; break; // flop 3
|
||||||
case 4: flop = TRUE; rotate = VIPS_ANGLE_D180; break; // flop 3
|
case 5: flip = TRUE; rotate = VIPS_ANGLE_D270; break; // flip 8
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return std::make_tuple(rotate, flip, flop);
|
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)
|
Assemble the suffix argument to dzsave, which is the format (by extname)
|
||||||
alongisde comma-separated arguments to the corresponding `formatsave` vips
|
alongisde comma-separated arguments to the corresponding `formatsave` vips
|
||||||
@ -1222,6 +1222,7 @@ NAN_METHOD(pipeline) {
|
|||||||
baton->gamma = AttrTo<double>(options, "gamma");
|
baton->gamma = AttrTo<double>(options, "gamma");
|
||||||
baton->greyscale = AttrTo<bool>(options, "greyscale");
|
baton->greyscale = AttrTo<bool>(options, "greyscale");
|
||||||
baton->normalise = AttrTo<bool>(options, "normalise");
|
baton->normalise = AttrTo<bool>(options, "normalise");
|
||||||
|
baton->useExifOrientation = AttrTo<bool>(options, "useExifOrientation");
|
||||||
baton->angle = AttrTo<int32_t>(options, "angle");
|
baton->angle = AttrTo<int32_t>(options, "angle");
|
||||||
baton->rotateBeforePreExtract = AttrTo<bool>(options, "rotateBeforePreExtract");
|
baton->rotateBeforePreExtract = AttrTo<bool>(options, "rotateBeforePreExtract");
|
||||||
baton->flip = AttrTo<bool>(options, "flip");
|
baton->flip = AttrTo<bool>(options, "flip");
|
||||||
|
@ -81,6 +81,7 @@ struct PipelineBaton {
|
|||||||
double gamma;
|
double gamma;
|
||||||
bool greyscale;
|
bool greyscale;
|
||||||
bool normalise;
|
bool normalise;
|
||||||
|
bool useExifOrientation;
|
||||||
int angle;
|
int angle;
|
||||||
bool rotateBeforePreExtract;
|
bool rotateBeforePreExtract;
|
||||||
bool flip;
|
bool flip;
|
||||||
@ -158,6 +159,7 @@ struct PipelineBaton {
|
|||||||
gamma(0.0),
|
gamma(0.0),
|
||||||
greyscale(false),
|
greyscale(false),
|
||||||
normalise(false),
|
normalise(false),
|
||||||
|
useExifOrientation(false),
|
||||||
angle(0),
|
angle(0),
|
||||||
flip(false),
|
flip(false),
|
||||||
flop(false),
|
flop(false),
|
||||||
|
BIN
test/fixtures/320x240.jpg
vendored
Normal file
BIN
test/fixtures/320x240.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
1
test/fixtures/index.js
vendored
1
test/fixtures/index.js
vendored
@ -64,6 +64,7 @@ module.exports = {
|
|||||||
inputJpgWithCorruptHeader: getPath('corrupt-header.jpg'),
|
inputJpgWithCorruptHeader: getPath('corrupt-header.jpg'),
|
||||||
inputJpgWithLowContrast: getPath('low-contrast.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
|
inputJpgWithLowContrast: getPath('low-contrast.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
|
||||||
inputJpgLarge: getPath('giant-image.jpg'),
|
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
|
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
||||||
inputPngWithTransparency: getPath('blackbug.png'), // public domain
|
inputPngWithTransparency: getPath('blackbug.png'), // public domain
|
||||||
|
@ -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) {
|
it('Rotate by 270 degrees, square output ignoring aspect ratio', function (done) {
|
||||||
sharp(fixtures.inputJpg)
|
sharp(fixtures.inputJpg)
|
||||||
.resize(240, 240)
|
.resize(240, 240)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user