Add support for GIF output using cgif in prebuilt binaries

This commit is contained in:
Lovell Fuller 2021-11-19 08:29:31 +00:00
parent b876abaf88
commit f7f3e43490
14 changed files with 157 additions and 105 deletions

View File

@ -4,7 +4,7 @@
The typical use case for this high speed Node.js module The typical use case for this high speed Node.js module
is to convert large images in common formats to is to convert large images in common formats to
smaller, web-friendly JPEG, PNG, WebP and AVIF images of varying dimensions. smaller, web-friendly JPEG, PNG, WebP, GIF and AVIF images of varying dimensions.
Resizing an image is typically 4x-5x faster than using the Resizing an image is typically 4x-5x faster than using the
quickest ImageMagick and GraphicsMagick settings quickest ImageMagick and GraphicsMagick settings

View File

@ -4,7 +4,7 @@
The typical use case for this high speed Node.js module The typical use case for this high speed Node.js module
is to convert large images in common formats to is to convert large images in common formats to
smaller, web-friendly JPEG, PNG, AVIF and WebP images of varying dimensions. smaller, web-friendly JPEG, PNG, WebP, GIF and AVIF images of varying dimensions.
Resizing an image is typically 4x-5x faster than using the Resizing an image is typically 4x-5x faster than using the
quickest ImageMagick and GraphicsMagick settings quickest ImageMagick and GraphicsMagick settings
@ -21,9 +21,9 @@ do not require any additional install or runtime dependencies.
### Formats ### Formats
This module supports reading JPEG, PNG, WebP, AVIF, TIFF, GIF and SVG images. This module supports reading JPEG, PNG, WebP, GIF, AVIF, TIFF and SVG images.
Output images can be in JPEG, PNG, WebP, AVIF and TIFF formats as well as uncompressed raw pixel data. Output images can be in JPEG, PNG, WebP, GIF, AVIF and TIFF formats as well as uncompressed raw pixel data.
Streams, Buffer objects and the filesystem can be used for input and output. Streams, Buffer objects and the filesystem can be used for input and output.

View File

@ -6,6 +6,8 @@ Requires libvips v8.12.0
### v0.30.0 - TBD ### v0.30.0 - TBD
* Add support for GIF output to prebuilt binaries.
* Reduce minimum Linux ARM64v8 glibc requirement to 2.17. * Reduce minimum Linux ARM64v8 glibc requirement to 2.17.
* Properly emit close events for duplex streams. * Properly emit close events for duplex streams.

View File

@ -4,7 +4,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Resize large images in common formats to smaller, web-friendly JPEG, PNG, WebP and AVIF images of varying dimensions"> <meta name="description" content="Resize large images in common formats to smaller, web-friendly JPEG, PNG, WebP, GIF and AVIF images of varying dimensions">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; style-src 'unsafe-inline'; <meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; style-src 'unsafe-inline';
img-src 'unsafe-inline' data: https://pixel.plumbing/px/ https://cdn.jsdelivr.net/gh/lovell/ https://www.google-analytics.com; img-src 'unsafe-inline' data: https://pixel.plumbing/px/ https://cdn.jsdelivr.net/gh/lovell/ https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com;

View File

@ -19,7 +19,7 @@ Ready-compiled sharp and libvips binaries are provided for use on the most commo
* macOS x64 (>= 10.13) * macOS x64 (>= 10.13)
* macOS ARM64 * macOS ARM64
* Linux x64 (glibc >= 2.17, musl >= 1.1.24) * Linux x64 (glibc >= 2.17, musl >= 1.1.24)
* Linux ARM64 (glibc >= 2.29, musl >= 1.1.24) * Linux ARM64 (glibc >= 2.17, musl >= 1.1.24)
* Windows x64 * Windows x64
* Windows x86 * Windows x86
@ -27,7 +27,7 @@ An ~7MB tarball containing libvips and its most commonly used dependencies
is downloaded via HTTPS and stored within `node_modules/sharp/vendor` during `npm install`. is downloaded via HTTPS and stored within `node_modules/sharp/vendor` during `npm install`.
This provides support for the This provides support for the
JPEG, PNG, WebP, AVIF, TIFF, GIF (input) and SVG (input) image formats. JPEG, PNG, WebP, AVIF, TIFF, GIF and SVG (input) image formats.
The following platforms have prebuilt libvips but not sharp: The following platforms have prebuilt libvips but not sharp:
@ -38,8 +38,6 @@ The following platforms have prebuilt libvips but not sharp:
The following platforms require compilation of both libvips and sharp from source: The following platforms require compilation of both libvips and sharp from source:
* Linux x86 * Linux x86
* Linux x64 (glibc <= 2.16, includes RHEL/CentOS 6)
* Linux ARM64 (glibc <= 2.28)
* Linux ARMv7 (glibc <= 2.27, musl) * Linux ARMv7 (glibc <= 2.27, musl)
* Linux ARMv6 (glibc <= 2.27, musl) * Linux ARMv6 (glibc <= 2.27, musl)
* Linux PowerPC * Linux PowerPC

