Add autoOrient operation and constructor option #4144

This commit is contained in:
Don Denton 2024-07-03 10:34:54 -04:00 committed by Lovell Fuller
parent b7ff2645c4
commit 14c83e1f4c
137 changed files with 428 additions and 136 deletions

View File

@ -46,6 +46,7 @@ and https://www.cairographics.org/operators/
| [images[].input.text.dpi] | <code>number</code> | <code>72</code> | the resolution (size) at which to render the text. Does not take effect if `height` is specified. | | [images[].input.text.dpi] | <code>number</code> | <code>72</code> | the resolution (size) at which to render the text. Does not take effect if `height` is specified. |
| [images[].input.text.rgba] | <code>boolean</code> | <code>false</code> | set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for Pango markup features like `<span foreground="red">Red!</span>`. | | [images[].input.text.rgba] | <code>boolean</code> | <code>false</code> | set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for Pango markup features like `<span foreground="red">Red!</span>`. |
| [images[].input.text.spacing] | <code>number</code> | <code>0</code> | text line height in points. Will use the font line height if none is specified. | | [images[].input.text.spacing] | <code>number</code> | <code>0</code> | text line height in points. Will use the font line height if none is specified. |
| [images[].autoOrient] | <code>Boolean</code> | <code>false</code> | set to true to use EXIF orientation data, if present, to orient the image. |
| [images[].blend] | <code>String</code> | <code>&#x27;over&#x27;</code> | how to blend this image with the image below. | | [images[].blend] | <code>String</code> | <code>&#x27;over&#x27;</code> | how to blend this image with the image below. |
| [images[].gravity] | <code>String</code> | <code>&#x27;centre&#x27;</code> | gravity at which to place the overlay. | | [images[].gravity] | <code>String</code> | <code>&#x27;centre&#x27;</code> | gravity at which to place the overlay. |
| [images[].top] | <code>Number</code> | | the pixel offset from the top edge. | | [images[].top] | <code>Number</code> | | the pixel offset from the top edge. |

View File

@ -33,6 +33,7 @@ where the overall height is the `pageHeight` multiplied by the number of `pages`
| [options.failOn] | <code>string</code> | <code>&quot;&#x27;warning&#x27;&quot;</code> | When to abort processing of invalid pixel data, one of (in order of sensitivity, least to most): 'none', 'truncated', 'error', 'warning'. Higher levels imply lower levels. Invalid metadata will always abort. | | [options.failOn] | <code>string</code> | <code>&quot;&#x27;warning&#x27;&quot;</code> | When to abort processing of invalid pixel data, one of (in order of sensitivity, least to most): 'none', 'truncated', 'error', 'warning'. Higher levels imply lower levels. Invalid metadata will always abort. |
| [options.limitInputPixels] | <code>number</code> \| <code>boolean</code> | <code>268402689</code> | Do not process input images where the number of pixels (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted. An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF). | | [options.limitInputPixels] | <code>number</code> \| <code>boolean</code> | <code>268402689</code> | Do not process input images where the number of pixels (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted. An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF). |
| [options.unlimited] | <code>boolean</code> | <code>false</code> | Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF). | | [options.unlimited] | <code>boolean</code> | <code>false</code> | Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF). |
| [options.autoOrient] | <code>boolean</code> | <code>false</code> | Set this to `true` to rotate/flip the image to match EXIF `Orientation`, if any. |
| [options.sequentialRead] | <code>boolean</code> | <code>true</code> | Set this to `false` to use random access rather than sequential read. Some operations will do this automatically. | | [options.sequentialRead] | <code>boolean</code> | <code>true</code> | Set this to `false` to use random access rather than sequential read. Some operations will do this automatically. |
| [options.density] | <code>number</code> | <code>72</code> | number representing the DPI for vector images in the range 1 to 100000. | | [options.density] | <code>number</code> | <code>72</code> | number representing the DPI for vector images in the range 1 to 100000. |
| [options.ignoreIcc] | <code>number</code> | <code>false</code> | should the embedded ICC profile, if any, be ignored. | | [options.ignoreIcc] | <code>number</code> | <code>false</code> | should the embedded ICC profile, if any, be ignored. |

