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.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.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**
```js

View File

@@ -38,4 +38,7 @@ slug: changelog/v0.35.0
[#4480](https://github.com/lovell/sharp/issues/4480)
[@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.

View File

@@ -1175,7 +1175,7 @@ function tiff (options) {
* @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 {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}
* @throws {Error} Invalid options
*/
@@ -1255,7 +1255,11 @@ function heif (options) {
}
if (is.defined(options.tune)) {
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 {
throw is.invalidParameterError('tune', 'one of: psnr, ssim, iq', options.tune);
}

View File

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

View File

@@ -426,7 +426,7 @@ namespace sharp {
}
if (ImageTypeSupportsPage(imageType)) {
option->set("n", descriptor->pages);
option->set("page", descriptor->page);
option->set("page", std::max(0, descriptor->page));
}
switch (imageType) {
case ImageType::SVG:
@@ -456,6 +456,22 @@ namespace sharp {
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)
*/
@@ -490,6 +506,9 @@ namespace sharp {
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
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) {
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);
if (imageType == ImageType::SVG || imageType == ImageType::PDF || imageType == ImageType::MAGICK) {
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) {
throw vips::VError(std::string("Input file has corrupt header: ") + err.what());

View File

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

View File

@@ -84,7 +84,7 @@ class PipelineWorker : public Napi::AsyncWorker {
if (nPages == -1) {
// 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
? image.get_int(VIPS_META_N_PAGES) - baton->input->page
? image.get_int(VIPS_META_N_PAGES) - std::max(0, baton->input->page)
: 1;
}

View File

@@ -127,7 +127,7 @@ module.exports = {
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
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'),
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 sharp = require('../../');
const { inputAvif, inputJpg, inputGifAnimated } = require('../fixtures');
const {
inputAvif,
inputAvifWithPitmBox,
inputJpg,
inputGifAnimated,
inputPng,
} = require('../fixtures');
describe('AVIF', () => {
it('called without options does not throw an error', () => {
@@ -17,16 +23,13 @@ describe('AVIF', () => {
});
it('can convert AVIF to JPEG', async () => {
const data = await sharp(inputAvif)
.resize(32)
.jpeg()
.toBuffer();
const data = await sharp(inputAvif).resize(32).jpeg().toBuffer();
const { size, ...metadata } = await sharp(data).metadata();
void size;
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 13,
width: 32
width: 32,
},
channels: 3,
chromaSubsampling: '4:2:0',
@@ -41,7 +44,7 @@ describe('AVIF', () => {
isProgressive: false,
isPalette: false,
space: 'srgb',
width: 32
width: 32,
});
});
@@ -55,7 +58,7 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 26,
width: 32
width: 32,
},
channels: 3,
compression: 'av1',
@@ -70,20 +73,47 @@ describe('AVIF', () => {
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
width: 32,
});
});
it('can passthrough AVIF', async () => {
const data = await sharp(inputAvif)
it('can convert PNG to lossless AVIF', async () => {
const data = await sharp(inputPng)
.resize(32)
.avif({ lossless: true, effort: 0 })
.toBuffer();
const { size, ...metadata } = await sharp(data).metadata();
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, {
autoOrient: {
height: 13,
width: 32
width: 32,
},
channels: 3,
compression: 'av1',
@@ -98,7 +128,7 @@ describe('AVIF', () => {
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
width: 32,
});
});
@@ -112,7 +142,7 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 300,
width: 10
width: 10,
},
channels: 4,
compression: 'av1',
@@ -127,7 +157,7 @@ describe('AVIF', () => {
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 10
width: 10,
});
});
@@ -142,7 +172,7 @@ describe('AVIF', () => {
assert.deepStrictEqual(metadata, {
autoOrient: {
height: 26,
width: 32
width: 32,
},
channels: 3,
compression: 'av1',
@@ -157,30 +187,37 @@ describe('AVIF', () => {
pagePrimary: 0,
pages: 1,
space: 'srgb',
width: 32
width: 32,
});
});
it('Invalid width - too large', async () =>
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 () =>
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', () =>
assert.throws(
() => 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 () => {
const ssim = await sharp(inputJpg)
@@ -192,5 +229,47 @@ describe('AVIF', () => {
.avif({ tune: 'iq', effort: 0 })
.toBuffer();
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 },
});
});
});