View File

@ -13,7 +13,7 @@ const debuglog = util.debuglog('sharp');
/** /**
* Constructor factory to create an instance of `sharp`, to which further methods are chained. * Constructor factory to create an instance of `sharp`, to which further methods are chained.
* *
* JPEG, PNG, WebP, AVIF or TIFF format image data can be streamed out from this object. * JPEG, PNG, WebP, GIF, AVIF or TIFF format image data can be streamed out from this object.
* When using Stream based output, derived attributes are available from the `info` event. * When using Stream based output, derived attributes are available from the `info` event.
* *
* Non-critical problems encountered during processing are emitted as `warning` events. * Non-critical problems encountered during processing are emitted as `warning` events.
@ -246,6 +246,9 @@ const Sharp = function (input, options) {
webpNearLossless: false, webpNearLossless: false,
webpSmartSubsample: false, webpSmartSubsample: false,
webpReductionEffort: 4, webpReductionEffort: 4,
gifBitdepth: 8,
gifEffort: 7,
gifDither: 1,
tiffQuality: 80, tiffQuality: 80,
tiffCompression: 'jpeg', tiffCompression: 'jpeg',
tiffPredictor: 'horizontal', tiffPredictor: 'horizontal',

View File

@ -22,14 +22,15 @@ const formats = new Map([
['j2c', 'jp2'] ['j2c', 'jp2']
]); ]);
const errMagickSave = new Error('GIF output requires libvips with support for ImageMagick');
const errJp2Save = new Error('JP2 output requires libvips with support for OpenJPEG'); const errJp2Save = new Error('JP2 output requires libvips with support for OpenJPEG');
const bitdepthFromColourCount = (colours) => 1 << 31 - Math.clz32(Math.ceil(Math.log2(colours)));
/** /**
* Write output image data to a file. * Write output image data to a file.
* *
* If an explicit output format is not selected, it will be inferred from the extension, * If an explicit output format is not selected, it will be inferred from the extension,
* with JPEG, PNG, WebP, AVIF, TIFF, DZI, and libvips' V format supported. * with JPEG, PNG, WebP, AVIF, TIFF, GIF, DZI, and libvips' V format supported.
* Note that raw pixel data is only supported for buffer output. * Note that raw pixel data is only supported for buffer output.
* *
* By default all metadata will be removed, which includes EXIF-based orientation. * By default all metadata will be removed, which includes EXIF-based orientation.
@ -63,8 +64,6 @@ function toFile (fileOut, callback) {
err = new Error('Missing output file path'); err = new Error('Missing output file path');
} else if (is.string(this.options.input.file) && path.resolve(this.options.input.file) === path.resolve(fileOut)) { } else if (is.string(this.options.input.file) && path.resolve(this.options.input.file) === path.resolve(fileOut)) {
err = new Error('Cannot use same file for input and output'); err = new Error('Cannot use same file for input and output');
} else if (this.options.formatOut === 'input' && fileOut.toLowerCase().endsWith('.gif') && !this.constructor.format.magick.output.file) {
err = errMagickSave;
} }
if (err) { if (err) {
if (is.fn(callback)) { if (is.fn(callback)) {
@ -81,9 +80,9 @@ function toFile (fileOut, callback) {
/** /**
* Write output to a Buffer. * Write output to a Buffer.
* JPEG, PNG, WebP, AVIF, TIFF and raw pixel data output are supported. * JPEG, PNG, WebP, AVIF, TIFF, GIF and raw pixel data output are supported.
* *
* If no explicit format is set, the output format will match the input image, except GIF and SVG input which become PNG output. * If no explicit format is set, the output format will match the input image, except SVG input which becomes PNG output.
* *
* By default all metadata will be removed, which includes EXIF-based orientation. * By default all metadata will be removed, which includes EXIF-based orientation.
* See {@link withMetadata} for control over this. * See {@link withMetadata} for control over this.
@ -412,7 +411,7 @@ function png (options) {
const colours = options.colours || options.colors; const colours = options.colours || options.colors;
if (is.defined(colours)) { if (is.defined(colours)) {
if (is.integer(colours) && is.inRange(colours, 2, 256)) { if (is.integer(colours) && is.inRange(colours, 2, 256)) {
this.options.pngBitdepth = 1 << 31 - Math.clz32(Math.ceil(Math.log2(colours))); this.options.pngBitdepth = bitdepthFromColourCount(colours);
} else { } else {
throw is.invalidParameterError('colours', 'integer between 2 and 256', colours); throw is.invalidParameterError('colours', 'integer between 2 and 256', colours);
} }
@ -495,13 +494,40 @@ function webp (options) {
} }
/** /**
* Use these GIF options for output image. * Use these GIF options for the output image.
* *
* Requires libvips compiled with support for ImageMagick or GraphicsMagick. * The first entry in the palette is reserved for transparency.
* The prebuilt binaries do not include this - see *
* {@link https://sharp.pixelplumbing.com/install#custom-libvips installing a custom libvips}. * @example
* // Convert PNG to GIF
* await sharp(pngBuffer)
* .gif()
* .toBuffer());
*
* @example
* // Convert animated WebP to animated GIF
* await sharp('animated.webp', { animated: true })
* .toFile('animated.gif');
*
* @example
* // Create 128x128, non-dithered thumbnail of an animated GIF
* const { pages } = await sharp('animated.gif').metadata();
* const gif = await sharp('animated.gif', { animated: true })
* .resize({
* width: 128,
* height: 128 * pages
* })
* .gif({
* pageHeight: 128,
* dither: 0
* })
* .toBuffer();
* *
* @param {Object} [options] - output options * @param {Object} [options] - output options
* @param {number} [options.colours=256] - maximum number of palette entries, including transparency, between 2 and 256
* @param {number} [options.colors=256] - alternative spelling of `options.colours`
* @param {number} [options.effort=7] - CPU effort, between 1 (fastest) and 10 (slowest)
* @param {number} [options.dither=1.0] - level of Floyd-Steinberg error diffusion, between 0 (least) and 1 (most)
* @param {number} [options.pageHeight] - page height for animated output * @param {number} [options.pageHeight] - page height for animated output
* @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation * @param {number} [options.loop=0] - number of animation iterations, use 0 for infinite animation
* @param {number[]} [options.delay] - list of delays between animation frames (in milliseconds) * @param {number[]} [options.delay] - list of delays between animation frames (in milliseconds)
@ -509,10 +535,30 @@ function webp (options) {
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid options * @throws {Error} Invalid options
*/ */
/* istanbul ignore next */
function gif (options) { function gif (options) {
if (!this.constructor.format.magick.output.buffer) { if (is.object(options)) {
throw errMagickSave; const colours = options.colours || options.colors;
if (is.defined(colours)) {
if (is.integer(colours) && is.inRange(colours, 2, 256)) {
this.options.gifBitdepth = bitdepthFromColourCount(colours);
} else {
throw is.invalidParameterError('colours', 'integer between 2 and 256', colours);
}
}
if (is.defined(options.effort)) {
if (is.number(options.effort) && is.inRange(options.effort, 1, 10)) {
this.options.gifEffort = options.effort;
} else {
throw is.invalidParameterError('effort', 'integer between 1 and 10', options.effort);
}
}
if (is.defined(options.dither)) {
if (is.number(options.dither) && is.inRange(options.dither, 0, 1)) {
this.options.gifDither = options.dither;
} else {
throw is.invalidParameterError('dither', 'number between 0.0 and 1.0', options.dither);
}
}
} }
trySetAnimationOptions(options, this.options); trySetAnimationOptions(options, this.options);
return this._updateFormatOut('gif', options); return this._updateFormatOut('gif', options);

View File

@ -1,6 +1,6 @@
{ {
"name": "sharp", "name": "sharp",
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images", "description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, GIF, AVIF and TIFF images",
"version": "0.29.3", "version": "0.29.3",
"author": "Lovell Fuller <npm@lovell.info>", "author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://github.com/lovell/sharp", "homepage": "https://github.com/lovell/sharp",

View File

@ -762,9 +762,6 @@ class PipelineWorker : public Napi::AsyncWorker {
baton->width = image.width(); baton->width = image.width();
baton->height = image.height(); baton->height = image.height();
bool const supportsGifOutput = vips_type_find("VipsOperation", "magicksave") != 0 &&
vips_type_find("VipsOperation", "magicksave_buffer") != 0;
image = sharp::SetAnimationProperties( image = sharp::SetAnimationProperties(
image, image,
baton->pageHeight, baton->pageHeight,
@ -817,8 +814,7 @@ class PipelineWorker : public Napi::AsyncWorker {
vips_area_unref(area); vips_area_unref(area);
baton->formatOut = "jp2"; baton->formatOut = "jp2";
} else if (baton->formatOut == "png" || (baton->formatOut == "input" && } else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
(inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) || (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
inputImageType == sharp::ImageType::SVG))) {
// Write PNG to buffer // Write PNG to buffer
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG); sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.pngsave_buffer(VImage::option()
@ -853,14 +849,14 @@ class PipelineWorker : public Napi::AsyncWorker {
vips_area_unref(area); vips_area_unref(area);
baton->formatOut = "webp"; baton->formatOut = "webp";
} else if (baton->formatOut == "gif" || } else if (baton->formatOut == "gif" ||
(baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) { (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF)) {
// Write GIF to buffer // Write GIF to buffer
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF); sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
VipsArea *area = reinterpret_cast<VipsArea*>(image.magicksave_buffer(VImage::option() VipsArea *area = reinterpret_cast<VipsArea*>(image.gifsave_buffer(VImage::option()
->set("strip", !baton->withMetadata) ->set("strip", !baton->withMetadata)
->set("optimize_gif_frames", TRUE) ->set("bitdepth", baton->gifBitdepth)
->set("optimize_gif_transparency", TRUE) ->set("effort", baton->gifEffort)
->set("format", "gif"))); ->set("dither", baton->gifDither)));
baton->bufferOut = static_cast<char*>(area->data); baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length; baton->bufferOutLength = area->length;
area->free_fn = nullptr; area->free_fn = nullptr;
@ -987,8 +983,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("tile_width", baton->jp2TileWidth)); ->set("tile_width", baton->jp2TileWidth));
baton->formatOut = "jp2"; baton->formatOut = "jp2";
} else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput && } else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
(inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) || (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
inputImageType == sharp::ImageType::SVG))) {
// Write PNG to file // Write PNG to file
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG); sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
@ -1015,14 +1010,14 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("alpha_q", baton->webpAlphaQuality)); ->set("alpha_q", baton->webpAlphaQuality));
baton->formatOut = "webp"; baton->formatOut = "webp";
} else if (baton->formatOut == "gif" || (mightMatchInput && isGif) || } else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
(willMatchInput && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) { (willMatchInput && inputImageType == sharp::ImageType::GIF)) {
// Write GIF to file // Write GIF to file
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF); sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
image.magicksave(const_cast<char*>(baton->fileOut.data()), VImage::option() image.gifsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata) ->set("strip", !baton->withMetadata)
->set("optimize_gif_frames", TRUE) ->set("bitdepth", baton->gifBitdepth)
->set("optimize_gif_transparency", TRUE) ->set("effort", baton->gifEffort)
->set("format", "gif")); ->set("dither", baton->gifDither));
baton->formatOut = "gif"; baton->formatOut = "gif";
} else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) || } else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
(willMatchInput && inputImageType == sharp::ImageType::TIFF)) { (willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
@ -1488,6 +1483,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless"); baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample"); baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
baton->webpReductionEffort = sharp::AttrAsUint32(options, "webpReductionEffort"); baton->webpReductionEffort = sharp::AttrAsUint32(options, "webpReductionEffort");
baton->gifBitdepth = sharp::AttrAsUint32(options, "gifBitdepth");
baton->gifEffort = sharp::AttrAsUint32(options, "gifEffort");
baton->gifDither = sharp::AttrAsDouble(options, "gifDither");
baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality"); baton->tiffQuality = sharp::AttrAsUint32(options, "tiffQuality");
baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid"); baton->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth"); baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");