View File

@ -72,15 +72,9 @@ image
``` ```
**Example** **Example**
```js ```js
// Based on EXIF rotation metadata, get the right-side-up width and height: // Get dimensions taking EXIF Orientation into account.
const { autoOrient } = await sharp(input).metadata();
const size = getNormalSize(await sharp(input).metadata()); const { width, height } = autoOrient;
function getNormalSize({ width, height, orientation }) {
return (orientation || 0) >= 5
? { width: height, height: width }
: { width, height };
}
``` ```

View File

@ -1,22 +1,19 @@
## rotate ## rotate
> rotate([angle], [options]) ⇒ <code>Sharp</code> > rotate([angle], [options]) ⇒ <code>Sharp</code>
Rotate the output image by either an explicit angle Rotate the output image.
or auto-orient based on the EXIF `Orientation` tag.
If an angle is provided, it is converted to a valid positive degree rotation. The provided angle is converted to a valid positive degree rotation.
For example, `-450` will produce a 270 degree rotation. For example, `-450` will produce a 270 degree rotation.
When rotating by an angle other than a multiple of 90, When rotating by an angle other than a multiple of 90,
the background colour can be provided with the `background` option. the background colour can be provided with the `background` option.
If no angle is provided, it is determined from the EXIF data. For backwards compatibility, if no angle is provided, `.autoOrient()` will be called.
Mirroring is supported and may infer the use of a flip operation.
The use of `rotate` without an angle will remove the EXIF `Orientation` tag, if any. Only one rotation can occur per pipeline (aside from an initial call without
arguments to orient via EXIF data). Previous calls to `rotate` in the same
Only one rotation can occur per pipeline. pipeline will be ignored.
Previous calls to `rotate` in the same pipeline will be ignored.
Multi-page images can only be rotated by 180 degrees. Multi-page images can only be rotated by 180 degrees.
@ -35,18 +32,6 @@ for example `.rotate(x).extract(y)` will produce a different result to `.extract
| [options] | <code>Object</code> | | if present, is an Object with optional attributes. | | [options] | <code>Object</code> | | if present, is an Object with optional attributes. |
| [options.background] | <code>string</code> \| <code>Object</code> | <code>&quot;\&quot;#000000\&quot;&quot;</code> | parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. | | [options.background] | <code>string</code> \| <code>Object</code> | <code>&quot;\&quot;#000000\&quot;&quot;</code> | parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. |
**Example**
```js
const pipeline = sharp()
.rotate()
.resize(null, 200)
.toBuffer(function (err, outputBuffer, info) {
// outputBuffer contains 200px high JPEG image data,
// auto-rotated using EXIF Orientation tag
// info.width and info.height contain the dimensions of the resized image
});
readableStream.pipe(pipeline);
```
**Example** **Example**
```js ```js
const rotateThenResize = await sharp(input) const rotateThenResize = await sharp(input)
@ -60,6 +45,34 @@ const resizeThenRotate = await sharp(input)
``` ```
## autoOrient
> autoOrient() ⇒ <code>Sharp</code>
Auto-orient based on the EXIF `Orientation` tag, then remove the tag.
Mirroring is supported and may infer the use of a flip operation.
Previous or subsequent use of `rotate(angle)` and either `flip()` or `flop()`
will logically occur after auto-orientation, regardless of call order.
**Example**
```js
const output = await sharp(input).autoOrient().toBuffer();
```
**Example**
```js
const pipeline = sharp()
.autoOrient()
.resize(null, 200)
.toBuffer(function (err, outputBuffer, info) {
// outputBuffer contains 200px high JPEG image data,
// auto-oriented using EXIF Orientation tag
// info.width and info.height contain the dimensions of the resized image
});
readableStream.pipe(pipeline);
```
## flip ## flip
> flip([flip]) ⇒ <code>Sharp</code> > flip([flip]) ⇒ <code>Sharp</code>

View File

@ -16,6 +16,10 @@ Requires libvips v8.16.0
* Expose WebP `smartDeblock` output option. * Expose WebP `smartDeblock` output option.
* Add `autoOrient` operation and constructor option.
[#4151](https://github.com/lovell/sharp/pull/4151)
[@happycollision](https://github.com/happycollision)
* TypeScript: Ensure channel counts use the correct range. * TypeScript: Ensure channel counts use the correct range.
[#4197](https://github.com/lovell/sharp/pull/4197) [#4197](https://github.com/lovell/sharp/pull/4197)
[@DavidVaness](https://github.com/DavidVaness) [@DavidVaness](https://github.com/DavidVaness)

View File

@ -308,3 +308,6 @@ GitHub: https://github.com/sumitd2
Name: Caleb Meredith Name: Caleb Meredith
GitHub: https://github.com/calebmer GitHub: https://github.com/calebmer
Name: Don Denton
GitHub: https://github.com/happycollision

File diff suppressed because one or more lines are too long

View File

@ -110,6 +110,7 @@ const blend = {
* @param {number} [images[].input.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified. * @param {number} [images[].input.text.dpi=72] - the resolution (size) at which to render the text. Does not take effect if `height` is specified.
* @param {boolean} [images[].input.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for Pango markup features like `<span foreground="red">Red!</span>`. * @param {boolean} [images[].input.text.rgba=false] - set this to true to enable RGBA output. This is useful for colour emoji rendering, or support for Pango markup features like `<span foreground="red">Red!</span>`.
* @param {number} [images[].input.text.spacing=0] - text line height in points. Will use the font line height if none is specified. * @param {number} [images[].input.text.spacing=0] - text line height in points. Will use the font line height if none is specified.
* @param {Boolean} [images[].autoOrient=false] - set to true to use EXIF orientation data, if present, to orient the image.
* @param {String} [images[].blend='over'] - how to blend this image with the image below. * @param {String} [images[].blend='over'] - how to blend this image with the image below.
* @param {String} [images[].gravity='centre'] - gravity at which to place the overlay. * @param {String} [images[].gravity='centre'] - gravity at which to place the overlay.
* @param {Number} [images[].top] - the pixel offset from the top edge. * @param {Number} [images[].top] - the pixel offset from the top edge.

View File

@ -132,6 +132,7 @@ const debuglog = util.debuglog('sharp');
* (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted. * (width x height) exceeds this limit. Assumes image dimensions contained in the input metadata can be trusted.
* An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF). * An integral Number of pixels, zero or false to remove limit, true to use default limit of 268402689 (0x3FFF x 0x3FFF).
* @param {boolean} [options.unlimited=false] - Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF). * @param {boolean} [options.unlimited=false] - Set this to `true` to remove safety features that help prevent memory exhaustion (JPEG, PNG, SVG, HEIF).
* @param {boolean} [options.autoOrient=false] - Set this to `true` to rotate/flip the image to match EXIF `Orientation`, if any.
* @param {boolean} [options.sequentialRead=true] - Set this to `false` to use random access rather than sequential read. Some operations will do this automatically. * @param {boolean} [options.sequentialRead=true] - Set this to `false` to use random access rather than sequential read. Some operations will do this automatically.
* @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000. * @param {number} [options.density=72] - number representing the DPI for vector images in the range 1 to 100000.
* @param {number} [options.ignoreIcc=false] - should the embedded ICC profile, if any, be ignored. * @param {number} [options.ignoreIcc=false] - should the embedded ICC profile, if any, be ignored.
@ -194,7 +195,6 @@ const Sharp = function (input, options) {
canvas: 'crop', canvas: 'crop',
position: 0, position: 0,
resizeBackground: [0, 0, 0, 255], resizeBackground: [0, 0, 0, 255],
useExifOrientation: false,
angle: 0, angle: 0,
rotationAngle: 0, rotationAngle: 0,
rotationBackground: [0, 0, 0, 255], rotationBackground: [0, 0, 0, 255],

82
lib/index.d.ts vendored
View File

@ -364,24 +364,72 @@ declare namespace sharp {
//#region Operation functions //#region Operation functions
/** /**
* Rotate the output image by either an explicit angle or auto-orient based on the EXIF Orientation tag. * Rotate the output image by either an explicit angle
* or auto-orient based on the EXIF `Orientation` tag.
* *
* If an angle is provided, it is converted to a valid positive degree rotation. For example, -450 will produce a 270deg rotation. * If an angle is provided, it is converted to a valid positive degree rotation.
* For example, `-450` will produce a 270 degree rotation.
* *
* When rotating by an angle other than a multiple of 90, the background colour can be provided with the background option. * When rotating by an angle other than a multiple of 90,
* the background colour can be provided with the `background` option.
* *
* If no angle is provided, it is determined from the EXIF data. Mirroring is supported and may infer the use of a flip operation. * If no angle is provided, it is determined from the 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. * The use of `rotate` without an angle will remove the EXIF `Orientation` tag, if any.
* *
* 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). * Only one rotation can occur per pipeline (aside from an initial call without
* @param angle angle of rotation. (optional, default auto) * arguments to orient via EXIF data). Previous calls to `rotate` in the same
* @param options if present, is an Object with optional attributes. * pipeline will be ignored.
*
* Multi-page images can only be rotated by 180 degrees.
*
* Method order is important when rotating, resizing and/or extracting regions,
* for example `.rotate(x).extract(y)` will produce a different result to `.extract(y).rotate(x)`.
*
* @example
* const pipeline = sharp()
* .rotate()
* .resize(null, 200)
* .toBuffer(function (err, outputBuffer, info) {
* // outputBuffer contains 200px high JPEG image data,
* // auto-rotated using EXIF Orientation tag
* // info.width and info.height contain the dimensions of the resized image
* });
* readableStream.pipe(pipeline);
*
* @example
* const rotateThenResize = await sharp(input)
* .rotate(90)
* .resize({ width: 16, height: 8, fit: 'fill' })
* .toBuffer();
* const resizeThenRotate = await sharp(input)
* .resize({ width: 16, height: 8, fit: 'fill' })
* .rotate(90)
* .toBuffer();
*
* @param {number} [angle=auto] angle of rotation.
* @param {Object} [options] - if present, is an Object with optional attributes.
* @param {string|Object} [options.background="#000000"] parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
* @returns {Sharp}
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
* @returns A sharp instance that can be used to chain operations
*/ */
rotate(angle?: number, options?: RotateOptions): Sharp; rotate(angle?: number, options?: RotateOptions): Sharp;
/**
* Alias for calling `rotate()` with no arguments, which orients the image based
* on EXIF orientsion.
*
* This operation is aliased to emphasize its purpose, helping to remove any
* confusion between rotation and orientation.
*
* @example
* const output = await sharp(input).autoOrient().toBuffer();
*
* @returns {Sharp}
*/
autoOrient(): Sharp
/** /**
* Flip the image about the vertical Y axis. This always occurs after rotation, if any. * Flip the image about the vertical Y axis. This always occurs after rotation, if any.
* The use of flip implies the removal of the EXIF Orientation tag, if any. * The use of flip implies the removal of the EXIF Orientation tag, if any.
@ -898,6 +946,13 @@ declare namespace sharp {
} }
interface SharpOptions { interface SharpOptions {
/**
* Auto-orient based on the EXIF `Orientation` tag, if present.
* Mirroring is supported and may infer the use of a flip operation.
*
* Using this option will remove the EXIF `Orientation` tag, if any.
*/
autoOrient?: boolean;
/** /**
* When to abort processing of invalid pixel data, one of (in order of sensitivity): * When to abort processing of invalid pixel data, one of (in order of sensitivity):
* 'none' (least), 'truncated', 'error' or 'warning' (most), highers level imply lower levels, invalid metadata will always abort. (optional, default 'warning') * 'none' (least), 'truncated', 'error' or 'warning' (most), highers level imply lower levels, invalid metadata will always abort. (optional, default 'warning')
@ -1062,6 +1117,13 @@ declare namespace sharp {
width?: number | undefined; width?: number | undefined;
/** Number of pixels high (EXIF orientation is not taken into consideration) */ /** Number of pixels high (EXIF orientation is not taken into consideration) */
height?: number | undefined; height?: number | undefined;
/** Any changed metadata after the image orientation is applied. */
autoOrient: {
/** Number of pixels wide (EXIF orientation is taken into consideration) */
width: number;
/** Number of pixels high (EXIF orientation is taken into consideration) */
height: number;
};
/** Name of colour space interpretation */ /** Name of colour space interpretation */
space?: keyof ColourspaceEnum | undefined; space?: keyof ColourspaceEnum | undefined;
/** Number of bands e.g. 3 for sRGB, 4 for CMYK */ /** Number of bands e.g. 3 for sRGB, 4 for CMYK */
@ -1512,6 +1574,8 @@ declare namespace sharp {
failOn?: FailOnOptions | undefined; failOn?: FailOnOptions | undefined;
/** see sharp() constructor, (optional, default 268402689) */ /** see sharp() constructor, (optional, default 268402689) */
limitInputPixels?: number | boolean | undefined; limitInputPixels?: number | boolean | undefined;
/** see sharp() constructor, (optional, default false) */
autoOrient?: boolean | undefined;
} }
interface TileOptions { interface TileOptions {

View File

@ -24,9 +24,9 @@ const align = {
* @private * @private
*/ */
function _inputOptionsFromObject (obj) { function _inputOptionsFromObject (obj) {
const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground } = obj; const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient } = obj;
return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground].some(is.defined) return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient].some(is.defined)
? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground } ? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient }
: undefined; : undefined;
} }
@ -36,6 +36,7 @@ function _inputOptionsFromObject (obj) {
*/ */
function _createInputDescriptor (input, inputOptions, containerOptions) { function _createInputDescriptor (input, inputOptions, containerOptions) {
const inputDescriptor = { const inputDescriptor = {
autoOrient: false,
failOn: 'warning', failOn: 'warning',
limitInputPixels: Math.pow(0x3FFF, 2), limitInputPixels: Math.pow(0x3FFF, 2),
ignoreIcc: false, ignoreIcc: false,
@ -93,6 +94,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
throw is.invalidParameterError('failOn', 'one of: none, truncated, error, warning', inputOptions.failOn); throw is.invalidParameterError('failOn', 'one of: none, truncated, error, warning', inputOptions.failOn);
} }
} }
// autoOrient
if (is.defined(inputOptions.autoOrient)) {
if (is.bool(inputOptions.autoOrient)) {
inputDescriptor.autoOrient = inputOptions.autoOrient;
} else {
throw is.invalidParameterError('autoOrient', 'boolean', inputOptions.autoOrient);
}
}
// Density // Density
if (is.defined(inputOptions.density)) { if (is.defined(inputOptions.density)) {
if (is.inRange(inputOptions.density, 1, 100000)) { if (is.inRange(inputOptions.density, 1, 100000)) {
@ -475,15 +484,9 @@ function _isStreamInput () {
* }); * });
* *
* @example * @example
* // Based on EXIF rotation metadata, get the right-side-up width and height: * // Get dimensions taking EXIF Orientation into account.
* * const { autoOrient } = await sharp(input).metadata();
* const size = getNormalSize(await sharp(input).metadata()); * const { width, height } = autoOrient;
*
* function getNormalSize({ width, height, orientation }) {
* return (orientation || 0) >= 5
* ? { width: height, height: width }
* : { width, height };
* }
* *
* @param {Function} [callback] - called with the arguments `(err, metadata)` * @param {Function} [callback] - called with the arguments `(err, metadata)`
* @returns {Promise<Object>|Sharp} * @returns {Promise<Object>|Sharp}

View File

@ -18,22 +18,19 @@ const vipsPrecision = {
}; };
/** /**
* Rotate the output image by either an explicit angle * Rotate the output image.
* or auto-orient based on the EXIF `Orientation` tag.
* *
* If an angle is provided, it is converted to a valid positive degree rotation. * The provided angle is converted to a valid positive degree rotation.
* For example, `-450` will produce a 270 degree rotation. * For example, `-450` will produce a 270 degree rotation.
* *
* When rotating by an angle other than a multiple of 90, * When rotating by an angle other than a multiple of 90,
* the background colour can be provided with the `background` option. * the background colour can be provided with the `background` option.
* *
* If no angle is provided, it is determined from the EXIF data. * For backwards compatibility, if no angle is provided, `.autoOrient()` will be called.
* Mirroring is supported and may infer the use of a flip operation.
* *
* The use of `rotate` without an angle will remove the EXIF `Orientation` tag, if any. * Only one rotation can occur per pipeline (aside from an initial call without
* * arguments to orient via EXIF data). Previous calls to `rotate` in the same
* Only one rotation can occur per pipeline. * pipeline will be ignored.
* Previous calls to `rotate` in the same pipeline will be ignored.
* *
* Multi-page images can only be rotated by 180 degrees. * Multi-page images can only be rotated by 180 degrees.
* *
@ -41,17 +38,6 @@ const vipsPrecision = {
* for example `.rotate(x).extract(y)` will produce a different result to `.extract(y).rotate(x)`. * for example `.rotate(x).extract(y)` will produce a different result to `.extract(y).rotate(x)`.
* *
* @example * @example
* const pipeline = sharp()
* .rotate()
* .resize(null, 200)
* .toBuffer(function (err, outputBuffer, info) {
* // outputBuffer contains 200px high JPEG image data,
* // auto-rotated using EXIF Orientation tag
* // info.width and info.height contain the dimensions of the resized image
* });
* readableStream.pipe(pipeline);
*
* @example
* const rotateThenResize = await sharp(input) * const rotateThenResize = await sharp(input)
* .rotate(90) * .rotate(90)
* .resize({ width: 16, height: 8, fit: 'fill' }) * .resize({ width: 16, height: 8, fit: 'fill' })
@ -68,12 +54,15 @@ const vipsPrecision = {
* @throws {Error} Invalid parameters * @throws {Error} Invalid parameters
*/ */
function rotate (angle, options) { function rotate (angle, options) {
if (this.options.useExifOrientation || this.options.angle || this.options.rotationAngle) {
this.options.debuglog('ignoring previous rotate options');
}
if (!is.defined(angle)) { if (!is.defined(angle)) {
this.options.useExifOrientation = true; return this.autoOrient();
} else if (is.integer(angle) && !(angle % 90)) { }
if (this.options.angle || this.options.rotationAngle) {
this.options.debuglog('ignoring previous rotate options');
this.options.angle = 0;
this.options.rotationAngle = 0;
}
if (is.integer(angle) && !(angle % 90)) {
this.options.angle = angle; this.options.angle = angle;
} else if (is.number(angle)) { } else if (is.number(angle)) {
this.options.rotationAngle = angle; this.options.rotationAngle = angle;
@ -92,6 +81,34 @@ function rotate (angle, options) {
return this; return this;
} }
/**
* Auto-orient based on the EXIF `Orientation` tag, then remove the tag.
* Mirroring is supported and may infer the use of a flip operation.
*
* Previous or subsequent use of `rotate(angle)` and either `flip()` or `flop()`
* will logically occur after auto-orientation, regardless of call order.
*
* @example
* const output = await sharp(input).autoOrient().toBuffer();
*
* @example
* const pipeline = sharp()
* .autoOrient()
* .resize(null, 200)
* .toBuffer(function (err, outputBuffer, info) {
* // outputBuffer contains 200px high JPEG image data,
* // auto-oriented using EXIF Orientation tag
* // info.width and info.height contain the dimensions of the resized image
* });
* readableStream.pipe(pipeline);
*
* @returns {Sharp}
*/
function autoOrient () {
this.options.input.autoOrient = true;
return this;
}
/** /**
* Mirror the image vertically (up-down) about the x-axis. * Mirror the image vertically (up-down) about the x-axis.
* This always occurs before rotation, if any. * This always occurs before rotation, if any.
@ -935,6 +952,7 @@ function modulate (options) {
*/ */
module.exports = function (Sharp) { module.exports = function (Sharp) {
Object.assign(Sharp.prototype, { Object.assign(Sharp.prototype, {
autoOrient,
rotate, rotate,
flip, flip,
flop, flop,

View File

@ -107,7 +107,7 @@ const mapFitToCanvas = {
* @private * @private
*/ */
function isRotationExpected (options) { function isRotationExpected (options) {
return (options.angle % 360) !== 0 || options.useExifOrientation === true || options.rotationAngle !== 0; return (options.angle % 360) !== 0 || options.input.autoOrient === true || options.rotationAngle !== 0;
} }
/** /**

View File

@ -166,6 +166,8 @@ namespace sharp {
descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; descriptor->access = AttrAsBool(input, "sequentialRead") ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
// Remove safety features and allow unlimited input // Remove safety features and allow unlimited input
descriptor->unlimited = AttrAsBool(input, "unlimited"); descriptor->unlimited = AttrAsBool(input, "unlimited");
// Use the EXIF orientation to auto orient the image
descriptor->autoOrient = AttrAsBool(input, "autoOrient");
return descriptor; return descriptor;
} }

View File

@ -33,6 +33,7 @@ namespace sharp {
struct InputDescriptor { // NOLINT(runtime/indentation_namespace) struct InputDescriptor { // NOLINT(runtime/indentation_namespace)
std::string name; std::string name;
std::string file; std::string file;
bool autoOrient;
char *buffer; char *buffer;
VipsFailOn failOn; VipsFailOn failOn;
uint64_t limitInputPixels; uint64_t limitInputPixels;
@ -73,6 +74,7 @@ namespace sharp {
std::vector<double> pdfBackground; std::vector<double> pdfBackground;
InputDescriptor(): InputDescriptor():
autoOrient(false),
buffer(nullptr), buffer(nullptr),
failOn(VIPS_FAIL_ON_WARNING), failOn(VIPS_FAIL_ON_WARNING),
limitInputPixels(0x3FFF * 0x3FFF), limitInputPixels(0x3FFF * 0x3FFF),

View File

@ -242,6 +242,15 @@ class MetadataWorker : public Napi::AsyncWorker {
if (baton->orientation > 0) { if (baton->orientation > 0) {
info.Set("orientation", baton->orientation); info.Set("orientation", baton->orientation);
} }
Napi::Object autoOrient = Napi::Object::New(env);
info.Set("autoOrient", autoOrient);
if (baton->orientation >= 5) {
autoOrient.Set("width", baton->height);
autoOrient.Set("height", baton->width);
} else {
autoOrient.Set("width", baton->width);
autoOrient.Set("height", baton->height);
}
if (baton->exifLength > 0) { if (baton->exifLength > 0) {
info.Set("exif", Napi::Buffer<char>::NewOrCopy(env, baton->exif, baton->exifLength, sharp::FreeCallback)); info.Set("exif", Napi::Buffer<char>::NewOrCopy(env, baton->exif, baton->exifLength, sharp::FreeCallback));
} }

View File

@ -63,14 +63,14 @@ class PipelineWorker : public Napi::AsyncWorker {
bool autoFlip = false; bool autoFlip = false;
bool autoFlop = false; bool autoFlop = false;
if (baton->useExifOrientation) { 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, autoFlip, autoFlop) = CalculateExifRotationAndFlip(sharp::ExifOrientation(image));
image = sharp::RemoveExifOrientation(image); image = sharp::RemoveExifOrientation(image);
} else {
rotation = CalculateAngleRotation(baton->angle);
} }
rotation = CalculateAngleRotation(baton->angle);
// Rotate pre-extract // Rotate pre-extract
bool const shouldRotateBefore = baton->rotateBeforePreExtract && bool const shouldRotateBefore = baton->rotateBeforePreExtract &&
(rotation != VIPS_ANGLE_D0 || autoRotation != VIPS_ANGLE_D0 || (rotation != VIPS_ANGLE_D0 || autoRotation != VIPS_ANGLE_D0 ||
@ -92,18 +92,14 @@ class PipelineWorker : public Napi::AsyncWorker {
image = image.rot(autoRotation); image = image.rot(autoRotation);
autoRotation = VIPS_ANGLE_D0; autoRotation = VIPS_ANGLE_D0;
} }
if (autoFlip) { if (autoFlip != baton->flip) {
image = image.flip(VIPS_DIRECTION_VERTICAL); image = image.flip(VIPS_DIRECTION_VERTICAL);
autoFlip = false; autoFlip = false;
} else if (baton->flip) {
image = image.flip(VIPS_DIRECTION_VERTICAL);
baton->flip = false; baton->flip = false;
} }
if (autoFlop) { if (autoFlop != baton->flop) {
image = image.flip(VIPS_DIRECTION_HORIZONTAL); image = image.flip(VIPS_DIRECTION_HORIZONTAL);
autoFlop = false; autoFlop = false;
} else 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) {
@ -396,11 +392,11 @@ 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 != autoFlip) {
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
if (baton->flop || autoFlop) { if (baton->flop != autoFlop) {
image = image.flip(VIPS_DIRECTION_HORIZONTAL); image = image.flip(VIPS_DIRECTION_HORIZONTAL);
} }
// Rotate post-extract 90-angle // Rotate post-extract 90-angle
@ -631,6 +627,30 @@ class PipelineWorker : public Napi::AsyncWorker {
composite->input->access = access; composite->input->access = access;
std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input); std::tie(compositeImage, compositeImageType) = sharp::OpenInput(composite->input);
compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspacePipeline); compositeImage = sharp::EnsureColourspace(compositeImage, baton->colourspacePipeline);
if (composite->input->autoOrient) {
// Respect EXIF Orientation
VipsAngle compositeAutoRotation = VIPS_ANGLE_D0;
bool compositeAutoFlip = false;
bool compositeAutoFlop = false;
std::tie(compositeAutoRotation, compositeAutoFlip, compositeAutoFlop) =
CalculateExifRotationAndFlip(sharp::ExifOrientation(compositeImage));
compositeImage = sharp::RemoveExifOrientation(compositeImage);
compositeImage = sharp::StaySequential(compositeImage,
compositeAutoRotation != VIPS_ANGLE_D0 || compositeAutoFlip);
if (compositeAutoRotation != VIPS_ANGLE_D0) {
compositeImage = compositeImage.rot(compositeAutoRotation);
}
if (compositeAutoFlip) {
compositeImage = compositeImage.flip(VIPS_DIRECTION_VERTICAL);
}
if (compositeAutoFlop) {
compositeImage = compositeImage.flip(VIPS_DIRECTION_HORIZONTAL);
}
}
// Verify within current dimensions // Verify within current dimensions
if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) { if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
throw vips::VError("Image to composite must have same dimensions or smaller"); throw vips::VError("Image to composite must have same dimensions or smaller");
@ -1567,7 +1587,6 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth"); baton->claheWidth = sharp::AttrAsUint32(options, "claheWidth");
baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight"); baton->claheHeight = sharp::AttrAsUint32(options, "claheHeight");
baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope"); baton->claheMaxSlope = sharp::AttrAsUint32(options, "claheMaxSlope");
baton->useExifOrientation = sharp::AttrAsBool(options, "useExifOrientation");
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");

View File

@ -109,7 +109,6 @@ struct PipelineBaton {
int claheWidth; int claheWidth;
int claheHeight; int claheHeight;
int claheMaxSlope; int claheMaxSlope;
bool useExifOrientation;
int angle; int angle;
double rotationAngle; double rotationAngle;
std::vector<double> rotationBackground; std::vector<double> rotationBackground;
@ -282,7 +281,6 @@ struct PipelineBaton {
claheWidth(0), claheWidth(0),
claheHeight(0), claheHeight(0),
claheMaxSlope(3), claheMaxSlope(3),
useExifOrientation(false),
angle(0), angle(0),
rotationAngle(0.0), rotationAngle(0.0),
rotationBackground{ 0.0, 0.0, 0.0, 255.0 }, rotationBackground{ 0.0, 0.0, 0.0, 255.0 },

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Some files were not shown because too many files have changed in this diff Show More