diff --git a/README.md b/README.md
index 4e1e72ab..e3842b38 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
The typical use case for this high speed Node.js module
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
quickest ImageMagick and GraphicsMagick settings
diff --git a/docs/README.md b/docs/README.md
index 5395eb44..48683164 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -4,7 +4,7 @@
The typical use case for this high speed Node.js module
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
quickest ImageMagick and GraphicsMagick settings
@@ -21,9 +21,9 @@ do not require any additional install or runtime dependencies.
### 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.
diff --git a/docs/changelog.md b/docs/changelog.md
index b2b383f5..bda9d0a1 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,8 @@ Requires libvips v8.12.0
### v0.30.0 - TBD
+* Add support for GIF output to prebuilt binaries.
+
* Reduce minimum Linux ARM64v8 glibc requirement to 2.17.
* Properly emit close events for duplex streams.
diff --git a/docs/index.html b/docs/index.html
index 022aee4f..374865c1 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -4,7 +4,7 @@
-
+
",
"homepage": "https://github.com/lovell/sharp",
diff --git a/src/pipeline.cc b/src/pipeline.cc
index edf4b334..270c7424 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -762,9 +762,6 @@ class PipelineWorker : public Napi::AsyncWorker {
baton->width = image.width();
baton->height = image.height();
- bool const supportsGifOutput = vips_type_find("VipsOperation", "magicksave") != 0 &&
- vips_type_find("VipsOperation", "magicksave_buffer") != 0;
-
image = sharp::SetAnimationProperties(
image,
baton->pageHeight,
@@ -817,8 +814,7 @@ class PipelineWorker : public Napi::AsyncWorker {
vips_area_unref(area);
baton->formatOut = "jp2";
} else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
- inputImageType == sharp::ImageType::SVG))) {
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
// Write PNG to buffer
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
VipsArea *area = reinterpret_cast(image.pngsave_buffer(VImage::option()
@@ -853,14 +849,14 @@ class PipelineWorker : public Napi::AsyncWorker {
vips_area_unref(area);
baton->formatOut = "webp";
} else if (baton->formatOut == "gif" ||
- (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
+ (baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF)) {
// Write GIF to buffer
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
- VipsArea *area = reinterpret_cast(image.magicksave_buffer(VImage::option()
+ VipsArea *area = reinterpret_cast(image.gifsave_buffer(VImage::option()
->set("strip", !baton->withMetadata)
- ->set("optimize_gif_frames", TRUE)
- ->set("optimize_gif_transparency", TRUE)
- ->set("format", "gif")));
+ ->set("bitdepth", baton->gifBitdepth)
+ ->set("effort", baton->gifEffort)
+ ->set("dither", baton->gifDither)));
baton->bufferOut = static_cast(area->data);
baton->bufferOutLength = area->length;
area->free_fn = nullptr;
@@ -987,8 +983,7 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("tile_width", baton->jp2TileWidth));
baton->formatOut = "jp2";
} else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
- (inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
- inputImageType == sharp::ImageType::SVG))) {
+ (inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::SVG))) {
// Write PNG to file
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
image.pngsave(const_cast(baton->fileOut.data()), VImage::option()
@@ -1015,14 +1010,14 @@ class PipelineWorker : public Napi::AsyncWorker {
->set("alpha_q", baton->webpAlphaQuality));
baton->formatOut = "webp";
} else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
- (willMatchInput && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
+ (willMatchInput && inputImageType == sharp::ImageType::GIF)) {
// Write GIF to file
sharp::AssertImageTypeDimensions(image, sharp::ImageType::GIF);
- image.magicksave(const_cast(baton->fileOut.data()), VImage::option()
+ image.gifsave(const_cast(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
- ->set("optimize_gif_frames", TRUE)
- ->set("optimize_gif_transparency", TRUE)
- ->set("format", "gif"));
+ ->set("bitdepth", baton->gifBitdepth)
+ ->set("effort", baton->gifEffort)
+ ->set("dither", baton->gifDither));
baton->formatOut = "gif";
} else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
(willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
@@ -1488,6 +1483,9 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
baton->webpNearLossless = sharp::AttrAsBool(options, "webpNearLossless");
baton->webpSmartSubsample = sharp::AttrAsBool(options, "webpSmartSubsample");
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->tiffPyramid = sharp::AttrAsBool(options, "tiffPyramid");
baton->tiffBitdepth = sharp::AttrAsUint32(options, "tiffBitdepth");
diff --git a/src/pipeline.h b/src/pipeline.h
index 816aa330..db1e7873 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -160,6 +160,9 @@ struct PipelineBaton {
bool webpLossless;
bool webpSmartSubsample;
int webpReductionEffort;
+ int gifBitdepth;
+ int gifEffort;
+ double gifDither;
int tiffQuality;
VipsForeignTiffCompression tiffCompression;
VipsForeignTiffPredictor tiffPredictor;
diff --git a/test/leak/leak.sh b/test/leak/leak.sh
index d417ed59..f21609e5 100755
--- a/test/leak/leak.sh
+++ b/test/leak/leak.sh
@@ -8,7 +8,7 @@ fi
curl -s -o ./test/leak/libvips.supp https://raw.githubusercontent.com/libvips/libvips/master/suppressions/valgrind.supp
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/sharp.supp \
--gen-suppressions=yes \
diff --git a/test/leak/sharp.supp b/test/leak/sharp.supp
index 5095ffb7..9e2b470e 100644
--- a/test/leak/sharp.supp
+++ b/test/leak/sharp.supp
@@ -225,14 +225,10 @@
fun:FcInitLoadConfigAndFonts
}
{
- leak_fontconfig_doContent
+ leak_fontconfig_XML_ParseBuffer
Memcheck:Leak
match-leak-kinds: definite
- fun:malloc
...
- fun:doContent
- fun:doProlog
- fun:prologInitProcessor
fun:XML_ParseBuffer
obj:*/libfontconfig.so.*
}
diff --git a/test/unit/gif.js b/test/unit/gif.js
index ff59be28..0cade87b 100644
--- a/test/unit/gif.js
+++ b/test/unit/gif.js
@@ -42,7 +42,7 @@ describe('GIF input', () => {
.then(({ data, info }) => {
assert.strictEqual(true, data.length > 0);
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.height);
assert.strictEqual(4, info.channels);
@@ -55,28 +55,30 @@ describe('GIF input', () => {
.then(({ data, info }) => {
assert.strictEqual(true, data.length > 0);
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(2400, info.height);
assert.strictEqual(4, info.channels);
})
);
- if (!sharp.format.magick.output.buffer) {
- it('GIF buffer output should fail due to missing ImageMagick', () => {
- assert.throws(
- () => sharp().gif(),
- /GIF output requires libvips with support for ImageMagick/
- );
- });
+ it('GIF with reduced colours, no dither, low effort reduces file size', async () => {
+ const original = await sharp(fixtures.inputJpg)
+ .resize(120, 80)
+ .gif()
+ .toBuffer();
- it('GIF file output should fail due to missing ImageMagick', () => {
- assert.rejects(
- async () => await sharp().toFile('test.gif'),
- /GIF output requires libvips with support for ImageMagick/
- );
- });
- }
+ const reduced = await sharp(fixtures.inputJpg)
+ .resize(120, 80)
+ .gif({
+ colours: 128,
+ dither: 0,
+ effort: 1
+ })
+ .toBuffer();
+
+ assert.strictEqual(true, reduced.length < original.length);
+ });
it('invalid pageHeight throws', () => {
assert.throws(() => {
@@ -88,7 +90,6 @@ describe('GIF input', () => {
assert.throws(() => {
sharp().gif({ loop: -1 });
});
-
assert.throws(() => {
sharp().gif({ loop: 65536 });
});
@@ -98,41 +99,59 @@ describe('GIF input', () => {
assert.throws(() => {
sharp().gif({ delay: [-1] });
});
-
assert.throws(() => {
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) {
- if (sharp.format.magick.output.buffer) {
- fs.createReadStream(fixtures.inputGifAnimated)
- .pipe(sharp({ animated: true }))
- .gif()
- .toBuffer(function (err, data, info) {
- if (err) throw err;
- assert.strictEqual(true, data.length > 0);
- assert.strictEqual('gif', info.format);
- fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
- });
- } else {
- done();
- }
+ fs.createReadStream(fixtures.inputGifAnimated)
+ .pipe(sharp({ animated: true }))
+ .gif()
+ .toBuffer(function (err, data, info) {
+ if (err) throw err;
+ assert.strictEqual(true, data.length > 0);
+ assert.strictEqual('gif', info.format);
+ fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
+ });
});
it('should work with streams when only pages is set', function (done) {
- if (sharp.format.magick.output.buffer) {
- fs.createReadStream(fixtures.inputGifAnimated)
- .pipe(sharp({ pages: -1 }))
- .gif()
- .toBuffer(function (err, data, info) {
- if (err) throw err;
- assert.strictEqual(true, data.length > 0);
- assert.strictEqual('gif', info.format);
- fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
- });
- } else {
- done();
- }
+ fs.createReadStream(fixtures.inputGifAnimated)
+ .pipe(sharp({ pages: -1 }))
+ .gif()
+ .toBuffer(function (err, data, info) {
+ if (err) throw err;
+ assert.strictEqual(true, data.length > 0);
+ assert.strictEqual('gif', info.format);
+ fixtures.assertSimilar(fixtures.inputGifAnimated, data, done);
+ });
});
});
diff --git a/test/unit/io.js b/test/unit/io.js
index ff5a8e37..628e2f89 100644
--- a/test/unit/io.js
+++ b/test/unit/io.js
@@ -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) {
sharp(fixtures.inputPng)
.resize(320, 80)