View File

@ -160,6 +160,9 @@ struct PipelineBaton {
bool webpLossless; bool webpLossless;
bool webpSmartSubsample; bool webpSmartSubsample;
int webpReductionEffort; int webpReductionEffort;
int gifBitdepth;
int gifEffort;
double gifDither;
int tiffQuality; int tiffQuality;
VipsForeignTiffCompression tiffCompression; VipsForeignTiffCompression tiffCompression;
VipsForeignTiffPredictor tiffPredictor; VipsForeignTiffPredictor tiffPredictor;

View File

@ -8,7 +8,7 @@ fi
curl -s -o ./test/leak/libvips.supp https://raw.githubusercontent.com/libvips/libvips/master/suppressions/valgrind.supp curl -s -o ./test/leak/libvips.supp https://raw.githubusercontent.com/libvips/libvips/master/suppressions/valgrind.supp
for test in ./test/unit/*.js; do for test in ./test/unit/*.js; do
G_SLICE=always-malloc G_DEBUG=gc-friendly valgrind \ G_SLICE=always-malloc G_DEBUG=gc-friendly VIPS_LEAK=1 valgrind \
--suppressions=test/leak/libvips.supp \ --suppressions=test/leak/libvips.supp \
--suppressions=test/leak/sharp.supp \ --suppressions=test/leak/sharp.supp \
--gen-suppressions=yes \ --gen-suppressions=yes \

View File

@ -225,14 +225,10 @@
fun:FcInitLoadConfigAndFonts fun:FcInitLoadConfigAndFonts
} }
{ {
leak_fontconfig_doContent leak_fontconfig_XML_ParseBuffer
Memcheck:Leak Memcheck:Leak
match-leak-kinds: definite match-leak-kinds: definite
fun:malloc
... ...
fun:doContent
fun:doProlog
fun:prologInitProcessor
fun:XML_ParseBuffer fun:XML_ParseBuffer
obj:*/libfontconfig.so.* obj:*/libfontconfig.so.*
} }

