Compare commits

..

4 Commits

Author SHA1 Message Date
Lovell Fuller
ef77388a73 Force MSVC to use exception handling
As of 8.18.0, libvips C++ wrapper retrieves error messages at
exception construction time rather than lazily when accessed.

On Windows this led to error messages being referenced rather
than copied, leading to access beyond their lifetime and possible
corruption.
2026-01-22 12:52:48 +00:00
Lovell Fuller
66764b359b Remove unused option parameter added in 8561f0d 2026-01-20 21:10:56 +00:00
Lovell Fuller
8561f0da1d Ensure HEIF primary item is used as default page #4487 2026-01-18 20:24:34 +00:00
Lovell Fuller
0468c1be9f Encoding lossless AVIF is mutually exclusive with iq tuning 2026-01-08 12:43:53 +00:00
10 changed files with 147 additions and 37 deletions

View File

@@ -758,7 +758,7 @@ When using Windows ARM64, this feature requires a CPU with ARM64v8.4 or later.
| [options.effort] | <code>number</code> | <code>4</code> | CPU effort, between 0 (fastest) and 9 (slowest) | | [options.effort] | <code>number</code> | <code>4</code> | CPU effort, between 0 (fastest) and 9 (slowest) |
| [options.chromaSubsampling] | <code>string</code> | <code>&quot;&#x27;4:4:4&#x27;&quot;</code> | set to '4:2:0' to use chroma subsampling | | [options.chromaSubsampling] | <code>string</code> | <code>&quot;&#x27;4:4:4&#x27;&quot;</code> | set to '4:2:0' to use chroma subsampling |
| [options.bitdepth] | <code>number</code> | <code>8</code> | set bitdepth to 8, 10 or 12 bit | | [options.bitdepth] | <code>number</code> | <code>8</code> | set bitdepth to 8, 10 or 12 bit |
| [options.tune] | <code>string</code> | <code>&quot;&#x27;iq&#x27;&quot;</code> | tune output for a quality metric, one of 'iq' (default), 'ssim' or 'psnr' | | [options.tune] | <code>string</code> | <code>&quot;&#x27;iq&#x27;&quot;</code> | tune output for a quality metric, one of 'iq' (default), 'ssim' (default when lossless) or 'psnr' |
**Example** **Example**
```js ```js

View File

