Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/constructor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,14 @@ const Sharp = function (input, options) {
// Function to notify of queue length changes
queueListener
};
if (is.defined(options) && is.defined(options.signal)) {
if (!(options.signal instanceof AbortSignal)) {
throw is.invalidParameterError('signal', 'AbortSignal', options.signal);
}
this.options.signal = options.signal;
} else {
this.options.signal = null;
}
this.options.input = this._createInputDescriptor(input, options, { allowStream: true });
return this;
};
Expand Down
96 changes: 73 additions & 23 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,35 +650,83 @@ declare namespace sharp {
* @throws {Error} Invalid parameters
* @returns A promise that fulfills with an object containing information on the resulting file
*/
toFile(fileOut: string): Promise<OutputInfo>;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
toFile(fileOut: string): Promise<OutputInfo>;

/**
* Write output image data to a file.
* @param fileOut The path to write the image data to.
* @param options Options object with signal for cancellation.
* @throws {Error} Invalid parameters
* @returns A promise that fulfills with an object containing information on the resulting file
*/
toFile(fileOut: string, options: { signal?: AbortSignal }): Promise<OutputInfo>;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* @param callback Callback function called on completion with three arguments (err, buffer, info).
* @returns A sharp instance that can be used to chain operations
*/
toBuffer(callback: (err: Error, buffer: Buffer<ArrayBuffer>, info: OutputInfo) => void): Sharp;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* The underlying `ArrayBuffer` may be marked as non-transferable by some JavaScript runtimes.
* @param options resolve options
* @param options.resolveWithObject Resolve the Promise with an Object containing data and info properties instead of resolving only with data.
* @returns A promise that resolves with the Buffer data.
*/
toBuffer(options?: { resolveWithObject: false }): Promise<Buffer<ArrayBuffer>>;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* The underlying `ArrayBuffer` may be marked as non-transferable by some JavaScript runtimes.
* @param options resolve options
* @param options.resolveWithObject Resolve the Promise with an Object containing data and info properties instead of resolving only with data.
* @returns A promise that resolves with an object containing the Buffer data and an info object containing the output image format, size (bytes), width, height and channels
*/
toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer<ArrayBuffer>; info: OutputInfo }>;
/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* @param options Options object with signal for cancellation.
* @param callback Callback function called on completion with three arguments (err, buffer, info).
* @returns A sharp instance that can be used to chain operations
*/
toBuffer(options: { signal: AbortSignal }, callback: (err: Error, buffer: Buffer<ArrayBuffer>, info: OutputInfo) => void): Sharp;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* The underlying `ArrayBuffer` may be marked as non-transferable by some JavaScript runtimes.
* @param options Options object with signal for cancellation.
* @param options.signal AbortSignal to cancel the operation.
* @returns A promise that resolves with the Buffer data.
*/
toBuffer(options?: { signal?: AbortSignal }): Promise<Buffer<ArrayBuffer>>;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* The underlying `ArrayBuffer` may be marked as non-transferable by some JavaScript runtimes.
* @param options resolve options
* @param options.resolveWithObject Resolve the Promise with an Object containing data and info properties instead of resolving only with data.
* @returns A promise that resolves with the Buffer data.
*/
toBuffer(options?: { resolveWithObject?: false }): Promise<Buffer<ArrayBuffer>>;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* The underlying `ArrayBuffer` may be marked as non-transferable by some JavaScript runtimes.
* @param options resolve options
* @param options.resolveWithObject Resolve the Promise with an Object containing data and info properties instead of resolving only with data.
* @returns A promise that resolves with an object containing the Buffer data and an info object containing the output image format, size (bytes), width, height and channels
*/
toBuffer(options: { resolveWithObject: true }): Promise<{ data: Buffer<ArrayBuffer>; info: OutputInfo }>;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* @param options resolve options
* @param options.resolveWithObject Resolve the Promise with an Object containing data and info properties instead of resolving only with data.
* @param callback Callback function called on completion with three arguments (err, buffer, info).
* @returns A sharp instance that can be used to chain operations
*/
toBuffer(options: { resolveWithObject?: false }, callback: (err: Error, buffer: Buffer<ArrayBuffer>, info: OutputInfo) => void): Sharp;

