Add pageHeight param to create/new for animated input #3236

This commit is contained in:
Lovell Fuller 2025-06-21 11:33:52 +01:00
parent 852c7f8663
commit e26d4e9d5b
11 changed files with 179 additions and 20 deletions

View File

@ -50,15 +50,17 @@ where the overall height is the `pageHeight` multiplied by the number of `pages`
| [options.raw.height] | <code>number</code> | | integral number of pixels high. | | [options.raw.height] | <code>number</code> | | integral number of pixels high. |
| [options.raw.channels] | <code>number</code> | | integral number of channels, between 1 and 4. | | [options.raw.channels] | <code>number</code> | | integral number of channels, between 1 and 4. |
| [options.raw.premultiplied] | <code>boolean</code> | | specifies that the raw input has already been premultiplied, set to `true` to avoid sharp premultiplying the image. (optional, default `false`) | | [options.raw.premultiplied] | <code>boolean</code> | | specifies that the raw input has already been premultiplied, set to `true` to avoid sharp premultiplying the image. (optional, default `false`) |
| [options.raw.pageHeight] | <code>number</code> | | The pixel height of each page/frame for animated images, must be an integral factor of `raw.height`. |
| [options.create] | <code>Object</code> | | describes a new image to be created. | | [options.create] | <code>Object</code> | | describes a new image to be created. |
| [options.create.width] | <code>number</code> | | integral number of pixels wide. | | [options.create.width] | <code>number</code> | | integral number of pixels wide. |
| [options.create.height] | <code>number</code> | | integral number of pixels high. | | [options.create.height] | <code>number</code> | | integral number of pixels high. |
| [options.create.channels] | <code>number</code> | | integral number of channels, either 3 (RGB) or 4 (RGBA). | | [options.create.channels] | <code>number</code> | | integral number of channels, either 3 (RGB) or 4 (RGBA). |
| [options.create.background] | <code>string</code> \| <code>Object</code> | | parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. | | [options.create.background] | <code>string</code> \| <code>Object</code> | | parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. |
| [options.create.pageHeight] | <code>number</code> | | The pixel height of each page/frame for animated images, must be an integral factor of `create.height`. |
| [options.create.noise] | <code>Object</code> | | describes a noise to be created. | | [options.create.noise] | <code>Object</code> | | describes a noise to be created. |
| [options.create.noise.type] | <code>string</code> | | type of generated noise, currently only `gaussian` is supported. | | [options.create.noise.type] | <code>string</code> | | type of generated noise, currently only `gaussian` is supported. |
| [options.create.noise.mean] | <code>number</code> | | mean of pixels in generated noise. | | [options.create.noise.mean] | <code>number</code> | <code>128</code> | Mean value of pixels in the generated noise. |
| [options.create.noise.sigma] | <code>number</code> | | standard deviation of pixels in generated noise. | | [options.create.noise.sigma] | <code>number</code> | <code>30</code> | Standard deviation of pixel values in the generated noise. |
| [options.text] | <code>Object</code> | | describes a new text image to be created. | | [options.text] | <code>Object</code> | | describes a new text image to be created. |
| [options.text.text] | <code>string</code> | | text to render as a UTF-8 string. It can contain Pango markup, for example `<i>Le</i>Monde`. | | [options.text.text] | <code>string</code> | | text to render as a UTF-8 string. It can contain Pango markup, for example `<i>Le</i>Monde`. |
| [options.text.font] | <code>string</code> | | font name to render with. | | [options.text.font] | <code>string</code> | | font name to render with. |

View File

