Skip to content

Commit

Permalink
Add autoOrient operation and constructor option #4144
Browse files Browse the repository at this point in the history
  • Loading branch information
happycollision authored and lovell committed Jan 17, 2025
1 parent b7ff264 commit 14c83e1
Show file tree
Hide file tree
Showing 137 changed files with 427 additions and 135 deletions.
1 change: 1 addition & 0 deletions docs/api-composite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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[].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[].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. |
Expand Down
1 change: 1 addition & 0 deletions docs/api-constructor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.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.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. |
Expand Down
12 changes: 3 additions & 9 deletions docs/api-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,9 @@ image
```
**Example**
```js
// Based on EXIF rotation metadata, get the right-side-up width and height:

const size = getNormalSize(await sharp(input).metadata());

function getNormalSize({ width, height, orientation }) {
return (orientation || 0) >= 5
? { width: height, height: width }
: { width, height };
}
// Get dimensions taking EXIF Orientation into account.
const { autoOrient } = await sharp(input).metadata();
const { width, height } = autoOrient;
```


Expand Down
55 changes: 34 additions & 21 deletions docs/api-operation.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
## rotate
> rotate([angle], [options]) ⇒ <code>Sharp</code>
Rotate the output image by either an explicit angle
or auto-orient based on the EXIF `Orientation` tag.
Rotate the output image.

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.

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.

The use of `rotate` without an angle will remove the EXIF `Orientation` tag, if any.
For backwards compatibility, if no angle is provided, `.autoOrient()` will be called.

Only one rotation can occur per pipeline.
Previous calls to `rotate` in the same pipeline will be ignored.
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
pipeline will be ignored.

Multi-page images can only be rotated by 180 degrees.

Expand All @@ -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.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**
```js
const rotateThenResize = await sharp(input)
Expand All @@ -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]) ⇒ <code>Sharp</code>
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Requires libvips v8.16.0

* 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.
[#4197](https://github.com/lovell/sharp/pull/4197)
[@DavidVaness](https://github.com/DavidVaness)
Expand Down
3 changes: 3 additions & 0 deletions docs/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,6 @@ GitHub: https://github.com/sumitd2

Name: Caleb Meredith
GitHub: https://github.com/calebmer

Name: Don Denton
GitHub: https://github.com/happycollision
2 changes: 1 addition & 1 deletion docs/search-index.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/composite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {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 {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[].gravity='centre'] - gravity at which to place the overlay.
* @param {Number} [images[].top] - the pixel offset from the top edge.
Expand Down
2 changes: 1 addition & 1 deletion lib/constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* 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.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 {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.
Expand Down Expand Up @@ -194,7 +195,6 @@ const Sharp = function (input, options) {
canvas: 'crop',
position: 0,
resizeBackground: [0, 0, 0, 255],
useExifOrientation: false,
angle: 0,
rotationAngle: 0,
rotationBackground: [0, 0, 0, 255],
Expand Down
82 changes: 73 additions & 9 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,24 +364,72 @@ declare namespace sharp {
//#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).
* @param angle angle of rotation. (optional, default auto)
* @param options if present, is an Object with optional attributes.
* 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
* 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
* @returns A sharp instance that can be used to chain operations
*/
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.
* The use of flip implies the removal of the EXIF Orientation tag, if any.
Expand Down Expand Up @@ -898,6 +946,13 @@ declare namespace sharp {
}

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):
* 'none' (least), 'truncated', 'error' or 'warning' (most), highers level imply lower levels, invalid metadata will always abort. (optional, default 'warning')
Expand Down Expand Up @@ -1062,6 +1117,13 @@ declare namespace sharp {
width?: number | undefined;
/** Number of pixels high (EXIF orientation is not taken into consideration) */
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 */
space?: keyof ColourspaceEnum | undefined;
/** Number of bands e.g. 3 for sRGB, 4 for CMYK */
Expand Down Expand Up @@ -1512,6 +1574,8 @@ declare namespace sharp {
failOn?: FailOnOptions | undefined;
/** see sharp() constructor, (optional, default 268402689) */
limitInputPixels?: number | boolean | undefined;
/** see sharp() constructor, (optional, default false) */
autoOrient?: boolean | undefined;
}

interface TileOptions {
Expand Down
27 changes: 15 additions & 12 deletions lib/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ const align = {
* @private
*/
function _inputOptionsFromObject (obj) {
const { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground } = obj;
return [raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground].some(is.defined)
? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground }
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, autoOrient].some(is.defined)
? { raw, density, limitInputPixels, ignoreIcc, unlimited, sequentialRead, failOn, failOnError, animated, page, pages, subifd, pdfBackground, autoOrient }
: undefined;
}

Expand All @@ -36,6 +36,7 @@ function _inputOptionsFromObject (obj) {
*/
function _createInputDescriptor (input, inputOptions, containerOptions) {
const inputDescriptor = {
autoOrient: false,
failOn: 'warning',
limitInputPixels: Math.pow(0x3FFF, 2),
ignoreIcc: false,
Expand Down Expand Up @@ -93,6 +94,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
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
if (is.defined(inputOptions.density)) {
if (is.inRange(inputOptions.density, 1, 100000)) {
Expand Down Expand Up @@ -475,15 +484,9 @@ function _isStreamInput () {
* });
*
* @example
* // Based on EXIF rotation metadata, get the right-side-up width and height:
*
* const size = getNormalSize(await sharp(input).metadata());
*
* function getNormalSize({ width, height, orientation }) {
* return (orientation || 0) >= 5
* ? { width: height, height: width }
* : { width, height };
* }
* // Get dimensions taking EXIF Orientation into account.
* const { autoOrient } = await sharp(input).metadata();
* const { width, height } = autoOrient;
*
* @param {Function} [callback] - called with the arguments `(err, metadata)`
* @returns {Promise<Object>|Sharp}
Expand Down
Loading

0 comments on commit 14c83e1

Please sign in to comment.