/**
* Write output to a Buffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
* By default, the format will match the input image, except SVG input which becomes PNG output.
* @param options resolve options
* @param options.resolveWithObject Resolve the Promise with an Object containing data and info properties instead of resolving only with data.
* @param callback Callback function called on completion with two arguments (err, result).
* @returns A sharp instance that can be used to chain operations
*/
toBuffer(options: { resolveWithObject: true }, callback: (err: Error, result: { data: Buffer<ArrayBuffer>; info: OutputInfo }) => void): Sharp;

/**
* Write output to a Uint8Array backed by a transferable ArrayBuffer. JPEG, PNG, WebP, AVIF, TIFF, GIF and RAW output are supported.
Expand Down Expand Up @@ -1045,6 +1093,8 @@ declare namespace sharp {
text?: CreateText | undefined;
/** Describes how array of input images should be joined. */
join?: Join | undefined;
/** AbortSignal to cancel processing. */
signal?: AbortSignal | undefined;
}

interface CacheOptions {
Expand Down
161 changes: 119 additions & 42 deletions lib/output.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ const bitdepthFromColourCount = (colours) => 1 << 32 - Math.clz32(Math.ceil(Math
* @returns {Promise<Object>} - when no callback is provided
* @throws {Error} Invalid parameters
*/
function toFile (fileOut, callback) {
function toFile (fileOut, options, callback) {
if (is.fn(options)) {
callback = options;
options = undefined;
}
let err;
if (!is.string(fileOut)) {
err = new Error('Missing output file path');
Expand All @@ -86,6 +90,12 @@ function toFile (fileOut, callback) {
return Promise.reject(err);
}
} else {
if (is.object(options) && is.defined(options.signal)) {
if (!(options.signal instanceof AbortSignal)) {
throw is.invalidParameterError('signal', 'AbortSignal', options.signal);
}
this.options.signal = options.signal;
}
this.options.fileOut = fileOut;
const stack = Error();
return this._pipeline(callback, stack);
Expand Down Expand Up @@ -151,7 +161,18 @@ function toFile (fileOut, callback) {
*/
function toBuffer (options, callback) {
if (is.object(options)) {
this._setBooleanOption('resolveWithObject', options.resolveWithObject);
if (is.defined(options.signal)) {
if (!(options.signal instanceof AbortSignal)) {
throw is.invalidParameterError('signal', 'AbortSignal', options.signal);
}
// When signal is present, reject resolveWithObject to protect from future breaking changes
if (is.defined(options.resolveWithObject)) {
throw is.invalidParameterError('resolveWithObject', 'undefined when signal is present', options.resolveWithObject);
}
this.options.signal = options.signal;
} else if (is.defined(options.resolveWithObject)) {
this._setBooleanOption('resolveWithObject', options.resolveWithObject);
}
} else if (this.options.resolveWithObject) {
this.options.resolveWithObject = false;
}
Expand Down Expand Up @@ -1645,29 +1666,95 @@ function _read () {
* @private
*/
function _pipeline (callback, stack) {
const signal = this.options.signal;

if (signal?.aborted) {
const err = new Error('The operation was aborted');
err.name = 'AbortError';
err.code = 'ABORT_ERR';
if (typeof callback === 'function') {
callback(err);
return this;
} else if (this.options.streamOut) {
process.nextTick(() => {
this.emit('error', err);
this.push(null);
this.on('end', () => this.emit('close'));
});
return this;
} else {
return Promise.reject(err);
}
}

const pipelineOptions = { ...this.options };
let abortView = null;
let abortListener = null;

if (signal) {
const abortBuffer = new SharedArrayBuffer(1);
abortView = new Int8Array(abortBuffer);
pipelineOptions.abortFlag = abortBuffer;
abortListener = () => {
Atomics.store(abortView, 0, 1);
};
signal.addEventListener('abort', abortListener, { once: true });
}

const cleanup = () => {
if (signal && abortListener) {
signal.removeEventListener('abort', abortListener);
}
};

const wrapCallback = (cb) => {
return (err, data, info) => {
cleanup();
if (err && signal?.aborted) {
const abortErr = new Error('The operation was aborted');
abortErr.name = 'AbortError';
abortErr.code = 'ABORT_ERR';
cb(abortErr);
} else if (err) {
cb(is.nativeError(err, stack));
} else {
cb(null, data, info);
}
};
};

const wrapStreamEmit = (emitError, emitInfo, pushData) => {
return (err, data, info) => {
cleanup();
if (err && signal?.aborted) {
const abortErr = new Error('The operation was aborted');
abortErr.name = 'AbortError';
abortErr.code = 'ABORT_ERR';
emitError(abortErr);
} else if (err) {
emitError(is.nativeError(err, stack));
} else {
emitInfo(info);
pushData(data);
}
pushData(null);
};
};
if (typeof callback === 'function') {
// output=file/buffer
if (this._isStreamInput()) {
// output=file/buffer, input=stream
this.on('finish', () => {
this._flattenBufferIn();
sharp.pipeline(this.options, (err, data, info) => {
if (err) {
callback(is.nativeError(err, stack));
} else {
callback(null, data, info);
}
});
sharp.pipeline(pipelineOptions, wrapCallback((err, data, info) => {
callback(err, data, info);
}));
});
} else {
// output=file/buffer, input=file/buffer
sharp.pipeline(this.options, (err, data, info) => {
if (err) {
callback(is.nativeError(err, stack));
} else {
callback(null, data, info);
}
});
sharp.pipeline(pipelineOptions, wrapCallback((err, data, info) => {
callback(err, data, info);
}));
}
return this;
} else if (this.options.streamOut) {
Expand All @@ -1676,32 +1763,22 @@ function _pipeline (callback, stack) {
// output=stream, input=stream
this.once('finish', () => {
this._flattenBufferIn();
sharp.pipeline(this.options, (err, data, info) => {
if (err) {
this.emit('error', is.nativeError(err, stack));
} else {
this.emit('info', info);
this.push(data);
}
this.push(null);
this.on('end', () => this.emit('close'));
});
sharp.pipeline(pipelineOptions, wrapStreamEmit(
(err) => this.emit('error', err),
(info) => this.emit('info', info),
(data) => this.push(data)
));
});
if (this.streamInFinished) {
this.emit('finish');
}
} else {
// output=stream, input=file/buffer
sharp.pipeline(this.options, (err, data, info) => {
if (err) {
this.emit('error', is.nativeError(err, stack));
} else {
this.emit('info', info);
this.push(data);
}
this.push(null);
this.on('end', () => this.emit('close'));
});
sharp.pipeline(pipelineOptions, wrapStreamEmit(
(err) => this.emit('error', err),
(info) => this.emit('info', info),
(data) => this.push(data)
));
}
return this;
} else {
Expand All @@ -1711,33 +1788,33 @@ function _pipeline (callback, stack) {
return new Promise((resolve, reject) => {
this.once('finish', () => {
this._flattenBufferIn();
sharp.pipeline(this.options, (err, data, info) => {
sharp.pipeline(pipelineOptions, wrapCallback((err, data, info) => {
if (err) {
reject(is.nativeError(err, stack));
reject(err);
} else {
if (this.options.resolveWithObject) {
resolve({ data, info });
} else {
resolve(data);
}
}
});
}));
});
});
} else {
// output=promise, input=file/buffer
return new Promise((resolve, reject) => {
sharp.pipeline(this.options, (err, data, info) => {
sharp.pipeline(pipelineOptions, wrapCallback((err, data, info) => {
if (err) {
reject(is.nativeError(err, stack));
reject(err);
} else {
if (this.options.resolveWithObject) {
resolve({ data, info });
} else {
resolve(data);
}
}
});
}));
});
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/common.cc
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,29 @@ namespace sharp {
}
}

/*
Attach an event listener for progress updates, used to detect abort via SharedArrayBuffer flag
*/
void SetAbortFlag(VImage image, std::atomic<int8_t> *abortFlag) {
if (abortFlag != nullptr) {
VipsImage *im = image.get_image();
if (im->progress_signal == NULL) {
g_signal_connect(im, "eval", G_CALLBACK(VipsAbortCallBack), abortFlag);
vips_image_set_progress(im, true);
}
}
}

/*
Event listener for progress updates, used to detect abort via SharedArrayBuffer flag
*/
void VipsAbortCallBack(VipsImage *im, VipsProgress *progress, std::atomic<int8_t> *abortFlag) {
if (abortFlag->load(std::memory_order_relaxed) != 0) {
vips_image_set_kill(im, true);
vips_error("abort", "%d%% complete", progress->percent);
}
}

/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given gravity during an embed.
Expand Down
Loading