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
2 changes: 1 addition & 1 deletion lib/constructor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ function clone () {
clone.options.queueListener = queueListener;
// Pass 'finish' event to clone for Stream-based input
if (this._isStreamInput()) {
this.on('finish', () => {
this._whenStreamInFinished(() => {
// Clone inherits input data
this._flattenBufferIn();
clone.options.input.buffer = this.options.input.buffer;
Expand Down
35 changes: 20 additions & 15 deletions lib/input.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -531,11 +531,6 @@ function _createInputDescriptor (input, inputOptions, containerOptions) {
function _write (chunk, _encoding, callback) {
if (Array.isArray(this.options.input.buffer)) {
if (is.buffer(chunk)) {
if (this.options.input.buffer.length === 0) {
this.on('finish', () => {
this.streamInFinished = true;
});
}
this.options.input.buffer.push(chunk);
callback();
} else {
Expand Down Expand Up @@ -565,6 +560,20 @@ function _isStreamInput () {
return Array.isArray(this.options.input.buffer);
}

/**
* Call fn when the Writable side of Stream-based input has finished,
* or immediately when it already has, as 'finish' is emitted only once.
* @private
* @param {Function} fn
*/
function _whenStreamInFinished (fn) {
if (this.writableFinished) {
fn();
} else {
this.once('finish', fn);
}
}

/**
* Fast access to (uncached) image metadata without decoding any compressed pixel data.
*
Expand Down Expand Up @@ -642,7 +651,7 @@ function metadata (callback) {
const stack = Error();
if (is.fn(callback)) {
if (this._isStreamInput()) {
this.on('finish', () => {
this._whenStreamInFinished(() => {
this._flattenBufferIn();
sharp.metadata(this.options, (err, metadata) => {
if (err) {
Expand All @@ -665,7 +674,7 @@ function metadata (callback) {
} else {
if (this._isStreamInput()) {
return new Promise((resolve, reject) => {
const finished = () => {
this._whenStreamInFinished(() => {
this._flattenBufferIn();
sharp.metadata(this.options, (err, metadata) => {
if (err) {
Expand All @@ -674,12 +683,7 @@ function metadata (callback) {
resolve(metadata);
}
});
};
if (this.writableFinished) {
finished();
} else {
this.once('finish', finished);
}
});
});
} else {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -744,7 +748,7 @@ function stats (callback) {
const stack = Error();
if (is.fn(callback)) {
if (this._isStreamInput()) {
this.on('finish', () => {
this._whenStreamInFinished(() => {
this._flattenBufferIn();
sharp.stats(this.options, (err, stats) => {
if (err) {
Expand All @@ -767,7 +771,7 @@ function stats (callback) {
} else {
if (this._isStreamInput()) {
return new Promise((resolve, reject) => {
this.on('finish', function () {
this._whenStreamInFinished(() => {
this._flattenBufferIn();
sharp.stats(this.options, (err, stats) => {
if (err) {
Expand Down Expand Up @@ -805,6 +809,7 @@ export default (Sharp) => {
_write,
_flattenBufferIn,
_isStreamInput,
_whenStreamInFinished,
// Public
metadata,
stats
Expand Down
9 changes: 3 additions & 6 deletions lib/output.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1649,7 +1649,7 @@ function _pipeline (callback, stack) {
// output=file/buffer
if (this._isStreamInput()) {
// output=file/buffer, input=stream
this.on('finish', () => {
this._whenStreamInFinished(() => {
this._flattenBufferIn();
sharp.pipeline(this.options, (err, data, info) => {
if (err) {
Expand All @@ -1674,7 +1674,7 @@ function _pipeline (callback, stack) {
// output=stream
if (this._isStreamInput()) {
// output=stream, input=stream
this.once('finish', () => {
this._whenStreamInFinished(() => {
this._flattenBufferIn();
sharp.pipeline(this.options, (err, data, info) => {
if (err) {
Expand All @@ -1687,9 +1687,6 @@ function _pipeline (callback, stack) {
this.on('end', () => this.emit('close'));
});
});
if (this.streamInFinished) {
this.emit('finish');
}
} else {
// output=stream, input=file/buffer
sharp.pipeline(this.options, (err, data, info) => {
Expand All @@ -1709,7 +1706,7 @@ function _pipeline (callback, stack) {
if (this._isStreamInput()) {
// output=promise, input=stream
return new Promise((resolve, reject) => {
this.once('finish', () => {
this._whenStreamInFinished(() => {
this._flattenBufferIn();
sharp.pipeline(this.options, (err, data, info) => {
if (err) {
Expand Down
76 changes: 76 additions & 0 deletions test/unit/io.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
SPDX-License-Identifier: Apache-2.0
*/

const { once } = require('node:events');
const { createReadStream, createWriteStream } = require('node:fs');
const fs = require('node:fs/promises');
const path = require('node:path');
Expand Down Expand Up @@ -368,6 +369,81 @@ suite('Input/output', () => {
await fs.rm(outputJpg);
});

suite('Output requested after Writable side of Stream-based input has finished', () => {
const finishedPipeline = async (pipeline) => {
createReadStream(fixtures.inputJpg).pipe(pipeline);
await once(pipeline, 'finish');
return pipeline;
};

test('Write to Buffer via Promise', async (t) => {
t.plan(2);
const pipeline = await finishedPipeline(sharp().resize(320, 240));
const { info } = await pipeline.toBuffer({ resolveWithObject: true });
t.assert.strictEqual(320, info.width);
t.assert.strictEqual(240, info.height);
});

test('Write to Buffer via callback', async (t) => {
t.plan(2);
const pipeline = await finishedPipeline(sharp().resize(320, 240));
const info = await new Promise((resolve, reject) => {
pipeline.toBuffer((err, _data, info) => err ? reject(err) : resolve(info));
});
t.assert.strictEqual(320, info.width);
t.assert.strictEqual(240, info.height);
});

test('Write to File via Promise', async (t) => {
t.plan(2);
const pipeline = await finishedPipeline(sharp().resize(320, 240));
const info = await pipeline.toFile(outputJpg);
t.assert.strictEqual(320, info.width);
t.assert.strictEqual(240, info.height);
await fs.rm(outputJpg);
});

test('Read metadata via Promise', async (t) => {
t.plan(1);
const pipeline = await finishedPipeline(sharp());
const { width } = await pipeline.metadata();
t.assert.strictEqual(2725, width);
});

test('Read metadata via callback', async (t) => {
t.plan(1);
const pipeline = await finishedPipeline(sharp());
const { width } = await new Promise((resolve, reject) => {
pipeline.metadata((err, metadata) => err ? reject(err) : resolve(metadata));
});
t.assert.strictEqual(2725, width);
});

test('Read stats via Promise', async (t) => {
t.plan(1);
const pipeline = await finishedPipeline(sharp());
const { isOpaque } = await pipeline.stats();
t.assert.strictEqual(true, isOpaque);
});

test('Read stats via callback', async (t) => {
t.plan(1);
const pipeline = await finishedPipeline(sharp());
const { isOpaque } = await new Promise((resolve, reject) => {
pipeline.stats((err, stats) => err ? reject(err) : resolve(stats));
});
t.assert.strictEqual(true, isOpaque);
});

test('Clone inherits input', async (t) => {
t.plan(2);
const pipeline = await finishedPipeline(sharp());
const { info } = await pipeline.clone().resize(320, 240).toBuffer({ resolveWithObject: true });
t.assert.strictEqual(320, info.width);
t.assert.strictEqual(240, info.height);
});
});

test('Non-Stream input generates error when provided Stream-like data', async (t) => {
t.plan(2);
t.assert.throws(
Expand Down