@@ -38,4 +38,7 @@ slug: changelog/v0.35.0
[#4480](https://github.com/lovell/sharp/issues/4480) [#4480](https://github.com/lovell/sharp/issues/4480)
[@eddienubes](https://github.com/eddienubes) [@eddienubes](https://github.com/eddienubes)
* Ensure HEIF primary item is used as default page/frame.
[#4487](https://github.com/lovell/sharp/issues/4487)
* Add WebP `exact` option for control over transparent pixel colour values. * Add WebP `exact` option for control over transparent pixel colour values.

View File

@@ -1175,7 +1175,7 @@ function tiff (options) {
* @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest) * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 9 (slowest)
* @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling * @param {string} [options.chromaSubsampling='4:4:4'] - set to '4:2:0' to use chroma subsampling
* @param {number} [options.bitdepth=8] - set bitdepth to 8, 10 or 12 bit * @param {number} [options.bitdepth=8] - set bitdepth to 8, 10 or 12 bit
* @param {string} [options.tune='iq'] - tune output for a quality metric, one of 'iq' (default), 'ssim' or 'psnr' * @param {string} [options.tune='iq'] - tune output for a quality metric, one of 'iq' (default), 'ssim' (default when lossless) or 'psnr'
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid options * @throws {Error} Invalid options
*/ */
@@ -1255,7 +1255,11 @@ function heif (options) {
} }
if (is.defined(options.tune)) { if (is.defined(options.tune)) {
if (is.string(options.tune) && is.inArray(options.tune, ['iq', 'ssim', 'psnr'])) { if (is.string(options.tune) && is.inArray(options.tune, ['iq', 'ssim', 'psnr'])) {
this.options.heifTune = options.tune; if (this.options.heifLossless && options.tune === 'iq') {
this.options.heifTune = 'ssim';
} else {
this.options.heifTune = options.tune;
}
} else { } else {
throw is.invalidParameterError('tune', 'one of: psnr, ssim, iq', options.tune); throw is.invalidParameterError('tune', 'one of: psnr, ssim, iq', options.tune);
} }

View File

@@ -21,6 +21,7 @@
'defines': [ 'defines': [
'_VIPS_PUBLIC=__declspec(dllexport)', '_VIPS_PUBLIC=__declspec(dllexport)',
'_ALLOW_KEYWORD_MACROS', '_ALLOW_KEYWORD_MACROS',
'_HAS_EXCEPTIONS=1',
'G_DISABLE_ASSERT', 'G_DISABLE_ASSERT',
'G_DISABLE_CAST_CHECKS', 'G_DISABLE_CAST_CHECKS',
'G_DISABLE_CHECKS' 'G_DISABLE_CHECKS'
@@ -148,7 +149,8 @@
['OS == "win"', { ['OS == "win"', {
'defines': [ 'defines': [
'_ALLOW_KEYWORD_MACROS', '_ALLOW_KEYWORD_MACROS',
'_FILE_OFFSET_BITS=64' '_FILE_OFFSET_BITS=64',
'_HAS_EXCEPTIONS=1'
], ],
'link_settings': { 'link_settings': {
'libraries': [ 'libraries': [

View File

@@ -426,7 +426,7 @@ namespace sharp {
} }
if (ImageTypeSupportsPage(imageType)) { if (ImageTypeSupportsPage(imageType)) {
option->set("n", descriptor->pages); option->set("n", descriptor->pages);
option->set("page", descriptor->page); option->set("page", std::max(0, descriptor->page));
} }
switch (imageType) { switch (imageType) {
case ImageType::SVG: case ImageType::SVG:
@@ -456,6 +456,22 @@ namespace sharp {
return option; return option;
} }
/*
Should HEIF image be re-opened using the primary item?
*/
static bool HeifPrimaryPageReopen(VImage image, InputDescriptor *descriptor) {
if (image.get_typeof(VIPS_META_N_PAGES) == G_TYPE_INT && image.get_typeof("heif-primary") == G_TYPE_INT) {
if (image.get_int(VIPS_META_N_PAGES) > 1 && descriptor->pages == 1 && descriptor->page == -1) {
int const pagePrimary = image.get_int("heif-primary");
if (pagePrimary != 0) {
descriptor->page = pagePrimary;
return true;
}
}
}
return false;
}
/* /*
Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data) Open an image from the given InputDescriptor (filesystem, compressed buffer, raw pixel data)
*/ */
@@ -490,6 +506,9 @@ namespace sharp {
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option); image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
image = SetDensity(image, descriptor->density); image = SetDensity(image, descriptor->density);
} else if (imageType == ImageType::HEIF && HeifPrimaryPageReopen(image, descriptor)) {
option = GetOptionsForImageType(imageType, descriptor);
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
} }
} catch (vips::VError const &err) { } catch (vips::VError const &err) {
throw vips::VError(std::string("Input buffer has corrupt header: ") + err.what()); throw vips::VError(std::string("Input buffer has corrupt header: ") + err.what());
@@ -577,6 +596,9 @@ namespace sharp {
image = VImage::new_from_file(descriptor->file.data(), option); image = VImage::new_from_file(descriptor->file.data(), option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) { if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
image = SetDensity(image, descriptor->density); image = SetDensity(image, descriptor->density);
} else if (imageType == ImageType::HEIF && HeifPrimaryPageReopen(image, descriptor)) {
option = GetOptionsForImageType(imageType, descriptor);
image = VImage::new_from_file(descriptor->file.data(), option);
} }
} catch (vips::VError const &err) { } catch (vips::VError const &err) {
throw vips::VError(std::string("Input file has corrupt header: ") + err.what()); throw vips::VError(std::string("Input file has corrupt header: ") + err.what());

View File

@@ -105,7 +105,7 @@ namespace sharp {
rawPremultiplied(false), rawPremultiplied(false),
rawPageHeight(0), rawPageHeight(0),
pages(1), pages(1),
page(0), page(-1),
createChannels(0), createChannels(0),
createWidth(0), createWidth(0),
createHeight(0), createHeight(0),

View File

@@ -84,7 +84,7 @@ class PipelineWorker : public Napi::AsyncWorker {
if (nPages == -1) { if (nPages == -1) {
// Resolve the number of pages if we need to render until the end of the document // Resolve the number of pages if we need to render until the end of the document
nPages = image.get_typeof(VIPS_META_N_PAGES) != 0 nPages = image.get_typeof(VIPS_META_N_PAGES) != 0
? image.get_int(VIPS_META_N_PAGES) - baton->input->page ? image.get_int(VIPS_META_N_PAGES) - std::max(0, baton->input->page)
: 1; : 1;
} }

View File

@@ -127,7 +127,7 @@ module.exports = {
inputSvgSmallViewBox: getPath('circle.svg'), inputSvgSmallViewBox: getPath('circle.svg'),
inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg inputSvgWithEmbeddedImages: getPath('struct-image-04-t.svg'), // https://dev.w3.org/SVG/profiles/1.2T/test/svg/struct-image-04-t.svg
inputAvif: getPath('sdr_cosmos12920_cicp1-13-6_yuv444_full_qp10.avif'), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix inputAvif: getPath('sdr_cosmos12920_cicp1-13-6_yuv444_full_qp10.avif'), // CC by-nc-nd https://github.com/AOMediaCodec/av1-avif/tree/master/testFiles/Netflix
inputAvifWithPitmBox: getPath('pitm.avif'), // https://github.com/lovell/sharp/issues/4487
inputJPGBig: getPath('flowers.jpeg'), inputJPGBig: getPath('flowers.jpeg'),
inputPngDotAndLines: getPath('dot-and-lines.png'), inputPngDotAndLines: getPath('dot-and-lines.png'),

BIN
test/fixtures/pitm.avif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -7,7 +7,13 @@ const { describe, it } = require('node:test');
const assert = require('node:assert'); const assert = require('node:assert');
const sharp = require('../../'); const sharp = require('../../');
const { inputAvif, inputJpg, inputGifAnimated } = require('../fixtures'); const {
inputAvif,
inputAvifWithPitmBox,
inputJpg,
inputGifAnimated,
inputPng,
} = require('../fixtures');
describe('AVIF', () => { describe('AVIF', () => {
it('called without options does not throw an error', () => { it('called without options does not throw an error', () => {
@@ -17,16 +23,13 @@ describe('AVIF', () => {
}); });
it('can convert AVIF to JPEG', async () => { it('can convert AVIF to JPEG', async () => {
const data = await sharp(inputAvif) const data = await sharp(inputAvif).resize(32).jpeg().toBuffer();
.resize(32)
.jpeg()
.toBuffer();
const { size, ...metadata } = await sharp(data).metadata(); const { size, ...metadata } = await sharp(data).metadata();
void size; void size;
assert.deepStrictEqual(metadata, { assert.deepStrictEqual(metadata, {
autoOrient: { autoOrient: {
height: 13, height: 13,
width: 32 width: 32,
}, },
channels: 3, channels: 3,
chromaSubsampling: '4:2:0', chromaSubsampling: '4:2:0',
@@ -41,7 +44,7 @@ describe('AVIF', () => {
isProgressive: false, isProgressive: false,
isPalette: false, isPalette: false,
space: 'srgb', space: 'srgb',
width: 32 width: 32,
}); });
}); });
@@ -55,7 +58,7 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, { assert.deepStrictEqual(metadata, {
autoOrient: { autoOrient: {
height: 26, height: 26,
width: 32 width: 32,
}, },
channels: 3, channels: 3,
compression: 'av1', compression: 'av1',
@@ -70,20 +73,47 @@ describe('AVIF', () => {
pagePrimary: 0, pagePrimary: 0,
pages: 1, pages: 1,
space: 'srgb', space: 'srgb',
width: 32 width: 32,
}); });
}); });
it('can passthrough AVIF', async () => { it('can convert PNG to lossless AVIF', async () => {
const data = await sharp(inputAvif) const data = await sharp(inputPng)
.resize(32) .resize(32)
.avif({ lossless: true, effort: 0 })
.toBuffer(); .toBuffer();
const { size, ...metadata } = await sharp(data).metadata(); const { size, ...metadata } = await sharp(data).metadata();
void size; void size;
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 24,
width: 32,
},
channels: 3,
compression: 'av1',
depth: 'uchar',
format: 'heif',
hasAlpha: false,
hasProfile: false,
height: 24,
isProgressive: false,
isPalette: false,
bitsPerSample: 8,
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32,
});
});
it('can passthrough AVIF', async () => {
const data = await sharp(inputAvif).resize(32).toBuffer();
const { size, ...metadata } = await sharp(data).metadata();
void size;
assert.deepStrictEqual(metadata, { assert.deepStrictEqual(metadata, {
autoOrient: { autoOrient: {
height: 13, height: 13,
width: 32 width: 32,
}, },
channels: 3, channels: 3,
compression: 'av1', compression: 'av1',
@@ -98,7 +128,7 @@ describe('AVIF', () => {
pagePrimary: 0, pagePrimary: 0,
pages: 1, pages: 1,
space: 'srgb', space: 'srgb',
width: 32 width: 32,
}); });
}); });
@@ -112,7 +142,7 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, { assert.deepStrictEqual(metadata, {
autoOrient: { autoOrient: {
height: 300, height: 300,
width: 10 width: 10,
}, },
channels: 4, channels: 4,
compression: 'av1', compression: 'av1',
@@ -127,7 +157,7 @@ describe('AVIF', () => {
pagePrimary: 0, pagePrimary: 0,
pages: 1, pages: 1,
space: 'srgb', space: 'srgb',
width: 10 width: 10,
}); });
}); });
@@ -142,7 +172,7 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, { assert.deepStrictEqual(metadata, {
autoOrient: { autoOrient: {
height: 26, height: 26,
width: 32 width: 32,
}, },
channels: 3, channels: 3,
compression: 'av1', compression: 'av1',
@@ -157,30 +187,37 @@ describe('AVIF', () => {
pagePrimary: 0, pagePrimary: 0,
pages: 1, pages: 1,
space: 'srgb', space: 'srgb',
width: 32 width: 32,
}); });
}); });
it('Invalid width - too large', async () => it('Invalid width - too large', async () =>
assert.rejects( assert.rejects(
() => sharp({ create: { width: 16385, height: 16, channels: 3, background: 'red' } }).avif().toBuffer(), () =>
/Processed image is too large for the HEIF format/ sharp({
) create: { width: 16385, height: 16, channels: 3, background: 'red' },
); })
.avif()
.toBuffer(),
/Processed image is too large for the HEIF format/,
));
it('Invalid height - too large', async () => it('Invalid height - too large', async () =>
assert.rejects( assert.rejects(
() => sharp({ create: { width: 16, height: 16385, channels: 3, background: 'red' } }).avif().toBuffer(), () =>
/Processed image is too large for the HEIF format/ sharp({
) create: { width: 16, height: 16385, channels: 3, background: 'red' },
); })
.avif()
.toBuffer(),
/Processed image is too large for the HEIF format/,
));
it('Invalid bitdepth value throws error', () => it('Invalid bitdepth value throws error', () =>
assert.throws( assert.throws(
() => sharp().avif({ bitdepth: 11 }), () => sharp().avif({ bitdepth: 11 }),
/Expected 8, 10 or 12 for bitdepth but received 11 of type number/ /Expected 8, 10 or 12 for bitdepth but received 11 of type number/,
) ));
);
it('Different tune options result in different file sizes', async () => { it('Different tune options result in different file sizes', async () => {
const ssim = await sharp(inputJpg) const ssim = await sharp(inputJpg)
@@ -192,5 +229,47 @@ describe('AVIF', () => {
.avif({ tune: 'iq', effort: 0 }) .avif({ tune: 'iq', effort: 0 })
.toBuffer(); .toBuffer();
assert(ssim.length < iq.length); assert(ssim.length < iq.length);
}) });
it('AVIF with non-zero primary item uses it as default page', async () => {
const { exif, ...metadata } = await sharp(inputAvifWithPitmBox).metadata();
void exif;
assert.deepStrictEqual(metadata, {
format: 'heif',
width: 4096,
height: 800,
space: 'srgb',
channels: 3,
depth: 'uchar',
isProgressive: false,
isPalette: false,
bitsPerSample: 8,
pages: 5,
pagePrimary: 4,
compression: 'av1',
resolutionUnit: 'cm',
hasProfile: false,
hasAlpha: false,
autoOrient: { width: 4096, height: 800 },
});
const data = await sharp(inputAvifWithPitmBox)
.png({ compressionLevel: 0 })
.toBuffer();
const { size, ...pngMetadata } = await sharp(data).metadata();
assert.deepStrictEqual(pngMetadata, {
format: 'png',
width: 4096,
height: 800,
space: 'srgb',
channels: 3,
depth: 'uchar',
isProgressive: false,
isPalette: false,
bitsPerSample: 8,
hasProfile: false,
hasAlpha: false,
autoOrient: { width: 4096, height: 800 },
});
});
}); });