@ -18,6 +18,9 @@ Requires libvips v8.17.0
* Expose `keepDuplicateFrames` GIF output parameter. * 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. * Expose JPEG 2000 `oneshot` decoder option.
[#4262](https://github.com/lovell/sharp/pull/4262) [#4262](https://github.com/lovell/sharp/pull/4262)
[@mbklein](https://github.com/mbklein) [@mbklein](https://github.com/mbklein)

View File

@ -160,15 +160,17 @@ const debuglog = util.debuglog('sharp');
* @param {number} [options.raw.channels] - integral number of channels, between 1 and 4. * @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` * @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`) * 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 {Object} [options.create] - describes a new image to be created.
* @param {number} [options.create.width] - integral number of pixels wide. * @param {number} [options.create.width] - integral number of pixels wide.
* @param {number} [options.create.height] - integral number of pixels high. * @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 {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 {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 {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 {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.mean=128] - Mean value of pixels in the generated noise.
* @param {number} [options.create.noise.sigma] - standard deviation of pixels in 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 {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 `<i>Le</i>Monde`. * @param {string} [options.text.text] - text to render as a UTF-8 string. It can contain Pango markup, for example `<i>Le</i>Monde`.
* @param {string} [options.text.font] - font name to render with. * @param {string} [options.text.font] - font name to render with.

7
lib/index.d.ts vendored
View File

@ -1061,6 +1061,8 @@ declare namespace sharp {
interface CreateRaw extends Raw { 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) */ /** Specifies that the raw input has already been premultiplied, set to true to avoid sharp premultiplying the image. (optional, default false) */
premultiplied?: boolean | undefined; 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; type CreateChannels = 3 | 4;
@ -1076,6 +1078,9 @@ declare namespace sharp {
background: Colour | Color; background: Colour | Color;
/** Describes a noise to be created. */ /** Describes a noise to be created. */
noise?: Noise | undefined; 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 { interface CreateText {
@ -1549,7 +1554,7 @@ declare namespace sharp {
interface Noise { interface Noise {
/** type of generated noise, currently only gaussian is supported. */ /** type of generated noise, currently only gaussian is supported. */
type?: 'gaussian' | undefined; type: 'gaussian';
/** mean of pixels in generated noise. */ /** mean of pixels in generated noise. */
mean?: number | undefined; mean?: number | undefined;
/** standard deviation of pixels in generated noise. */ /** standard deviation of pixels in generated noise. */

View File

@ -185,8 +185,6 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
inputDescriptor.rawWidth = inputOptions.raw.width; inputDescriptor.rawWidth = inputOptions.raw.width;
inputDescriptor.rawHeight = inputOptions.raw.height; inputDescriptor.rawHeight = inputOptions.raw.height;
inputDescriptor.rawChannels = inputOptions.raw.channels; inputDescriptor.rawChannels = inputOptions.raw.channels;
inputDescriptor.rawPremultiplied = !!inputOptions.raw.premultiplied;
switch (input.constructor) { switch (input.constructor) {
case Uint8Array: case Uint8Array:
case Uint8ClampedArray: case Uint8ClampedArray:
@ -220,6 +218,25 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
} else { } else {
throw new Error('Expected width, height and channels for raw pixel input'); 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) // Multi-page input (GIF, TIFF, PDF)
if (is.defined(inputOptions.animated)) { if (is.defined(inputOptions.animated)) {
@ -316,28 +333,45 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
inputDescriptor.createWidth = inputOptions.create.width; inputDescriptor.createWidth = inputOptions.create.width;
inputDescriptor.createHeight = inputOptions.create.height; inputDescriptor.createHeight = inputOptions.create.height;
inputDescriptor.createChannels = inputOptions.create.channels; 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 // Noise
if (is.defined(inputOptions.create.noise)) { if (is.defined(inputOptions.create.noise)) {
if (!is.object(inputOptions.create.noise)) { if (!is.object(inputOptions.create.noise)) {
throw new Error('Expected noise to be an object'); 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'); 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)) { if (!is.inRange(inputOptions.create.channels, 1, 4)) {
throw is.invalidParameterError('create.channels', 'number between 1 and 4', inputOptions.create.channels); throw is.invalidParameterError('create.channels', 'number between 1 and 4', inputOptions.create.channels);
} }
inputDescriptor.createNoiseType = inputOptions.create.noise.type; 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)) { if (is.number(inputOptions.create.noise.mean) && is.inRange(inputOptions.create.noise.mean, 0, 10000)) {
inputDescriptor.createNoiseMean = inputOptions.create.noise.mean; inputDescriptor.createNoiseMean = inputOptions.create.noise.mean;
} else { } else {
throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean); throw is.invalidParameterError('create.noise.mean', 'number between 0 and 10000', inputOptions.create.noise.mean);
} }
}
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)) { if (is.number(inputOptions.create.noise.sigma) && is.inRange(inputOptions.create.noise.sigma, 0, 10000)) {
inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma; inputDescriptor.createNoiseSigma = inputOptions.create.noise.sigma;
} else { } else {
throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma); throw is.invalidParameterError('create.noise.sigma', 'number between 0 and 10000', inputOptions.create.noise.sigma);
} }
}
} else if (is.defined(inputOptions.create.background)) { } else if (is.defined(inputOptions.create.background)) {
if (!is.inRange(inputOptions.create.channels, 3, 4)) { if (!is.inRange(inputOptions.create.channels, 3, 4)) {
throw is.invalidParameterError('create.channels', 'number between 3 and 4', inputOptions.create.channels); throw is.invalidParameterError('create.channels', 'number between 3 and 4', inputOptions.create.channels);

View File

@ -179,12 +179,12 @@
"icc": "^3.0.0", "icc": "^3.0.0",
"jsdoc-to-markdown": "^9.1.1", "jsdoc-to-markdown": "^9.1.1",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"mocha": "^11.6.0", "mocha": "^11.7.0",
"node-addon-api": "^8.3.1", "node-addon-api": "^8.4.0",
"node-gyp": "^11.2.0", "node-gyp": "^11.2.0",
"nyc": "^17.1.0", "nyc": "^17.1.0",
"semistandard": "^17.0.0", "semistandard": "^17.0.0",
"tar-fs": "^3.0.9", "tar-fs": "^3.0.10",
"tsd": "^0.32.0" "tsd": "^0.32.0"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",

View File

@ -93,6 +93,7 @@ namespace sharp {
descriptor->rawWidth = AttrAsUint32(input, "rawWidth"); descriptor->rawWidth = AttrAsUint32(input, "rawWidth");
descriptor->rawHeight = AttrAsUint32(input, "rawHeight"); descriptor->rawHeight = AttrAsUint32(input, "rawHeight");
descriptor->rawPremultiplied = AttrAsBool(input, "rawPremultiplied"); descriptor->rawPremultiplied = AttrAsBool(input, "rawPremultiplied");
descriptor->rawPageHeight = AttrAsUint32(input, "rawPageHeight");
} }
// Multi-page input (GIF, TIFF, PDF) // Multi-page input (GIF, TIFF, PDF)
if (HasAttr(input, "pages")) { if (HasAttr(input, "pages")) {
@ -129,6 +130,7 @@ namespace sharp {
descriptor->createChannels = AttrAsUint32(input, "createChannels"); descriptor->createChannels = AttrAsUint32(input, "createChannels");
descriptor->createWidth = AttrAsUint32(input, "createWidth"); descriptor->createWidth = AttrAsUint32(input, "createWidth");
descriptor->createHeight = AttrAsUint32(input, "createHeight"); descriptor->createHeight = AttrAsUint32(input, "createHeight");
descriptor->createPageHeight = AttrAsUint32(input, "createPageHeight");
if (HasAttr(input, "createNoiseType")) { if (HasAttr(input, "createNoiseType")) {
descriptor->createNoiseType = AttrAsStr(input, "createNoiseType"); descriptor->createNoiseType = AttrAsStr(input, "createNoiseType");
descriptor->createNoiseMean = AttrAsDouble(input, "createNoiseMean"); descriptor->createNoiseMean = AttrAsDouble(input, "createNoiseMean");
@ -453,6 +455,10 @@ namespace sharp {
} else { } else {
image.get_image()->Type = is8bit ? VIPS_INTERPRETATION_sRGB : VIPS_INTERPRETATION_RGB16; 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<int>(descriptor->rawHeight / descriptor->rawPageHeight));
}
if (descriptor->rawPremultiplied) { if (descriptor->rawPremultiplied) {
image = image.unpremultiply(); image = image.unpremultiply();
} }
@ -502,6 +508,10 @@ namespace sharp {
channels < 3 ? VIPS_INTERPRETATION_B_W : VIPS_INTERPRETATION_sRGB)) channels < 3 ? VIPS_INTERPRETATION_B_W : VIPS_INTERPRETATION_sRGB))
.new_from_image(background); .new_from_image(background);
} }
if (descriptor->createPageHeight > 0) {
image.set(VIPS_META_PAGE_HEIGHT, descriptor->createPageHeight);
image.set(VIPS_META_N_PAGES, static_cast<int>(descriptor->createHeight / descriptor->createPageHeight));
}
image = image.cast(VIPS_FORMAT_UCHAR); image = image.cast(VIPS_FORMAT_UCHAR);
imageType = ImageType::RAW; imageType = ImageType::RAW;
} else if (descriptor->textValue.length() > 0) { } else if (descriptor->textValue.length() > 0) {

View File

@ -48,11 +48,13 @@ namespace sharp {
int rawWidth; int rawWidth;
int rawHeight; int rawHeight;
bool rawPremultiplied; bool rawPremultiplied;
int rawPageHeight;
int pages; int pages;
int page; int page;
int createChannels; int createChannels;
int createWidth; int createWidth;
int createHeight; int createHeight;
int createPageHeight;
std::vector<double> createBackground; std::vector<double> createBackground;
std::string createNoiseType; std::string createNoiseType;
double createNoiseMean; double createNoiseMean;
@ -98,11 +100,13 @@ namespace sharp {
rawWidth(0), rawWidth(0),
rawHeight(0), rawHeight(0),
rawPremultiplied(false), rawPremultiplied(false),
rawPageHeight(0),
pages(1), pages(1),
page(0), page(0),
createChannels(0), createChannels(0),
createWidth(0), createWidth(0),
createHeight(0), createHeight(0),
createPageHeight(0),
createBackground{ 0.0, 0.0, 0.0, 255.0 }, createBackground{ 0.0, 0.0, 0.0, 255.0 },
createNoiseMean(0.0), createNoiseMean(0.0),
createNoiseSigma(0.0), createNoiseSigma(0.0),

View File

@ -418,6 +418,7 @@ sharp({
channels: 4, channels: 4,
height: 25000, height: 25000,
width: 25000, width: 25000,
pageHeight: 1000,
}, },
limitInputPixels: false, limitInputPixels: false,
}) })
@ -734,6 +735,13 @@ sharp({ svg: { stylesheet: 'test' }});
sharp({ svg: { highBitdepth: true }}); sharp({ svg: { highBitdepth: true }});
sharp({ svg: { highBitdepth: false }}); 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: true });
sharp({ autoOrient: false }); sharp({ autoOrient: false });
sharp().autoOrient(); sharp().autoOrient();

View File

@ -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 () { it('no create object properties specified', function () {
assert.throws(function () { assert.throws(function () {
sharp({ 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/
);
});
}); });

View File

@ -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) { it('RGB', function (done) {
// Convert to raw pixel data // Convert to raw pixel data
sharp(fixtures.inputJpg) 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', () => { describe('16-bit roundtrip', () => {
it('grey', async () => { it('grey', async () => {
const grey = 42000; const grey = 42000;