diff --git a/docs/src/content/docs/api-constructor.md b/docs/src/content/docs/api-constructor.md
index 139e08f4..3504239d 100644
--- a/docs/src/content/docs/api-constructor.md
+++ b/docs/src/content/docs/api-constructor.md
@@ -50,15 +50,17 @@ where the overall height is the `pageHeight` multiplied by the number of `pages`
| [options.raw.height] | number
| | integral number of pixels high. |
| [options.raw.channels] | number
| | integral number of channels, between 1 and 4. |
| [options.raw.premultiplied] | boolean
| | specifies that the raw input has already been premultiplied, set to `true` to avoid sharp premultiplying the image. (optional, default `false`) |
+| [options.raw.pageHeight] | number
| | The pixel height of each page/frame for animated images, must be an integral factor of `raw.height`. |
| [options.create] | Object
| | describes a new image to be created. |
| [options.create.width] | number
| | integral number of pixels wide. |
| [options.create.height] | number
| | integral number of pixels high. |
| [options.create.channels] | number
| | integral number of channels, either 3 (RGB) or 4 (RGBA). |
| [options.create.background] | string
\| Object
| | parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. |
+| [options.create.pageHeight] | number
| | The pixel height of each page/frame for animated images, must be an integral factor of `create.height`. |
| [options.create.noise] | Object
| | describes a noise to be created. |
| [options.create.noise.type] | string
| | type of generated noise, currently only `gaussian` is supported. |
-| [options.create.noise.mean] | number
| | mean of pixels in generated noise. |
-| [options.create.noise.sigma] | number
| | standard deviation of pixels in generated noise. |
+| [options.create.noise.mean] | number
| 128
| Mean value of pixels in the generated noise. |
+| [options.create.noise.sigma] | number
| 30
| Standard deviation of pixel values in the generated noise. |
| [options.text] | Object
| | describes a new text image to be created. |
| [options.text.text] | string
| | text to render as a UTF-8 string. It can contain Pango markup, for example `LeMonde`. |
| [options.text.font] | string
| | font name to render with. |
diff --git a/docs/src/content/docs/changelog.md b/docs/src/content/docs/changelog.md
index d39669d7..0a248750 100644
--- a/docs/src/content/docs/changelog.md
+++ b/docs/src/content/docs/changelog.md
@@ -18,6 +18,9 @@ Requires libvips v8.17.0
* Expose `keepDuplicateFrames` GIF output parameter.
+* Add `pageHeight` option to `create` and `raw` input for animated images.
+ [#3236](https://github.com/lovell/sharp/issues/3236)
+
* Expose JPEG 2000 `oneshot` decoder option.
[#4262](https://github.com/lovell/sharp/pull/4262)
[@mbklein](https://github.com/mbklein)
diff --git a/lib/constructor.js b/lib/constructor.js
index daac3b5f..e5d2a0c5 100644
--- a/lib/constructor.js
+++ b/lib/constructor.js
@@ -160,15 +160,17 @@ const debuglog = util.debuglog('sharp');
* @param {number} [options.raw.channels] - integral number of channels, between 1 and 4.
* @param {boolean} [options.raw.premultiplied] - specifies that the raw input has already been premultiplied, set to `true`
* to avoid sharp premultiplying the image. (optional, default `false`)
+ * @param {number} [options.raw.pageHeight] - The pixel height of each page/frame for animated images, must be an integral factor of `raw.height`.
* @param {Object} [options.create] - describes a new image to be created.
* @param {number} [options.create.width] - integral number of pixels wide.
* @param {number} [options.create.height] - integral number of pixels high.
* @param {number} [options.create.channels] - integral number of channels, either 3 (RGB) or 4 (RGBA).
* @param {string|Object} [options.create.background] - parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
+ * @param {number} [options.create.pageHeight] - The pixel height of each page/frame for animated images, must be an integral factor of `create.height`.
* @param {Object} [options.create.noise] - describes a noise to be created.
* @param {string} [options.create.noise.type] - type of generated noise, currently only `gaussian` is supported.
- * @param {number} [options.create.noise.mean] - mean of pixels in generated noise.
- * @param {number} [options.create.noise.sigma] - standard deviation of pixels in generated noise.
+ * @param {number} [options.create.noise.mean=128] - Mean value of pixels in the generated noise.
+ * @param {number} [options.create.noise.sigma=30] - Standard deviation of pixel values in the generated noise.
* @param {Object} [options.text] - describes a new text image to be created.
* @param {string} [options.text.text] - text to render as a UTF-8 string. It can contain Pango markup, for example `LeMonde`.
* @param {string} [options.text.font] - font name to render with.
diff --git a/lib/index.d.ts b/lib/index.d.ts
index 4b39148e..395ec977 100644
--- a/lib/index.d.ts
+++ b/lib/index.d.ts
@@ -1061,6 +1061,8 @@ declare namespace sharp {
interface CreateRaw extends Raw {
/** Specifies that the raw input has already been premultiplied, set to true to avoid sharp premultiplying the image. (optional, default false) */
premultiplied?: boolean | undefined;
+ /** The height of each page/frame for animated images, must be an integral factor of the overall image height. */
+ pageHeight?: number | undefined;
}
type CreateChannels = 3 | 4;
@@ -1076,6 +1078,9 @@ declare namespace sharp {
background: Colour | Color;
/** Describes a noise to be created. */
noise?: Noise | undefined;
+ /** The height of each page/frame for animated images, must be an integral factor of the overall image height. */
+ pageHeight?: number | undefined;
+
}
interface CreateText {
@@ -1549,7 +1554,7 @@ declare namespace sharp {
interface Noise {
/** type of generated noise, currently only gaussian is supported. */
- type?: 'gaussian' | undefined;
+ type: 'gaussian';
/** mean of pixels in generated noise. */
mean?: number | undefined;
/** standard deviation of pixels in generated noise. */
diff --git a/lib/input.js b/lib/input.js
index 436bfb75..ceb6d854 100644
--- a/lib/input.js
+++ b/lib/input.js
@@ -185,8 +185,6 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
inputDescriptor.rawWidth = inputOptions.raw.width;
inputDescriptor.rawHeight = inputOptions.raw.height;
inputDescriptor.rawChannels = inputOptions.raw.channels;
- inputDescriptor.rawPremultiplied = !!inputOptions.raw.premultiplied;
-
switch (input.constructor) {
case Uint8Array:
case Uint8ClampedArray:
@@ -220,6 +218,25 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
} else {
throw new Error('Expected width, height and channels for raw pixel input');
}
+ inputDescriptor.rawPremultiplied = false;
+ if (is.defined(inputOptions.raw.premultiplied)) {
+ if (is.bool(inputOptions.raw.premultiplied)) {
+ inputDescriptor.rawPremultiplied = inputOptions.raw.premultiplied;
+ } else {
+ throw is.invalidParameterError('raw.premultiplied', 'boolean', inputOptions.raw.premultiplied);
+ }
+ }
+ inputDescriptor.rawPageHeight = 0;
+ if (is.defined(inputOptions.raw.pageHeight)) {
+ if (is.integer(inputOptions.raw.pageHeight) && inputOptions.raw.pageHeight > 0 && inputOptions.raw.pageHeight <= inputOptions.raw.height) {
+ if (inputOptions.raw.height % inputOptions.raw.pageHeight !== 0) {
+ throw new Error(`Expected raw.height ${inputOptions.raw.height} to be a multiple of raw.pageHeight ${inputOptions.raw.pageHeight}`);
+ }
+ inputDescriptor.rawPageHeight = inputOptions.raw.pageHeight;
+ } else {
+ throw is.invalidParameterError('raw.pageHeight', 'positive integer', inputOptions.raw.pageHeight);
+ }
+ }
}
// Multi-page input (GIF, TIFF, PDF)
if (is.defined(inputOptions.animated)) {
@@ -316,27 +333,44 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
inputDescriptor.createWidth = inputOptions.create.width;
inputDescriptor.createHeight = inputOptions.create.height;
inputDescriptor.createChannels = inputOptions.create.channels;
+ inputDescriptor.createPageHeight = 0;
+ if (is.defined(inputOptions.create.pageHeight)) {
+ if (is.integer(inputOptions.create.pageHeight) && inputOptions.create.pageHeight > 0 && inputOptions.create.pageHeight <= inputOptions.create.height) {
+ if (inputOptions.create.height % inputOptions.create.pageHeight !== 0) {
+ throw new Error(`Expected create.height ${inputOptions.create.height} to be a multiple of create.pageHeight ${inputOptions.create.pageHeight}`);
+ }
+ inputDescriptor.createPageHeight = inputOptions.create.pageHeight;
+ } else {
+ throw is.invalidParameterError('create.pageHeight', 'positive integer', inputOptions.create.pageHeight);
+ }
+ }
// Noise
if (is.defined(inputOptions.create.noise)) {
if (!is.object(inputOptions.create.noise)) {
throw new Error('Expected noise to be an object');
}
- if (!is.inArray(inputOptions.create.noise.type, ['gaussian'])) {
+ if (inputOptions.create.noise.type !== 'gaussian') {
throw new Error('Only gaussian noise is supported at the moment');
}
+ inputDescriptor.createNoiseType = inputOptions.create.noise.type;
if (!is.inRange(inputOptions.create.channels, 1, 4)) {
throw is.invalidParameterError('create.channels', 'number between 1 and 4', inputOptions.create.channels);
}
- inputDescriptor.createNoiseType = inputOptions.create.noise.type;
- if (is.number(inputOptions.create.noise.mean) && is.inRange(inputOptions.create.noise.mean, 0, 10000)) {
- inputDescriptor.createNoiseMean = inputOptions.create.noise.mean;
- } else {
- throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean);
+ inputDescriptor.createNoiseMean = 128;
+ if (is.defined(inputOptions.create.noise.mean)) {
+ if (is.number(inputOptions.create.noise.mean) && is.inRange(inputOptions.create.noise.mean, 0, 10000)) {
+ inputDescriptor.createNoiseMean = inputOptions.create.noise.mean;
+ } else {
+ throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean);
+ }
}
- if (is.number(inputOptions.create.noise.sigma) && is.inRange(inputOptions.create.noise.sigma, 0, 10000)) {
- inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma;
- } else {
- throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma);
+ inputDescriptor.createNoiseSigma = 30;
+ if (is.defined(inputOptions.create.noise.sigma)) {
+ if (is.number(inputOptions.create.noise.sigma) && is.inRange(inputOptions.create.noise.sigma, 0, 10000)) {
+ inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma;
+ } else {
+ throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma);
+ }
}
} else if (is.defined(inputOptions.create.background)) {
if (!is.inRange(inputOptions.create.channels, 3, 4)) {
diff --git a/package.json b/package.json
index 13816bce..a3d2a9ed 100644
--- a/package.json
+++ b/package.json
@@ -179,12 +179,12 @@
"icc": "^3.0.0",
"jsdoc-to-markdown": "^9.1.1",
"license-checker": "^25.0.1",
- "mocha": "^11.6.0",
- "node-addon-api": "^8.3.1",
+ "mocha": "^11.7.0",
+ "node-addon-api": "^8.4.0",
"node-gyp": "^11.2.0",
"nyc": "^17.1.0",
"semistandard": "^17.0.0",
- "tar-fs": "^3.0.9",
+ "tar-fs": "^3.0.10",
"tsd": "^0.32.0"
},
"license": "Apache-2.0",
diff --git a/src/common.cc b/src/common.cc
index e465dc2f..df779e8b 100644
--- a/src/common.cc
+++ b/src/common.cc
@@ -93,6 +93,7 @@ namespace sharp {
descriptor->rawWidth = AttrAsUint32(input, "rawWidth");
descriptor->rawHeight = AttrAsUint32(input, "rawHeight");
descriptor->rawPremultiplied = AttrAsBool(input, "rawPremultiplied");
+ descriptor->rawPageHeight = AttrAsUint32(input, "rawPageHeight");
}
// Multi-page input (GIF, TIFF, PDF)
if (HasAttr(input, "pages")) {
@@ -129,6 +130,7 @@ namespace sharp {
descriptor->createChannels = AttrAsUint32(input, "createChannels");
descriptor->createWidth = AttrAsUint32(input, "createWidth");
descriptor->createHeight = AttrAsUint32(input, "createHeight");
+ descriptor->createPageHeight = AttrAsUint32(input, "createPageHeight");
if (HasAttr(input, "createNoiseType")) {
descriptor->createNoiseType = AttrAsStr(input, "createNoiseType");
descriptor->createNoiseMean = AttrAsDouble(input, "createNoiseMean");
@@ -453,6 +455,10 @@ namespace sharp {
} else {
image.get_image()->Type = is8bit ? VIPS_INTERPRETATION_sRGB : VIPS_INTERPRETATION_RGB16;
}
+ if (descriptor->rawPageHeight > 0) {
+ image.set(VIPS_META_PAGE_HEIGHT, descriptor->rawPageHeight);
+ image.set(VIPS_META_N_PAGES, static_cast(descriptor->rawHeight / descriptor->rawPageHeight));
+ }
if (descriptor->rawPremultiplied) {
image = image.unpremultiply();
}
@@ -502,6 +508,10 @@ namespace sharp {
channels < 3 ? VIPS_INTERPRETATION_B_W : VIPS_INTERPRETATION_sRGB))
.new_from_image(background);
}
+ if (descriptor->createPageHeight > 0) {
+ image.set(VIPS_META_PAGE_HEIGHT, descriptor->createPageHeight);
+ image.set(VIPS_META_N_PAGES, static_cast(descriptor->createHeight / descriptor->createPageHeight));
+ }
image = image.cast(VIPS_FORMAT_UCHAR);
imageType = ImageType::RAW;
} else if (descriptor->textValue.length() > 0) {
diff --git a/src/common.h b/src/common.h
index e199942d..40048aa2 100644
--- a/src/common.h
+++ b/src/common.h
@@ -48,11 +48,13 @@ namespace sharp {
int rawWidth;
int rawHeight;
bool rawPremultiplied;
+ int rawPageHeight;
int pages;
int page;
int createChannels;
int createWidth;
int createHeight;
+ int createPageHeight;
std::vector createBackground;
std::string createNoiseType;
double createNoiseMean;
@@ -98,11 +100,13 @@ namespace sharp {
rawWidth(0),
rawHeight(0),
rawPremultiplied(false),
+ rawPageHeight(0),
pages(1),
page(0),
createChannels(0),
createWidth(0),
createHeight(0),
+ createPageHeight(0),
createBackground{ 0.0, 0.0, 0.0, 255.0 },
createNoiseMean(0.0),
createNoiseSigma(0.0),
diff --git a/test/types/sharp.test-d.ts b/test/types/sharp.test-d.ts
index d00cbd29..2d65eb49 100644
--- a/test/types/sharp.test-d.ts
+++ b/test/types/sharp.test-d.ts
@@ -418,6 +418,7 @@ sharp({
channels: 4,
height: 25000,
width: 25000,
+ pageHeight: 1000,
},
limitInputPixels: false,
})
@@ -734,6 +735,13 @@ sharp({ svg: { stylesheet: 'test' }});
sharp({ svg: { highBitdepth: true }});
sharp({ svg: { highBitdepth: false }});
+// Raw input options
+const raw: sharp.Raw = { width: 1, height: 1, channels: 3 };
+sharp({ raw });
+sharp({ raw: { ...raw, premultiplied: true } });
+sharp({ raw: { ...raw, premultiplied: false } });
+sharp({ raw: { ...raw, pageHeight: 1 } });
+
sharp({ autoOrient: true });
sharp({ autoOrient: false });
sharp().autoOrient();
diff --git a/test/unit/noise.js b/test/unit/noise.js
index bdd6bbbc..ac3a63db 100644
--- a/test/unit/noise.js
+++ b/test/unit/noise.js
@@ -173,6 +173,26 @@ describe('Gaussian noise', function () {
});
});
+ it('animated noise', async () => {
+ const gif = await sharp({
+ create: {
+ width: 16,
+ height: 64,
+ pageHeight: 16,
+ channels: 3,
+ noise: { type: 'gaussian' }
+ }
+ })
+ .gif()
+ .toBuffer();
+
+ const { width, height, pages, delay } = await sharp(gif).metadata();
+ assert.strictEqual(width, 16);
+ assert.strictEqual(height, 16);
+ assert.strictEqual(pages, 4);
+ assert.strictEqual(delay.length, 4);
+ });
+
it('no create object properties specified', function () {
assert.throws(function () {
sharp({
@@ -259,4 +279,29 @@ describe('Gaussian noise', function () {
});
});
});
+
+ it('Invalid pageHeight', () => {
+ const create = {
+ width: 8,
+ height: 8,
+ channels: 4,
+ noise: { type: 'gaussian' }
+ };
+ assert.throws(
+ () => sharp({ create: { ...create, pageHeight: 'zoinks' } }),
+ /Expected positive integer for create\.pageHeight but received zoinks of type string/
+ );
+ assert.throws(
+ () => sharp({ create: { ...create, pageHeight: -1 } }),
+ /Expected positive integer for create\.pageHeight but received -1 of type number/
+ );
+ assert.throws(
+ () => sharp({ create: { ...create, pageHeight: 9 } }),
+ /Expected positive integer for create\.pageHeight but received 9 of type number/
+ );
+ assert.throws(
+ () => sharp({ create: { ...create, pageHeight: 3 } }),
+ /Expected create\.height 8 to be a multiple of create\.pageHeight 3/
+ );
+ });
});
diff --git a/test/unit/raw.js b/test/unit/raw.js
index 7ad24012..9a01d0f0 100644
--- a/test/unit/raw.js
+++ b/test/unit/raw.js
@@ -55,6 +55,35 @@ describe('Raw pixel data', function () {
});
});
+ it('Invalid premultiplied', () => {
+ assert.throws(
+ () => sharp({ raw: { width: 1, height: 1, channels: 4, premultiplied: 'zoinks' } }),
+ /Expected boolean for raw\.premultiplied but received zoinks of type string/
+ );
+ });
+
+ it('Invalid pageHeight', () => {
+ const width = 8;
+ const height = 8;
+ const channels = 4;
+ assert.throws(
+ () => sharp({ raw: { width, height, channels, pageHeight: 'zoinks' } }),
+ /Expected positive integer for raw\.pageHeight but received zoinks of type string/
+ );
+ assert.throws(
+ () => sharp({ raw: { width, height, channels, pageHeight: -1 } }),
+ /Expected positive integer for raw\.pageHeight but received -1 of type number/
+ );
+ assert.throws(
+ () => sharp({ raw: { width, height, channels, pageHeight: 9 } }),
+ /Expected positive integer for raw\.pageHeight but received 9 of type number/
+ );
+ assert.throws(
+ () => sharp({ raw: { width, height, channels, pageHeight: 3 } }),
+ /Expected raw\.height 8 to be a multiple of raw\.pageHeight 3/
+ );
+ });
+
it('RGB', function (done) {
// Convert to raw pixel data
sharp(fixtures.inputJpg)
@@ -285,6 +314,23 @@ describe('Raw pixel data', function () {
}
});
+ it('Animated', async () => {
+ const gif = await sharp(
+ Buffer.alloc(8),
+ { raw: { width: 1, height: 2, channels: 4, pageHeight: 1 }, animated: true }
+ )
+ .gif({ keepDuplicateFrames: true })
+ .toBuffer();
+
+ console.log(await sharp(gif).metadata());
+
+ const { width, height, pages, delay } = await sharp(gif).metadata();
+ assert.strictEqual(width, 1);
+ assert.strictEqual(height, 1);
+ assert.strictEqual(pages, 2);
+ assert.strictEqual(delay.length, 2);
+ });
+
describe('16-bit roundtrip', () => {
it('grey', async () => {
const grey = 42000;