Compare commits

...

3 Commits

Author SHA1 Message Date
Lovell Fuller
17d4a684df WIP: Switch from custom VError to standard runtime_error 2026-01-23 22:26:15 +00:00
Lovell Fuller
ed6b7384d0 Ensure TIFF output bitdepth option is limited to 1, 2 or 4 2026-01-23 21:29:40 +00:00
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
14 changed files with 44 additions and 45 deletions

View File

@@ -717,7 +717,7 @@ instead of providing `xres` and `yres` in pixels/mm.
| [options.xres] | <code>number</code> | <code>1.0</code> | horizontal resolution in pixels/mm | | [options.xres] | <code>number</code> | <code>1.0</code> | horizontal resolution in pixels/mm |
| [options.yres] | <code>number</code> | <code>1.0</code> | vertical resolution in pixels/mm | | [options.yres] | <code>number</code> | <code>1.0</code> | vertical resolution in pixels/mm |
| [options.resolutionUnit] | <code>string</code> | <code>&quot;&#x27;inch&#x27;&quot;</code> | resolution unit options: inch, cm | | [options.resolutionUnit] | <code>string</code> | <code>&quot;&#x27;inch&#x27;&quot;</code> | resolution unit options: inch, cm |
| [options.bitdepth] | <code>number</code> | <code>8</code> | reduce bitdepth to 1, 2 or 4 bit | | [options.bitdepth] | <code>number</code> | <code>0</code> | reduce bitdepth to 1, 2 or 4 bit |
| [options.miniswhite] | <code>boolean</code> | <code>false</code> | write 1-bit images as miniswhite | | [options.miniswhite] | <code>boolean</code> | <code>false</code> | write 1-bit images as miniswhite |
**Example** **Example**

View File

@@ -20,6 +20,8 @@ slug: changelog/v0.35.0
* Upgrade to libvips v8.18.0 for upstream bug fixes. * Upgrade to libvips v8.18.0 for upstream bug fixes.
* Ensure TIFF output `bitdepth` option is limited to 1, 2 or 4.
* Deprecate Windows 32-bit (win32-ia32) prebuilt binaries. * Deprecate Windows 32-bit (win32-ia32) prebuilt binaries.
* Add AVIF/HEIF `tune` option for control over quality metrics. * Add AVIF/HEIF `tune` option for control over quality metrics.

View File

@@ -366,7 +366,7 @@ const Sharp = function (input, options) {
tiffPredictor: 'horizontal', tiffPredictor: 'horizontal',
tiffPyramid: false, tiffPyramid: false,
tiffMiniswhite: false, tiffMiniswhite: false,
tiffBitdepth: 8, tiffBitdepth: 0,
tiffTile: false, tiffTile: false,
tiffTileHeight: 256, tiffTileHeight: 256,
tiffTileWidth: 256, tiffTileWidth: 256,

4
lib/index.d.ts vendored
View File

@@ -1479,8 +1479,8 @@ declare namespace sharp {
xres?: number | undefined; xres?: number | undefined;
/** Vertical resolution in pixels/mm (optional, default 1.0) */ /** Vertical resolution in pixels/mm (optional, default 1.0) */
yres?: number | undefined; yres?: number | undefined;
/** Reduce bitdepth to 1, 2 or 4 bit (optional, default 8) */ /** Reduce bitdepth to 1, 2 or 4 bit (optional) */
bitdepth?: 1 | 2 | 4 | 8 | undefined; bitdepth?: 1 | 2 | 4 | undefined;
/** Write 1-bit images as miniswhite (optional, default false) */ /** Write 1-bit images as miniswhite (optional, default false) */
miniswhite?: boolean | undefined; miniswhite?: boolean | undefined;
/** Resolution unit options: inch, cm (optional, default 'inch') */ /** Resolution unit options: inch, cm (optional, default 'inch') */

View File

@@ -1055,7 +1055,7 @@ function trySetAnimationOptions (source, target) {
* @param {number} [options.xres=1.0] - horizontal resolution in pixels/mm * @param {number} [options.xres=1.0] - horizontal resolution in pixels/mm
* @param {number} [options.yres=1.0] - vertical resolution in pixels/mm * @param {number} [options.yres=1.0] - vertical resolution in pixels/mm
* @param {string} [options.resolutionUnit='inch'] - resolution unit options: inch, cm * @param {string} [options.resolutionUnit='inch'] - resolution unit options: inch, cm
* @param {number} [options.bitdepth=8] - reduce bitdepth to 1, 2 or 4 bit * @param {number} [options.bitdepth=0] - reduce bitdepth to 1, 2 or 4 bit
* @param {boolean} [options.miniswhite=false] - write 1-bit images as miniswhite * @param {boolean} [options.miniswhite=false] - write 1-bit images as miniswhite
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid options * @throws {Error} Invalid options
@@ -1070,10 +1070,10 @@ function tiff (options) {
} }
} }
if (is.defined(options.bitdepth)) { if (is.defined(options.bitdepth)) {
if (is.integer(options.bitdepth) && is.inArray(options.bitdepth, [1, 2, 4, 8])) { if (is.integer(options.bitdepth) && is.inArray(options.bitdepth, [1, 2, 4])) {
this.options.tiffBitdepth = options.bitdepth; this.options.tiffBitdepth = options.bitdepth;
} else { } else {
throw is.invalidParameterError('bitdepth', '1, 2, 4 or 8', options.bitdepth); throw is.invalidParameterError('bitdepth', '1, 2 or 4', options.bitdepth);
} }
} }
// tiling // tiling

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

