Compare commits

..

7 Commits

Author SHA1 Message Date
Lovell Fuller
73bec629cf Docs: website already hosts images, no need to use jsdelivr 2025-05-05 13:20:49 +01:00
Lovell Fuller
758d7e63cc Docs: changelog entry for #4387 2025-05-05 13:19:10 +01:00
Stephen Tse
eba3e9aeb2 TypeScript: Add smartDeblock definition to WebpOptions (#4387) 2025-05-05 13:15:50 +01:00
Lovell Fuller
701143afb3 Ensure animated GIF to WebP conversion retains loop #3394 2025-04-23 15:43:58 +01:00
Steven D'Onfro
38b6f44611 TypeScript: Add missing properties for animated images (#4369) 2025-04-12 11:38:24 +01:00
Lovell Fuller
5b5dfbad77 Ensure pdfBackground constructor property is used #4207
Slightly refactor the way background colours are set
2025-04-09 22:21:14 +01:00
Kleis Auke Wolthuizen
a642767329 Docs: update path to api-resize-fit.svg (#4368) 2025-04-08 10:15:29 +01:00
14 changed files with 72 additions and 53 deletions

View File

@@ -1,6 +1,6 @@
# sharp
<img src="https://cdn.jsdelivr.net/gh/lovell/sharp@main/docs/public/sharp-logo.svg" width="160" height="160" alt="sharp logo" align="right">
<img src="https://sharp.pixelplumbing.com/sharp-logo.svg" width="160" height="160" alt="sharp logo" align="right">
The typical use case for this high speed Node-API module
is to convert large images in common formats to

View File

@@ -18,7 +18,7 @@ export default defineConfig({
tag: 'meta',
attrs: {
'http-equiv': 'Content-Security-Policy',
content: "default-src 'self'; connect-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.jsdelivr.net/gh/lovell/; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com/beacon.min.js/;"
content: "default-src 'self'; connect-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com/beacon.min.js/;"
}
}, {
tag: 'link',

View File

@@ -17,7 +17,7 @@ When both a `width` and `height` are provided, the possible methods by which the
Some of these values are based on the [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) CSS property.
<img alt="Examples of various values for the fit property when resizing" width="100%" style="aspect-ratio: 998/243" src="https://cdn.jsdelivr.net/gh/lovell/sharp@main/docs/image/api-resize-fit.svg">
<img alt="Examples of various values for the fit property when resizing" width="100%" style="aspect-ratio: 998/243" src="/api-resize-fit.svg">
When using a **fit** of `cover` or `contain`, the default **position** is `centre`. Other options are:
- `sharp.position`: `top`, `right top`, `right`, `right bottom`, `bottom`, `left bottom`, `left`, `left top`.

View File

@@ -6,6 +6,18 @@ title: Changelog
Requires libvips v8.16.1
### v0.34.2 - TBD
* Ensure animated GIF to WebP conversion retains loop (regression in 0.34.0).
[#3394](https://github.com/lovell/sharp/issues/3394)
* Ensure `pdfBackground` constructor property is used.
[#4207](https://github.com/lovell/sharp/pull/4207)
* TypeScript: Ensure `smartDeblock` property is included in WebP definition.
[#4387](https://github.com/lovell/sharp/pull/4387)
[@Stephen-X](https://github.com/Stephen-X)
### v0.34.1 - 7th April 2025
* TypeScript: Ensure new `autoOrient` property is optional.

View File

@@ -2,7 +2,7 @@
title: "High performance Node.js image processing"
---
<img src="https://cdn.jsdelivr.net/gh/lovell/sharp@main/docs/public/sharp-logo.svg" width="160" height="160" alt="sharp logo" align="right">
<img src="/sharp-logo.svg" width="160" height="160" alt="sharp logo" align="right">
The typical use case for this high speed Node-API module
is to convert large images in common formats to

View File

@@ -134,6 +134,26 @@ function toColorspace (colorspace) {
return this.toColourspace(colorspace);
}
/**
* Create a RGBA colour array from a given value.
* @private
* @param {string|Object} value
* @throws {Error} Invalid value
*/
function _getBackgroundColourOption (value) {
if (is.object(value) || is.string(value)) {
const colour = color(value);
return [
colour.red(),
colour.green(),
colour.blue(),
Math.round(colour.alpha() * 255)
];
} else {
throw is.invalidParameterError('background', 'object or string', value);
}
}
/**
* Update a colour attribute of the this.options Object.
* @private
@@ -143,17 +163,7 @@ function toColorspace (colorspace) {
*/
function _setBackgroundColourOption (key, value) {
if (is.defined(value)) {
if (is.object(value) || is.string(value)) {
const colour = color(value);
this.options[key] = [
colour.red(),
colour.green(),
colour.blue(),
Math.round(colour.alpha() * 255)
];
} else {
throw is.invalidParameterError('background', 'object or string', value);
}
this.options[key] = _getBackgroundColourOption(value);
}
}
@@ -173,6 +183,7 @@ module.exports = function (Sharp) {
toColourspace,
toColorspace,
// Private
_getBackgroundColourOption,
_setBackgroundColourOption
});
// Class attributes

View File

@@ -298,7 +298,7 @@ const Sharp = function (input, options) {
withExif: {},
withExifMerge: true,
resolveWithObject: false,
loop: 1,
loop: -1,
delay: [],
// output format
jpegQuality: 80,

6
lib/index.d.ts vendored
View File

@@ -1336,6 +1336,8 @@ declare namespace sharp {
nearLossless?: boolean | undefined;
/** Use high quality chroma subsampling (optional, default false) */
smartSubsample?: boolean | undefined;
/** Auto-adjust the deblocking filter, slow but can improve low contrast edges (optional, default false) */
smartDeblock?: boolean | undefined;
/** Level of CPU effort to reduce file size, integer 0-6 (optional, default 4) */
effort?: number | undefined;
/** Prevent use of animation key frames to minimise file size (slow) (optional, default false) */
@@ -1699,6 +1701,10 @@ declare namespace sharp {
/** When using the attention crop strategy, the focal point of the cropped region */
attentionX?: number | undefined;
attentionY?: number | undefined;
/** Number of pages/frames contained within the image, with support for TIFF, HEIF, PDF, animated GIF and animated WebP */
pages?: number | undefined;
/** Number of pixels high each page in a multi-page image will be. */
pageHeight?: number | undefined;
}
interface AvailableFormatInfo {

View File

@@ -3,7 +3,6 @@
'use strict';
const color = require('color');
const is = require('./is');
const sharp = require('./sharp');
@@ -249,7 +248,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
}
// PDF background colour
if (is.defined(inputOptions.pdfBackground)) {
this._setBackgroundColourOption('pdfBackground', inputOptions.pdfBackground);
inputDescriptor.pdfBackground = this._getBackgroundColourOption(inputOptions.pdfBackground);
}
// Create new image
if (is.defined(inputOptions.create)) {
@@ -288,13 +287,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
if (!is.inRange(inputOptions.create.channels, 3, 4)) {
throw is.invalidParameterError('create.channels', 'number between 3 and 4', inputOptions.create.channels);
}
const background = color(inputOptions.create.background);
inputDescriptor.createBackground = [
background.red(),
background.green(),
background.blue(),
Math.round(background.alpha() * 255)
];
inputDescriptor.createBackground = this._getBackgroundColourOption(inputOptions.create.background);
} else {
throw new Error('Expected valid noise or background to create a new input image');
}
@@ -410,13 +403,7 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
}
}
if (is.defined(inputOptions.join.background)) {
const background = color(inputOptions.join.background);
inputDescriptor.joinBackground = [
background.red(),
background.green(),
background.blue(),
Math.round(background.alpha() * 255)
];
inputDescriptor.joinBackground = this._getBackgroundColourOption(inputOptions.join.background);
}
if (is.defined(inputOptions.join.halign)) {
if (is.string(inputOptions.join.halign) && is.string(this.constructor.align[inputOptions.join.halign])) {

View File

@@ -3,7 +3,6 @@
'use strict';
const color = require('color');
const is = require('./is');
/**
@@ -67,13 +66,7 @@ function rotate (angle, options) {
} else if (is.number(angle)) {
this.options.rotationAngle = angle;
if (is.object(options) && options.background) {
const backgroundColour = color(options.background);
this.options.rotationBackground = [
backgroundColour.red(),
backgroundColour.green(),
backgroundColour.blue(),
Math.round(backgroundColour.alpha() * 255)
];
this._setBackgroundColourOption('rotationBackground', options.background);
}
} else {
throw is.invalidParameterError('angle', 'numeric', angle);

View File

@@ -129,7 +129,7 @@ function isResizeExpected (options) {
*
* Some of these values are based on the [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) CSS property.
*
* <img alt="Examples of various values for the fit property when resizing" width="100%" style="aspect-ratio: 998/243" src="https://cdn.jsdelivr.net/gh/lovell/sharp@main/docs/image/api-resize-fit.svg">
* <img alt="Examples of various values for the fit property when resizing" width="100%" style="aspect-ratio: 998/243" src="/api-resize-fit.svg">
*
* When using a **fit** of `cover` or `contain`, the default **position** is `centre`. Other options are:
* - `sharp.position`: `top`, `right top`, `right`, `right bottom`, `bottom`, `left bottom`, `left`, `left top`.

View File

@@ -651,22 +651,21 @@ namespace sharp {
*/
VImage SetAnimationProperties(VImage image, int nPages, int pageHeight, std::vector<int> delay, int loop) {
bool hasDelay = !delay.empty();
// Avoid a copy if none of the animation properties are needed.
if (nPages == 1 && !hasDelay && loop == -1) return image;
if (delay.size() == 1) {
// We have just one delay, repeat that value for all frames.
delay.insert(delay.end(), nPages - 1, delay[0]);
}
// Attaching metadata, need to copy the image.
VImage copy = image.copy();
// Only set page-height if we have more than one page, or this could
// accidentally turn into an animated image later.
if (nPages > 1) copy.set(VIPS_META_PAGE_HEIGHT, pageHeight);
if (hasDelay) copy.set("delay", delay);
if (hasDelay) {
if (delay.size() == 1) {
// We have just one delay, repeat that value for all frames.
delay.insert(delay.end(), nPages - 1, delay[0]);
}
copy.set("delay", delay);
}
if (nPages == 1 && !hasDelay && loop == -1) {
loop = 1;
}
if (loop != -1) copy.set("loop", loop);
return copy;

View File

@@ -384,7 +384,7 @@ struct PipelineBaton {
ensureAlpha(-1.0),
colourspacePipeline(VIPS_INTERPRETATION_LAST),
colourspace(VIPS_INTERPRETATION_LAST),
loop(1),
loop(-1),
tileSize(256),
tileOverlap(0),
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),

View File

@@ -238,4 +238,15 @@ describe('GIF input', () => {
assert.strictEqual(1, loop);
}
});
it('Animated GIF to animated WebP merges identical frames', async () => {
const webp = await sharp(fixtures.inputGifAnimated, { animated: true })
.webp()
.toBuffer();
const { delay, loop, pages } = await sharp(webp).metadata();
assert.deepStrictEqual([120, 120, 90, 120, 120, 90, 120, 90, 30], delay);
assert.strictEqual(0, loop);
assert.strictEqual(9, pages);
});
});