View File

@ -42,7 +42,7 @@ describe('GIF input', () => {
.then(({ data, info }) => { .then(({ data, info }) => {
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size); assert.strictEqual(data.length, info.size);
assert.strictEqual(sharp.format.magick.input.buffer ? 'gif' : 'png', info.format); assert.strictEqual('gif', info.format);
assert.strictEqual(80, info.width); assert.strictEqual(80, info.width);
assert.strictEqual(80, info.height); assert.strictEqual(80, info.height);
assert.strictEqual(4, info.channels); assert.strictEqual(4, info.channels);
@ -55,28 +55,30 @@ describe('GIF input', () => {
.then(({ data, info }) => { .then(({ data, info }) => {
assert.strictEqual(true, data.length > 0); assert.strictEqual(true, data.length > 0);
assert.strictEqual(data.length, info.size); assert.strictEqual(data.length, info.size);
assert.strictEqual(sharp.format.magick.input.buffer ? 'gif' : 'png', info.format); assert.strictEqual('gif', info.format);
assert.strictEqual(80, info.width); assert.strictEqual(80, info.width);
assert.strictEqual(2400, info.height); assert.strictEqual(2400, info.height);
assert.strictEqual(4, info.channels); assert.strictEqual(4, info.channels);
}) })
); );
if (!sharp.format.magick.output.buffer) { it('GIF with reduced colours, no dither, low effort reduces file size', async () => {
it('GIF buffer output should fail due to missing ImageMagick', () => { const original = await sharp(fixtures.inputJpg)
assert.throws( .resize(120, 80)
() => sharp().gif(), .gif()
/GIF output requires libvips with support for ImageMagick/ .toBuffer();
);
});
it('GIF file output should fail due to missing ImageMagick', () => { const reduced = await sharp(fixtures.inputJpg)
assert.rejects( .resize(120, 80)
async () => await sharp().toFile('test.gif'), .gif({
/GIF output requires libvips with support for ImageMagick/ colours: 128,
); dither: 0,
}); effort: 1
} })
.toBuffer();
assert.strictEqual(true, reduced.length < original.length);
});
it('invalid pageHeight throws', () => { it('invalid pageHeight throws', () => {
assert.throws(() => { assert.throws(() => {
@ -88,7 +90,6 @@ describe('GIF input', () => {
assert.throws(() => { assert.throws(() => {
sharp().gif({ loop: -1 }); sharp().gif({ loop: -1 });
}); });
assert.throws(() => { assert.throws(() => {
sharp().gif({ loop: 65536 }); sharp().gif({ loop: 65536 });
}); });
@ -98,41 +99,59 @@ describe('GIF input', () => {
assert.throws(() => { assert.throws(() => {
sharp().gif({ delay: [-1] }); sharp().gif({ delay: [-1] });
}); });
assert.throws(() => { assert.throws(() => {
sharp().gif({ delay: [65536] }); sharp().gif({ delay: [65536] });
}); });
}); });
it('invalid colour throws', () => {
assert.throws(() => {
sharp().gif({ colours: 1 });
});
assert.throws(() => {
sharp().gif({ colours: 'fail' });
});
});
it('invalid effort throws', () => {
assert.throws(() => {
sharp().gif({ effort: 0 });
});
assert.throws(() => {
sharp().gif({ effort: 'fail' });
});
});
it('invalid dither throws', () => {
assert.throws(() => {
sharp().gif({ dither: 1.1 });
});
assert.throws(() => {
sharp().gif({ effort: 'fail' });
});
});
it('should work with streams when only animated is set', function (done) { it('should work with streams when only animated is set', function (done) {
if (sharp.format.magick.output.buffer) { fs.createReadStream(fixtures.inputGifAnimated)
fs.createReadStream(fixtures.inputGifAnimated) .pipe(sharp({ animated: true }))
.pipe(sharp({ animated: true })) .gif()
.gif() .toBuffer(function (err, data, info) {
.toBuffer(function (err, data, info) { if (err) throw err;
if (err) throw err; assert.strictEqual(true, data.length > 0);
assert.strictEqual(true, data.length > 0); assert.strictEqual('gif', info.format);
assert.strictEqual('gif', info.format); fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
fixtures.assertSimilar(fixtures.inputGifAnimated, data, done); });
});
} else {
done();
}
}); });
it('should work with streams when only pages is set', function (done) { it('should work with streams when only pages is set', function (done) {
if (sharp.format.magick.output.buffer) { fs.createReadStream(fixtures.inputGifAnimated)
fs.createReadStream(fixtures.inputGifAnimated) .pipe(sharp({ pages: -1 }))
.pipe(sharp({ pages: -1 })) .gif()
.gif() .toBuffer(function (err, data, info) {
.toBuffer(function (err, data, info) { if (err) throw err;
if (err) throw err; assert.strictEqual(true, data.length > 0);
assert.strictEqual(true, data.length > 0); assert.strictEqual('gif', info.format);
assert.strictEqual('gif', info.format); fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
fixtures.assertSimilar(fixtures.inputGifAnimated, data, done); });
});
} else {
done();
}
}); });
}); });

View File

@ -561,19 +561,6 @@ describe('Input/output', function () {
}); });
}); });
it('Autoconvert GIF input to PNG output', function (done) {
sharp(fixtures.inputGif)
.resize(320, 80)
.toFile(outputZoinks, function (err, info) {
if (err) throw err;
assert.strictEqual(true, info.size > 0);
assert.strictEqual(sharp.format.magick.input.buffer ? 'gif' : 'png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
rimraf(outputZoinks, done);
});
});
it('Force JPEG format for PNG input', function (done) { it('Force JPEG format for PNG input', function (done) {
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.resize(320, 80) .resize(320, 80)