@@ -510,11 +510,11 @@ namespace sharp {
option = GetOptionsForImageType(imageType, descriptor); option = GetOptionsForImageType(imageType, descriptor);
image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option); image = VImage::new_from_buffer(descriptor->buffer, descriptor->bufferLength, nullptr, option);
} }
} catch (vips::VError const &err) { } catch (std::runtime_error const &err) {
throw vips::VError(std::string("Input buffer has corrupt header: ") + err.what()); throw std::runtime_error(std::string("Input buffer has corrupt header: ") + err.what());
} }
} else { } else {
throw vips::VError("Input buffer contains unsupported image format"); throw std::runtime_error("Input buffer contains unsupported image format");
} }
} }
} else { } else {
@@ -585,10 +585,10 @@ namespace sharp {
imageType = DetermineImageType(descriptor->file.data()); imageType = DetermineImageType(descriptor->file.data());
if (imageType == ImageType::MISSING) { if (imageType == ImageType::MISSING) {
if (descriptor->file.find("<svg") != std::string::npos) { if (descriptor->file.find("<svg") != std::string::npos) {
throw vips::VError("Input file is missing, did you mean " throw std::runtime_error("Input file is missing, did you mean "
"sharp(Buffer.from('" + descriptor->file.substr(0, 8) + "...')?"); "sharp(Buffer.from('" + descriptor->file.substr(0, 8) + "...')?");
} }
throw vips::VError("Input file is missing: " + descriptor->file); throw std::runtime_error("Input file is missing: " + descriptor->file);
} }
if (imageType != ImageType::UNKNOWN) { if (imageType != ImageType::UNKNOWN) {
try { try {
@@ -600,11 +600,11 @@ namespace sharp {
option = GetOptionsForImageType(imageType, descriptor); option = GetOptionsForImageType(imageType, descriptor);
image = VImage::new_from_file(descriptor->file.data(), option); image = VImage::new_from_file(descriptor->file.data(), option);
} }
} catch (vips::VError const &err) { } catch (std::runtime_error const &err) {
throw vips::VError(std::string("Input file has corrupt header: ") + err.what()); throw std::runtime_error(std::string("Input file has corrupt header: ") + err.what());
} }
} else { } else {
throw vips::VError("Input file contains unsupported image format"); throw std::runtime_error("Input file contains unsupported image format");
} }
} }
} }
@@ -612,7 +612,7 @@ namespace sharp {
// Limit input images to a given number of pixels, where pixels = width * height // Limit input images to a given number of pixels, where pixels = width * height
if (descriptor->limitInputPixels > 0 && if (descriptor->limitInputPixels > 0 &&
static_cast<uint64_t>(image.width()) * image.height() > descriptor->limitInputPixels) { static_cast<uint64_t>(image.width()) * image.height() > descriptor->limitInputPixels) {
throw vips::VError("Input image exceeds pixel limit"); throw std::runtime_error("Input image exceeds pixel limit");
} }
return std::make_tuple(image, imageType); return std::make_tuple(image, imageType);
} }
@@ -788,19 +788,19 @@ namespace sharp {
: image.height(); : image.height();
if (imageType == ImageType::JPEG) { if (imageType == ImageType::JPEG) {
if (image.width() > 65535 || height > 65535) { if (image.width() > 65535 || height > 65535) {
throw vips::VError("Processed image is too large for the JPEG format"); throw std::runtime_error("Processed image is too large for the JPEG format");
} }
} else if (imageType == ImageType::WEBP) { } else if (imageType == ImageType::WEBP) {
if (image.width() > 16383 || height > 16383) { if (image.width() > 16383 || height > 16383) {
throw vips::VError("Processed image is too large for the WebP format"); throw std::runtime_error("Processed image is too large for the WebP format");
} }
} else if (imageType == ImageType::GIF) { } else if (imageType == ImageType::GIF) {
if (image.width() > 65535 || height > 65535) { if (image.width() > 65535 || height > 65535) {
throw vips::VError("Processed image is too large for the GIF format"); throw std::runtime_error("Processed image is too large for the GIF format");
} }
} else if (imageType == ImageType::HEIF) { } else if (imageType == ImageType::HEIF) {
if (image.width() > 16384 || height > 16384) { if (image.width() > 16384 || height > 16384) {
throw vips::VError("Processed image is too large for the HEIF format"); throw std::runtime_error("Processed image is too large for the HEIF format");
} }
} }
} }

