Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/src/content/docs/api-resize.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ When using a **fit** of `cover` or `contain`, the default **position** is `centr
- `sharp.position`: `top`, `right top`, `right`, `right bottom`, `bottom`, `left bottom`, `left`, `left top`.
- `sharp.gravity`: `north`, `northeast`, `east`, `southeast`, `south`, `southwest`, `west`, `northwest`, `center` or `centre`.
- `sharp.strategy`: `cover` only, dynamically crop using either the `entropy` or `attention` strategy.
- `[x, y]`: horizontal and vertical alignment, each a number between `0` and `1` inclusive.
`[0, 0]` aligns to the top left, `[0.5, 0.5]` to the centre and `[1, 1]` to the bottom right.

Some of these values are based on the [object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) CSS property.
Coordinates align the resized image within the output: for `cover`, `[0.25, 0.75]` removes
25% of the excess width from the left and 75% of the excess height from the top.
For `contain`, these fractions instead determine the padding on the left and top.
Offsets are rounded to the nearest pixel for `cover` and down for `contain`, matching named positions.

The strategy-based approach initially resizes so one dimension is at its target length
then repeatedly ranks edge regions, discarding the edge with the lowest score based on the selected strategy.
Expand Down Expand Up @@ -61,7 +67,7 @@ Previous calls to `resize` in the same pipeline will be ignored.
| [options.width] | <code>number</code> | | An alternative means of specifying `width`. If both are present this takes priority. |
| [options.height] | <code>number</code> | | An alternative means of specifying `height`. If both are present this takes priority. |
| [options.fit] | <code>String</code> | <code>&#x27;cover&#x27;</code> | How the image should be resized/cropped to fit the target dimension(s), one of `cover`, `contain`, `fill`, `inside` or `outside`. |
| [options.position] | <code>String</code> | <code>&#x27;centre&#x27;</code> | A position, gravity or strategy to use when `fit` is `cover` or `contain`. |
| [options.position] | <code>String</code> \| <code>number</code> \| <code>Array.&lt;number&gt;</code> | <code>&#x27;centre&#x27;</code> | A position, gravity, strategy or `[x, y]` alignment to use when `fit` is `cover` or `contain`. Coordinates must each be between `0` and `1` inclusive. |
| [options.background] | <code>String</code> \| <code>Object</code> | <code>{r: 0, g: 0, b: 0, alpha: 1}</code> | background colour when `fit` is `contain`, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency. |
| [options.kernel] | <code>String</code> | <code>&#x27;lanczos3&#x27;</code> | The kernel to use for image reduction and the inferred interpolator to use for upsampling. Use the `fastShrinkOnLoad` option to control kernel vs shrink-on-load. |
| [options.withoutEnlargement] | <code>Boolean</code> | <code>false</code> | Do not scale up if the width *or* height are already less than the target dimensions, equivalent to GraphicsMagick's `>` geometry option. This may result in output dimensions smaller than the target dimensions. |
Expand Down
1 change: 1 addition & 0 deletions lib/constructor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ const Sharp = function (input, options) {
height: -1,
canvas: 'crop',
position: 0,
positionCoordinates: [],
resizeBackground: [0, 0, 0, 255],
angle: 0,
rotationAngle: 0,
Expand Down
5 changes: 3 additions & 2 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,7 @@ declare namespace sharp {
* - sharp.position: top, right top, right, right bottom, bottom, left bottom, left, left top.
* - sharp.gravity: north, northeast, east, southeast, south, southwest, west, northwest, center or centre.
* - sharp.strategy: cover only, dynamically crop using either the entropy or attention strategy. Some of these values are based on the object-position CSS property.
* - [x, y]: horizontal and vertical alignment between 0 and 1 inclusive, from top left to bottom right.
*
* The strategy-based approach resizes so one dimension is at its target length then repeatedly ranks edge regions,
* discarding the edge with the lowest score based on the selected strategy.
Expand Down Expand Up @@ -1587,8 +1588,8 @@ declare namespace sharp {
height?: number | undefined;
/** How the image should be resized to fit both provided dimensions, one of cover, contain, fill, inside or outside. (optional, default 'cover') */
fit?: keyof FitEnum | undefined;
/** Position, gravity or strategy to use when fit is cover or contain. (optional, default 'centre') */
position?: number | string | undefined;
/** Position, gravity, strategy or [x, y] alignment (each between 0 and 1) to use when fit is cover or contain. (optional, default 'centre') */
position?: number | string | readonly [number, number] | undefined;
/** Background colour when using a fit of contain, parsed by the color module, defaults to black without transparency. (optional, default {r:0,g:0,b:0,alpha:1}) */
background?: ColorLike | undefined;
/** The kernel to use for image reduction. (optional, default 'lanczos3') */
Expand Down
31 changes: 24 additions & 7 deletions lib/resize.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,14 @@ function isResizeExpected (options) {
* - `sharp.position`: `top`, `right top`, `right`, `right bottom`, `bottom`, `left bottom`, `left`, `left top`.
* - `sharp.gravity`: `north`, `northeast`, `east`, `southeast`, `south`, `southwest`, `west`, `northwest`, `center` or `centre`.
* - `sharp.strategy`: `cover` only, dynamically crop using either the `entropy` or `attention` strategy.
* - `[x, y]`: horizontal and vertical alignment, each a number between `0` and `1` inclusive.
* `[0, 0]` aligns to the top left, `[0.5, 0.5]` to the centre and `[1, 1]` to the bottom right.
*
* Some of these values are based on the [object-position](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) CSS property.
* Coordinates align the resized image within the output: for `cover`, `[0.25, 0.75]` removes
* 25% of the excess width from the left and 75% of the excess height from the top.
* For `contain`, these fractions instead determine the padding on the left and top.
* Offsets are rounded to the nearest pixel for `cover` and down for `contain`, matching named positions.
*
* The strategy-based approach initially resizes so one dimension is at its target length
* then repeatedly ranks edge regions, discarding the edge with the lowest score based on the selected strategy.
Expand Down Expand Up @@ -246,7 +252,7 @@ function isResizeExpected (options) {
* @param {number} [options.width] - An alternative means of specifying `width`. If both are present this takes priority.
* @param {number} [options.height] - An alternative means of specifying `height`. If both are present this takes priority.
* @param {String} [options.fit='cover'] - How the image should be resized/cropped to fit the target dimension(s), one of `cover`, `contain`, `fill`, `inside` or `outside`.
* @param {String} [options.position='centre'] - A position, gravity or strategy to use when `fit` is `cover` or `contain`.
* @param {String|number|Array<number>} [options.position='centre'] - A position, gravity, strategy or `[x, y]` alignment to use when `fit` is `cover` or `contain`. Coordinates must each be between `0` and `1` inclusive.
* @param {String|Object} [options.background={r: 0, g: 0, b: 0, alpha: 1}] - background colour when `fit` is `contain`, parsed by the [color](https://www.npmjs.org/package/color) module, defaults to black without transparency.
* @param {String} [options.kernel='lanczos3'] - The kernel to use for image reduction and the inferred interpolator to use for upsampling. Use the `fastShrinkOnLoad` option to control kernel vs shrink-on-load.
* @param {Boolean} [options.withoutEnlargement=false] - Do not scale up if the width *or* height are already less than the target dimensions, equivalent to GraphicsMagick's `>` geometry option. This may result in output dimensions smaller than the target dimensions.
Expand Down Expand Up @@ -310,13 +316,24 @@ function resize (widthOrOptions, height, options) {
}
// Position
if (is.defined(options.position)) {
const pos = is.integer(options.position)
? options.position
: strategy[options.position] || position[options.position] || gravity[options.position];
if (is.integer(pos) && (is.inRange(pos, 0, 8) || is.inRange(pos, 16, 17))) {
this.options.position = pos;
if (Array.isArray(options.position)) {
const [x, y] = options.position;
if (options.position.length !== 2 || !is.number(x) || !is.inRange(x, 0, 1) ||
!is.number(y) || !is.inRange(y, 0, 1)) {
throw is.invalidParameterError('position', 'an array of two numbers between 0 and 1', options.position);
}
this.options.position = 0;
this.options.positionCoordinates = [x, y];
} else {
throw is.invalidParameterError('position', 'valid position/gravity/strategy', options.position);
const pos = is.integer(options.position)
? options.position
: strategy[options.position] || position[options.position] || gravity[options.position];
if (is.integer(pos) && (is.inRange(pos, 0, 8) || is.inRange(pos, 16, 17))) {
this.options.position = pos;
this.options.positionCoordinates = [];
} else {
throw is.invalidParameterError('position', 'valid position/gravity/strategy', options.position);
}
}
}
// Background
Expand Down
15 changes: 12 additions & 3 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,14 @@ class PipelineWorker : public Napi::AsyncWorker {
std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);

// Embed
const auto& [left, top] = sharp::CalculateEmbedPosition(
auto [left, top] = sharp::CalculateEmbedPosition(
inputWidth, inputHeight, baton->width, baton->height, baton->position);
const int width = std::max(inputWidth, baton->width);
const int height = std::max(inputHeight, baton->height);
if (baton->positionCoordinates.size() == 2) {
left = static_cast<int>(std::floor((width - inputWidth) * baton->positionCoordinates[0]));
top = static_cast<int>(std::floor((height - inputHeight) * baton->positionCoordinates[1]));
}

image = nPages > 1
? sharp::EmbedMultiPage(image,
Expand All @@ -589,11 +593,15 @@ class PipelineWorker : public Napi::AsyncWorker {

// Crop
if (baton->position < 9) {
// Gravity-based crop
const auto& [left, top] = sharp::CalculateCrop(
// Gravity- or coordinate-based crop
auto [left, top] = sharp::CalculateCrop(
inputWidth, inputHeight, baton->width, baton->height, baton->position);
const int width = std::min(inputWidth, baton->width);
const int height = std::min(inputHeight, baton->height);
if (baton->positionCoordinates.size() == 2) {
left = static_cast<int>(std::round((inputWidth - width) * baton->positionCoordinates[0]));
top = static_cast<int>(std::round((inputHeight - height) * baton->positionCoordinates[1]));
}

image = nPages > 1
? sharp::CropMultiPage(image,
Expand Down Expand Up @@ -1796,6 +1804,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->withoutEnlargement = sharp::AttrAsBool(options, "withoutEnlargement");
baton->withoutReduction = sharp::AttrAsBool(options, "withoutReduction");
baton->position = sharp::AttrAsInt32(options, "position");
baton->positionCoordinates = sharp::AttrAsVectorOfDouble(options, "positionCoordinates");
baton->resizeBackground = sharp::AttrAsVectorOfDouble(options, "resizeBackground");
baton->kernel = sharp::AttrAsEnum<VipsKernel>(options, "kernel", VIPS_TYPE_KERNEL);
baton->fastShrinkOnLoad = sharp::AttrAsBool(options, "fastShrinkOnLoad");
Expand Down
2 changes: 2 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ struct PipelineBaton {
VipsKernel kernel;
sharp::Canvas canvas;
int position;
std::vector<double> positionCoordinates;
std::vector<double> resizeBackground;
bool hasCropOffset;
int cropOffsetLeft;
Expand Down Expand Up @@ -257,6 +258,7 @@ struct PipelineBaton {
kernel(VIPS_KERNEL_LANCZOS3),
canvas(sharp::Canvas::CROP),
position(0),
positionCoordinates{},
resizeBackground{ 0.0, 0.0, 0.0, 255.0 },
hasCropOffset(false),
cropOffsetLeft(0),
Expand Down
12 changes: 12 additions & 0 deletions test/types/sharp.test-d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,18 @@ sharp(input)
// of the image data in inputBuffer
});

// Coordinate-based resize position accepts mutable and readonly pairs.
sharp(input).resize(200, 300, { position: [0.25, 0.75] });
sharp(input).resize({ width: 200, height: 300, fit: 'contain', position: [0.25, 0.75] as const });
const resizePosition: [number, number] = [0.25, 0.75];
sharp(input).resize(200, 300, { position: resizePosition });
// @ts-expect-error
sharp(input).resize(200, 300, { position: [0.5] });
// @ts-expect-error
sharp(input).resize(200, 300, { position: [0.5, 0.5, 0.5] });
// @ts-expect-error
sharp(input).resize(200, 300, { position: ['0.5', 0.5] });

sharp(input).resize({ kernel: 'mks2013' });

transformer = sharp()
Expand Down
12 changes: 12 additions & 0 deletions test/types/sharp.test-d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,18 @@ sharp(input)
// of the image data in inputBuffer
});

// Coordinate-based resize position accepts mutable and readonly pairs.
sharp(input).resize(200, 300, { position: [0.25, 0.75] });
sharp(input).resize({ width: 200, height: 300, fit: 'contain', position: [0.25, 0.75] as const });
const resizePosition: [number, number] = [0.25, 0.75];
sharp(input).resize(200, 300, { position: resizePosition });
// @ts-expect-error
sharp(input).resize(200, 300, { position: [0.5] });
// @ts-expect-error
sharp(input).resize(200, 300, { position: [0.5, 0.5, 0.5] });
// @ts-expect-error
sharp(input).resize(200, 300, { position: ['0.5', 0.5] });

sharp(input).resize({ kernel: 'mks2013' });

transformer = sharp()
Expand Down
Loading