mirror of
https://github.com/lovell/sharp.git
synced 2025-07-22 08:12:27 +02:00
Ensure autoOrient occurs before non-90 rotation #4425
- Separate orient vs rotate ordering logic - Simplify EXIF auto-orient by using only rotate and/or flop
This commit is contained in:
parent
67462bee79
commit
08b4242efe
@ -3,5 +3,8 @@ title: v0.34.4 - TBD
|
|||||||
slug: changelog/v0.34.4
|
slug: changelog/v0.34.4
|
||||||
---
|
---
|
||||||
|
|
||||||
|
* Ensure `autoOrient` occurs before non-90 angle rotation.
|
||||||
|
[#4425](https://github.com/lovell/sharp/issues/4425)
|
||||||
|
|
||||||
* Ensure `autoOrient` removes existing metadata after shrink-on-load.
|
* Ensure `autoOrient` removes existing metadata after shrink-on-load.
|
||||||
[#4431](https://github.com/lovell/sharp/issues/4431)
|
[#4431](https://github.com/lovell/sharp/issues/4431)
|
||||||
|
@ -230,7 +230,8 @@ const Sharp = function (input, options) {
|
|||||||
angle: 0,
|
angle: 0,
|
||||||
rotationAngle: 0,
|
rotationAngle: 0,
|
||||||
rotationBackground: [0, 0, 0, 255],
|
rotationBackground: [0, 0, 0, 255],
|
||||||
rotateBeforePreExtract: false,
|
rotateBefore: false,
|
||||||
|
orientBefore: false,
|
||||||
flip: false,
|
flip: false,
|
||||||
flop: false,
|
flop: false,
|
||||||
extendTop: 0,
|
extendTop: 0,
|
||||||
|
@ -107,7 +107,7 @@ const mapFitToCanvas = {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function isRotationExpected (options) {
|
function isRotationExpected (options) {
|
||||||
return (options.angle % 360) !== 0 || options.input.autoOrient === true || options.rotationAngle !== 0;
|
return (options.angle % 360) !== 0 || options.rotationAngle !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -343,7 +343,7 @@ function resize (widthOrOptions, height, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isRotationExpected(this.options) && isResizeExpected(this.options)) {
|
if (isRotationExpected(this.options) && isResizeExpected(this.options)) {
|
||||||
this.options.rotateBeforePreExtract = true;
|
this.options.rotateBefore = true;
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -490,9 +490,12 @@ function extract (options) {
|
|||||||
// Ensure existing rotation occurs before pre-resize extraction
|
// Ensure existing rotation occurs before pre-resize extraction
|
||||||
if (isRotationExpected(this.options) && !isResizeExpected(this.options)) {
|
if (isRotationExpected(this.options) && !isResizeExpected(this.options)) {
|
||||||
if (this.options.widthPre === -1 || this.options.widthPost === -1) {
|
if (this.options.widthPre === -1 || this.options.widthPost === -1) {
|
||||||
this.options.rotateBeforePreExtract = true;
|
this.options.rotateBefore = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.options.input.autoOrient) {
|
||||||
|
this.options.orientBefore = true;
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,7 +569,7 @@ function trim (options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isRotationExpected(this.options)) {
|
if (isRotationExpected(this.options)) {
|
||||||
this.options.rotateBeforePreExtract = true;
|
this.options.rotateBefore = true;
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -92,30 +92,22 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
// Calculate angle of rotation
|
// Calculate angle of rotation
|
||||||
VipsAngle rotation = VIPS_ANGLE_D0;
|
VipsAngle rotation = VIPS_ANGLE_D0;
|
||||||
VipsAngle autoRotation = VIPS_ANGLE_D0;
|
VipsAngle autoRotation = VIPS_ANGLE_D0;
|
||||||
bool autoFlip = false;
|
|
||||||
bool autoFlop = false;
|
bool autoFlop = false;
|
||||||
|
|
||||||
if (baton->input->autoOrient) {
|
if (baton->input->autoOrient) {
|
||||||
// Rotate and flip image according to Exif orientation
|
// Rotate and flip image according to Exif orientation
|
||||||
std::tie(autoRotation, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
|
std::tie(autoRotation, autoFlop) = CalculateExifRotationAndFlop(sharp::ExifOrientation(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
rotation = CalculateAngleRotation(baton->angle);
|
rotation = CalculateAngleRotation(baton->angle);
|
||||||
|
|
||||||
// Rotate pre-extract
|
bool const shouldRotateBefore = baton->rotateBefore &&
|
||||||
bool const shouldRotateBefore = baton->rotateBeforePreExtract &&
|
(rotation != VIPS_ANGLE_D0 || baton->flip || baton->flop || baton->rotationAngle != 0.0);
|
||||||
(rotation != VIPS_ANGLE_D0 || autoRotation != VIPS_ANGLE_D0 ||
|
bool const shouldOrientBefore = (shouldRotateBefore || baton->orientBefore) &&
|
||||||
autoFlip || baton->flip || autoFlop || baton->flop ||
|
(autoRotation != VIPS_ANGLE_D0 || autoFlop);
|
||||||
baton->rotationAngle != 0.0);
|
|
||||||
|
|
||||||
if (shouldRotateBefore) {
|
|
||||||
image = sharp::StaySequential(image,
|
|
||||||
rotation != VIPS_ANGLE_D0 ||
|
|
||||||
autoRotation != VIPS_ANGLE_D0 ||
|
|
||||||
autoFlip ||
|
|
||||||
baton->flip ||
|
|
||||||
baton->rotationAngle != 0.0);
|
|
||||||
|
|
||||||
|
if (shouldOrientBefore) {
|
||||||
|
image = sharp::StaySequential(image, autoRotation != VIPS_ANGLE_D0);
|
||||||
if (autoRotation != VIPS_ANGLE_D0) {
|
if (autoRotation != VIPS_ANGLE_D0) {
|
||||||
if (autoRotation != VIPS_ANGLE_D180) {
|
if (autoRotation != VIPS_ANGLE_D180) {
|
||||||
MultiPageUnsupported(nPages, "Rotate");
|
MultiPageUnsupported(nPages, "Rotate");
|
||||||
@ -123,14 +115,20 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
image = image.rot(autoRotation);
|
image = image.rot(autoRotation);
|
||||||
autoRotation = VIPS_ANGLE_D0;
|
autoRotation = VIPS_ANGLE_D0;
|
||||||
}
|
}
|
||||||
if (autoFlip != baton->flip) {
|
if (autoFlop) {
|
||||||
image = image.flip(VIPS_DIRECTION_VERTICAL);
|
|
||||||
autoFlip = false;
|
|
||||||
baton->flip = false;
|
|
||||||
}
|
|
||||||
if (autoFlop != baton->flop) {
|
|
||||||
image = image.flip(VIPS_DIRECTION_HORIZONTAL);
|
image = image.flip(VIPS_DIRECTION_HORIZONTAL);
|
||||||
autoFlop = false;
|
autoFlop = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRotateBefore) {
|
||||||
|
image = sharp::StaySequential(image, rotation != VIPS_ANGLE_D0 || baton->flip || baton->rotationAngle != 0.0);
|
||||||
|
if (baton->flip) {
|
||||||
|
image = image.flip(VIPS_DIRECTION_VERTICAL);
|
||||||
|
baton->flip = false;
|
||||||
|
}
|
||||||
|
if (baton->flop) {
|
||||||
|
image = image.flip(VIPS_DIRECTION_HORIZONTAL);
|
||||||
baton->flop = false;
|
baton->flop = false;
|
||||||
}
|
}
|
||||||
if (rotation != VIPS_ANGLE_D0) {
|
if (rotation != VIPS_ANGLE_D0) {
|
||||||
@ -145,6 +143,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
std::vector<double> background;
|
std::vector<double> background;
|
||||||
std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, false);
|
std::tie(image, background) = sharp::ApplyAlpha(image, baton->rotationBackground, false);
|
||||||
image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background)).copy_memory();
|
image = image.rotate(baton->rotationAngle, VImage::option()->set("background", background)).copy_memory();
|
||||||
|
baton->rotationAngle = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,8 +182,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
// When auto-rotating by 90 or 270 degrees, swap the target width and
|
// When auto-rotating by 90 or 270 degrees, swap the target width and
|
||||||
// height to ensure the behavior aligns with how it would have been if
|
// height to ensure the behavior aligns with how it would have been if
|
||||||
// the rotation had taken place *before* resizing.
|
// the rotation had taken place *before* resizing.
|
||||||
if (!baton->rotateBeforePreExtract &&
|
if (autoRotation == VIPS_ANGLE_D90 || autoRotation == VIPS_ANGLE_D270) {
|
||||||
(autoRotation == VIPS_ANGLE_D90 || autoRotation == VIPS_ANGLE_D270)) {
|
|
||||||
std::swap(targetResizeWidth, targetResizeHeight);
|
std::swap(targetResizeWidth, targetResizeHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +204,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
// - input colourspace is not specified;
|
// - input colourspace is not specified;
|
||||||
bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
|
bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
|
||||||
baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold < 0.0 &&
|
baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold < 0.0 &&
|
||||||
baton->colourspacePipeline == VIPS_INTERPRETATION_LAST && !shouldRotateBefore;
|
baton->colourspacePipeline == VIPS_INTERPRETATION_LAST && !(shouldOrientBefore || shouldRotateBefore);
|
||||||
|
|
||||||
if (shouldPreShrink) {
|
if (shouldPreShrink) {
|
||||||
// The common part of the shrink: the bit by which both axes must be shrunk
|
// The common part of the shrink: the bit by which both axes must be shrunk
|
||||||
@ -398,7 +396,6 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
image = sharp::StaySequential(image,
|
image = sharp::StaySequential(image,
|
||||||
autoRotation != VIPS_ANGLE_D0 ||
|
autoRotation != VIPS_ANGLE_D0 ||
|
||||||
baton->flip ||
|
baton->flip ||
|
||||||
autoFlip ||
|
|
||||||
rotation != VIPS_ANGLE_D0);
|
rotation != VIPS_ANGLE_D0);
|
||||||
// Auto-rotate post-extract
|
// Auto-rotate post-extract
|
||||||
if (autoRotation != VIPS_ANGLE_D0) {
|
if (autoRotation != VIPS_ANGLE_D0) {
|
||||||
@ -408,7 +405,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
image = image.rot(autoRotation);
|
image = image.rot(autoRotation);
|
||||||
}
|
}
|
||||||
// Mirror vertically (up-down) about the x-axis
|
// Mirror vertically (up-down) about the x-axis
|
||||||
if (baton->flip != autoFlip) {
|
if (baton->flip) {
|
||||||
image = image.flip(VIPS_DIRECTION_VERTICAL);
|
image = image.flip(VIPS_DIRECTION_VERTICAL);
|
||||||
}
|
}
|
||||||
// Mirror horizontally (left-right) about the y-axis
|
// Mirror horizontally (left-right) about the y-axis
|
||||||
@ -515,7 +512,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rotate post-extract non-90 angle
|
// Rotate post-extract non-90 angle
|
||||||
if (!baton->rotateBeforePreExtract && baton->rotationAngle != 0.0) {
|
if (!baton->rotateBefore && baton->rotationAngle != 0.0) {
|
||||||
MultiPageUnsupported(nPages, "Rotate");
|
MultiPageUnsupported(nPages, "Rotate");
|
||||||
image = sharp::StaySequential(image);
|
image = sharp::StaySequential(image);
|
||||||
std::vector<double> background;
|
std::vector<double> background;
|
||||||
@ -656,21 +653,16 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
if (composite->input->autoOrient) {
|
if (composite->input->autoOrient) {
|
||||||
// Respect EXIF Orientation
|
// Respect EXIF Orientation
|
||||||
VipsAngle compositeAutoRotation = VIPS_ANGLE_D0;
|
VipsAngle compositeAutoRotation = VIPS_ANGLE_D0;
|
||||||
bool compositeAutoFlip = false;
|
|
||||||
bool compositeAutoFlop = false;
|
bool compositeAutoFlop = false;
|
||||||
std::tie(compositeAutoRotation, compositeAutoFlip, compositeAutoFlop) =
|
std::tie(compositeAutoRotation, compositeAutoFlop) =
|
||||||
CalculateExifRotationAndFlip(sharp::ExifOrientation(compositeImage));
|
CalculateExifRotationAndFlop(sharp::ExifOrientation(compositeImage));
|
||||||
|
|
||||||
compositeImage = sharp::RemoveExifOrientation(compositeImage);
|
compositeImage = sharp::RemoveExifOrientation(compositeImage);
|
||||||
compositeImage = sharp::StaySequential(compositeImage,
|
compositeImage = sharp::StaySequential(compositeImage, compositeAutoRotation != VIPS_ANGLE_D0);
|
||||||
compositeAutoRotation != VIPS_ANGLE_D0 || compositeAutoFlip);
|
|
||||||
|
|
||||||
if (compositeAutoRotation != VIPS_ANGLE_D0) {
|
if (compositeAutoRotation != VIPS_ANGLE_D0) {
|
||||||
compositeImage = compositeImage.rot(compositeAutoRotation);
|
compositeImage = compositeImage.rot(compositeAutoRotation);
|
||||||
}
|
}
|
||||||
if (compositeAutoFlip) {
|
|
||||||
compositeImage = compositeImage.flip(VIPS_DIRECTION_VERTICAL);
|
|
||||||
}
|
|
||||||
if (compositeAutoFlop) {
|
if (compositeAutoFlop) {
|
||||||
compositeImage = compositeImage.flip(VIPS_DIRECTION_HORIZONTAL);
|
compositeImage = compositeImage.flip(VIPS_DIRECTION_HORIZONTAL);
|
||||||
}
|
}
|
||||||
@ -1402,21 +1394,20 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
Calculate the angle of rotation and need-to-flip for the given Exif orientation
|
Calculate the angle of rotation and need-to-flip for the given Exif orientation
|
||||||
By default, returns zero, i.e. no rotation.
|
By default, returns zero, i.e. no rotation.
|
||||||
*/
|
*/
|
||||||
std::tuple<VipsAngle, bool, bool>
|
std::tuple<VipsAngle, bool>
|
||||||
CalculateExifRotationAndFlip(int const exifOrientation) {
|
CalculateExifRotationAndFlop(int const exifOrientation) {
|
||||||
VipsAngle rotate = VIPS_ANGLE_D0;
|
VipsAngle rotate = VIPS_ANGLE_D0;
|
||||||
bool flip = false;
|
|
||||||
bool flop = false;
|
bool flop = false;
|
||||||
switch (exifOrientation) {
|
switch (exifOrientation) {
|
||||||
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;
|
||||||
case 7: flip = true; rotate = VIPS_ANGLE_D90; break; // flip 6
|
case 7: flop = true; rotate = VIPS_ANGLE_D270; break;
|
||||||
case 4: flop = true; rotate = VIPS_ANGLE_D180; break; // flop 3
|
case 4: flop = true; rotate = VIPS_ANGLE_D180; break;
|
||||||
case 5: flip = true; rotate = VIPS_ANGLE_D270; break; // flip 8
|
case 5: flop = true; rotate = VIPS_ANGLE_D90; break;
|
||||||
}
|
}
|
||||||
return std::make_tuple(rotate, flip, flop);
|
return std::make_tuple(rotate, flop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -1641,7 +1632,8 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
|||||||
baton->angle = sharp::AttrAsInt32(options, "angle");
|
baton->angle = sharp::AttrAsInt32(options, "angle");
|
||||||
baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
|
baton->rotationAngle = sharp::AttrAsDouble(options, "rotationAngle");
|
||||||
baton->rotationBackground = sharp::AttrAsVectorOfDouble(options, "rotationBackground");
|
baton->rotationBackground = sharp::AttrAsVectorOfDouble(options, "rotationBackground");
|
||||||
baton->rotateBeforePreExtract = sharp::AttrAsBool(options, "rotateBeforePreExtract");
|
baton->rotateBefore = sharp::AttrAsBool(options, "rotateBefore");
|
||||||
|
baton->orientBefore = sharp::AttrAsBool(options, "orientBefore");
|
||||||
baton->flip = sharp::AttrAsBool(options, "flip");
|
baton->flip = sharp::AttrAsBool(options, "flip");
|
||||||
baton->flop = sharp::AttrAsBool(options, "flop");
|
baton->flop = sharp::AttrAsBool(options, "flop");
|
||||||
baton->extendTop = sharp::AttrAsInt32(options, "extendTop");
|
baton->extendTop = sharp::AttrAsInt32(options, "extendTop");
|
||||||
|
@ -115,7 +115,8 @@ struct PipelineBaton {
|
|||||||
int angle;
|
int angle;
|
||||||
double rotationAngle;
|
double rotationAngle;
|
||||||
std::vector<double> rotationBackground;
|
std::vector<double> rotationBackground;
|
||||||
bool rotateBeforePreExtract;
|
bool rotateBefore;
|
||||||
|
bool orientBefore;
|
||||||
bool flip;
|
bool flip;
|
||||||
bool flop;
|
bool flop;
|
||||||
int extendTop;
|
int extendTop;
|
||||||
|
@ -565,9 +565,9 @@ describe('Rotation', function () {
|
|||||||
.raw()
|
.raw()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
assert.strictEqual(r, 60);
|
assert.strictEqual(r, 61);
|
||||||
assert.strictEqual(g, 73);
|
assert.strictEqual(g, 74);
|
||||||
assert.strictEqual(b, 52);
|
assert.strictEqual(b, 51);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Flip and rotate ordering', async () => {
|
it('Flip and rotate ordering', async () => {
|
||||||
@ -648,6 +648,27 @@ describe('Rotation', function () {
|
|||||||
assert.strictEqual(orientation, undefined);
|
assert.strictEqual(orientation, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Auto-orient and rotate 45', async () => {
|
||||||
|
const data = await sharp(fixtures.inputJpgWithLandscapeExif2, { autoOrient: true })
|
||||||
|
.rotate(45)
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const { width, height } = await sharp(data).metadata();
|
||||||
|
assert.strictEqual(width, 742);
|
||||||
|
assert.strictEqual(height, 742);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Auto-orient, extract and rotate 45', async () => {
|
||||||
|
const data = await sharp(fixtures.inputJpgWithLandscapeExif2, { autoOrient: true })
|
||||||
|
.extract({ left: 20, top: 20, width: 200, height: 100 })
|
||||||
|
.rotate(45)
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const { width, height } = await sharp(data).metadata();
|
||||||
|
assert.strictEqual(width, 212);
|
||||||
|
assert.strictEqual(height, 212);
|
||||||
|
});
|
||||||
|
|
||||||
it('Invalid autoOrient throws', () =>
|
it('Invalid autoOrient throws', () =>
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => sharp({ autoOrient: 'fail' }),
|
() => sharp({ autoOrient: 'fail' }),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user