View File

@@ -31,7 +31,7 @@ class MetadataWorker : public Napi::AsyncWorker {
sharp::ImageType imageType = sharp::ImageType::UNKNOWN; sharp::ImageType imageType = sharp::ImageType::UNKNOWN;
try { try {
std::tie(image, imageType) = OpenInput(baton->input); std::tie(image, imageType) = OpenInput(baton->input);
} catch (vips::VError const &err) { } catch (std::runtime_error const &err) {
(baton->err).append(err.what()); (baton->err).append(err.what());
} }
if (imageType != sharp::ImageType::UNKNOWN) { if (imageType != sharp::ImageType::UNKNOWN) {

View File

@@ -14,7 +14,6 @@
#include "./operations.h" #include "./operations.h"
using vips::VImage; using vips::VImage;
using vips::VError;
namespace sharp { namespace sharp {
/* /*
@@ -287,7 +286,7 @@ namespace sharp {
*/ */
VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt, int const margin) { VImage Trim(VImage image, std::vector<double> background, double threshold, bool const lineArt, int const margin) {
if (image.width() < 3 && image.height() < 3) { if (image.width() < 3 && image.height() < 3) {
throw VError("Image to trim must be at least 3x3 pixels"); throw std::runtime_error("Image to trim must be at least 3x3 pixels");
} }
if (background.size() == 0) { if (background.size() == 0) {
// Top-left pixel provides the default background colour if none is given // Top-left pixel provides the default background colour if none is given
@@ -361,7 +360,7 @@ namespace sharp {
VImage Linear(VImage image, std::vector<double> const a, std::vector<double> const b) { VImage Linear(VImage image, std::vector<double> const a, std::vector<double> const b) {
size_t const bands = static_cast<size_t>(image.bands()); size_t const bands = static_cast<size_t>(image.bands());
if (a.size() > bands) { if (a.size() > bands) {
throw VError("Band expansion using linear is unsupported"); throw std::runtime_error("Band expansion using linear is unsupported");
} }
bool const uchar = !Is16Bit(image.interpretation()); bool const uchar = !Is16Bit(image.interpretation());
if (image.has_alpha() && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) { if (image.has_alpha() && a.size() != bands && (a.size() == 1 || a.size() == bands - 1 || bands - 1 == 1)) {

View File

@@ -274,7 +274,7 @@ class PipelineWorker : public Napi::AsyncWorker {
} }
sharp::SetDensity(image, baton->input->density); sharp::SetDensity(image, baton->input->density);
if (image.width() > 32767 || image.height() > 32767) { if (image.width() > 32767 || image.height() > 32767) {
throw vips::VError("Input SVG image will exceed 32767x32767 pixel limit when scaled"); throw std::runtime_error("Input SVG image will exceed 32767x32767 pixel limit when scaled");
} }
} else if (inputImageType == sharp::ImageType::PDF) { } else if (inputImageType == sharp::ImageType::PDF) {
if (baton->input->buffer != nullptr) { if (baton->input->buffer != nullptr) {
@@ -290,7 +290,7 @@ class PipelineWorker : public Napi::AsyncWorker {
} }
} else { } else {
if (inputImageType == sharp::ImageType::SVG && (image.width() > 32767 || image.height() > 32767)) { if (inputImageType == sharp::ImageType::SVG && (image.width() > 32767 || image.height() > 32767)) {
throw vips::VError("Input SVG image exceeds 32767x32767 pixel limit"); throw std::runtime_error("Input SVG image exceeds 32767x32767 pixel limit");
} }
} }
if (baton->input->autoOrient) { if (baton->input->autoOrient) {
@@ -675,7 +675,7 @@ class PipelineWorker : public Napi::AsyncWorker {
// Verify within current dimensions // Verify within current dimensions
if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) { if (compositeImage.width() > image.width() || compositeImage.height() > image.height()) {
throw vips::VError("Image to composite must have same dimensions or smaller"); throw std::runtime_error("Image to composite must have same dimensions or smaller");
} }
// Check if overlay is tiled // Check if overlay is tiled
if (composite->tile) { if (composite->tile) {
@@ -1086,20 +1086,19 @@ class PipelineWorker : public Napi::AsyncWorker {
// Get raw image data // Get raw image data
baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength)); baton->bufferOut = static_cast<char*>(image.write_to_memory(&baton->bufferOutLength));
if (baton->bufferOut == nullptr) { if (baton->bufferOut == nullptr) {
(baton->err).append("Could not allocate enough memory for raw output"); throw std::runtime_error("Could not allocate enough memory for raw output");
return Error();
} }
baton->formatOut = "raw"; baton->formatOut = "raw";
} else { } else {
// Unsupported output format // Unsupported output format
(baton->err).append("Unsupported output format "); auto unsupported = std::string("Unsupported output format ");
if (baton->formatOut == "input") { if (baton->formatOut == "input") {
(baton->err).append("when trying to match input format of "); unsupported.append("when trying to match input format of ");
(baton->err).append(ImageTypeId(inputImageType)); unsupported.append(ImageTypeId(inputImageType));
} else { } else {
(baton->err).append(baton->formatOut); unsupported.append(baton->formatOut);
} }
return Error(); throw std::runtime_error(unsupported);
} }
} else { } else {
// File output // File output
@@ -1274,7 +1273,7 @@ class PipelineWorker : public Napi::AsyncWorker {
return Error(); return Error();
} }
} }
} catch (vips::VError const &err) { } catch (std::runtime_error const &err) {
char const *what = err.what(); char const *what = err.what();
if (what && what[0]) { if (what && what[0]) {
(baton->err).append(what); (baton->err).append(what);
@@ -1306,7 +1305,6 @@ class PipelineWorker : public Napi::AsyncWorker {
} }
warning = sharp::VipsWarningPop(); warning = sharp::VipsWarningPop();
} }
if (baton->err.empty()) { if (baton->err.empty()) {
int width = baton->width; int width = baton->width;
int height = baton->height; int height = baton->height;
@@ -1407,7 +1405,7 @@ class PipelineWorker : public Napi::AsyncWorker {
void MultiPageUnsupported(int const pages, std::string op) { void MultiPageUnsupported(int const pages, std::string op) {
if (pages > 1) { if (pages > 1) {
throw vips::VError(op + " is not supported for multi-page images"); throw std::runtime_error(op + " is not supported for multi-page images");
} }
} }

View File

@@ -365,7 +365,7 @@ struct PipelineBaton {
tiffBigtiff(false), tiffBigtiff(false),
tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL), tiffPredictor(VIPS_FOREIGN_TIFF_PREDICTOR_HORIZONTAL),
tiffPyramid(false), tiffPyramid(false),
tiffBitdepth(8), tiffBitdepth(0),
tiffMiniswhite(false), tiffMiniswhite(false),
tiffTile(false), tiffTile(false),
tiffTileHeight(256), tiffTileHeight(256),

View File

@@ -39,7 +39,7 @@ class StatsWorker : public Napi::AsyncWorker {
sharp::ImageType imageType = sharp::ImageType::UNKNOWN; sharp::ImageType imageType = sharp::ImageType::UNKNOWN;
try { try {
std::tie(image, imageType) = OpenInput(baton->input); std::tie(image, imageType) = OpenInput(baton->input);
} catch (vips::VError const &err) { } catch (std::runtime_error const &err) {
(baton->err).append(err.what()); (baton->err).append(err.what());
} }
if (imageType != sharp::ImageType::UNKNOWN) { if (imageType != sharp::ImageType::UNKNOWN) {
@@ -92,7 +92,7 @@ class StatsWorker : public Napi::AsyncWorker {
baton->dominantRed = dx * 16 + 8; baton->dominantRed = dx * 16 + 8;
baton->dominantGreen = dy * 16 + 8; baton->dominantGreen = dy * 16 + 8;
baton->dominantBlue = dz * 16 + 8; baton->dominantBlue = dz * 16 + 8;
} catch (vips::VError const &err) { } catch (std::runtime_error const &err) {
(baton->err).append(err.what()); (baton->err).append(err.what());
} }
} }
@@ -112,7 +112,6 @@ class StatsWorker : public Napi::AsyncWorker {
debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) }); debuglog.Call(Receiver().Value(), { Napi::String::New(env, warning) });
warning = sharp::VipsWarningPop(); warning = sharp::VipsWarningPop();
} }
if (baton->err.empty()) { if (baton->err.empty()) {
// Stats Object // Stats Object
Napi::Object info = Napi::Object::New(env); Napi::Object info = Napi::Object::New(env);

View File

@@ -244,7 +244,7 @@ Napi::Value _maxColourDistance(const Napi::CallbackInfo& info) {
} }
// Calculate colour distance // Calculate colour distance
maxColourDistance = image1.dE00(image2).max(); maxColourDistance = image1.dE00(image2).max();
} catch (vips::VError const &err) { } catch (std::runtime_error const &err) {
throw Napi::Error::New(env, err.what()); throw Napi::Error::New(env, err.what());
} }

View File

@@ -122,7 +122,6 @@ describe('TIFF', () => {
sharp(fixtures.inputTiff8BitDepth) sharp(fixtures.inputTiff8BitDepth)
.toColourspace('b-w') // can only squash 1 band uchar images .toColourspace('b-w') // can only squash 1 band uchar images
.tiff({ .tiff({
bitdepth: 8,
compression: 'none', compression: 'none',
predictor: 'none' predictor: 'none'
}) })
@@ -154,7 +153,7 @@ describe('TIFF', () => {
it('Invalid TIFF bitdepth value throws error', () => { it('Invalid TIFF bitdepth value throws error', () => {
assert.throws(() => { assert.throws(() => {
sharp().tiff({ bitdepth: 3 }); sharp().tiff({ bitdepth: 3 });
}, /Error: Expected 1, 2, 4 or 8 for bitdepth but received 3 of type number/); }, /Error: Expected 1, 2 or 4 for bitdepth but received 3 of type number/);
}); });
it('TIFF setting xres and yres on file', () => it('TIFF setting xres and yres on file', () =>