Skip to content

Commit

Permalink
IGIF support
Browse files Browse the repository at this point in the history
  • Loading branch information
deftomat committed May 29, 2019
1 parent 159e8da commit f297c6b
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 5 deletions.
49 changes: 48 additions & 1 deletion lib/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,52 @@ function tiff (options) {
return this._updateFormatOut('tiff', options);
}

/**
* Use these GIF options for output image.
*
* Requires `imageMagick` to save encoded pipeline output into GIF image.
*
* @example
* // Convert any input to GIF output
* const data = await sharp(input)
* .gif()
* .toBuffer();
*
* @param {Object} [options]
* @param {Boolean} [options.pageHeight] - height of one page, uses pageHeight extracted from source if omitted
* @param {Number} [options.pageDelay=0] - delay between each page, in centiseconds
* @param {Boolean} [options.pageLoop=0] - number of loops before animation ends (0 for infinite animation), uses pageLoop extracted from source if omitted
* @returns {Sharp}
* @throws {Error} Invalid options
*/
function gif (options) {
if (is.object(options)) {
if (is.defined(options.pageHeight)) {
if (is.integer(options.pageHeight) && options.pageHeight > 0) {
this.options.pageHeight = options.pageHeight;
} else {
throw new Error('Invalid Value for pageHeight ' + options.pageHeight + ' Only positive numeric values allowed for options.pageHeight');
}
}
if (is.defined(options.pageDelay)) {
if (is.integer(options.pageDelay) && is.inRange(options.pageDelay, 0, 65535)) {
this.options.pageDelay = options.pageDelay;
} else {
throw new Error('Invalid pageDelay (integer, 0-65535) ' + options.pageDelay);
}
}
if (is.defined(options.pageLoop)) {
if (is.integer(options.pageLoop) && is.inRange(options.pageLoop, 0, 65535)) {
this.options.pageLoop = options.pageLoop;
} else {
throw new Error('Invalid pageLoop (integer, 0-65535) ' + options.pageLoop);
}
}
}

return this._updateFormatOut('gif', options);
}

/**
* Force output to be raw, uncompressed uint8 pixel data.
*
Expand Down Expand Up @@ -463,7 +509,7 @@ function toFormat (format, options) {
format = format.id;
}
if (format === 'jpg') format = 'jpeg';
if (!is.inArray(format, ['jpeg', 'png', 'webp', 'tiff', 'raw'])) {
if (!is.inArray(format, ['jpeg', 'png', 'webp', 'tiff', 'gif', 'raw'])) {
throw new Error('Unsupported output format ' + format);
}
return this[format](options);
Expand Down Expand Up @@ -720,6 +766,7 @@ module.exports = function (Sharp) {
png,
webp,
tiff,
gif,
raw,
toFormat,
tile,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sharp",
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP and TIFF images",
"description": "High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, GIF and TIFF images",
"version": "0.22.0",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://github.com/lovell/sharp",
Expand Down
14 changes: 14 additions & 0 deletions src/common.cc
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ namespace sharp {
bool IsWebp(std::string const &str) {
return EndsWith(str, ".webp") || EndsWith(str, ".WEBP");
}
bool IsGif(std::string const &str) {
return EndsWith(str, ".gif") || EndsWith(str, ".GIF");
}
bool IsTiff(std::string const &str) {
return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
}
Expand Down Expand Up @@ -402,6 +405,17 @@ namespace sharp {
if (image.width() > 16383 || image.height() > 16383) {
throw vips::VError("Processed image is too large for the WebP format");
}
} else if (imageType == ImageType::GIF) {
if (image.width() > 65535) {
throw vips::VError("Processed image is too large for the GIF format. Width cannot be larger than 65535 pixels.");
}
if (image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) {
if (image.get_int(VIPS_META_PAGE_HEIGHT) > 65535) {
throw vips::VError("Processed image is too large for the GIF format. Page height cannot be larger than 65535 pixels.");
}
} else if (image.height() > 65535) {
throw vips::VError("Processed image is too large for the GIF format. Height cannot be larger than 65535 pixels.");
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ namespace sharp {
bool IsJpeg(std::string const &str);
bool IsPng(std::string const &str);
bool IsWebp(std::string const &str);
bool IsGif(std::string const &str);
bool IsTiff(std::string const &str);
bool IsDz(std::string const &str);
bool IsDzZip(std::string const &str);
Expand Down
12 changes: 12 additions & 0 deletions src/metadata.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ class MetadataWorker : public Nan::AsyncWorker {
if (image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) {
baton->pageHeight = image.get_int(VIPS_META_PAGE_HEIGHT);
}
if (image.get_typeof("gif-delay") == G_TYPE_INT) {
baton->pageDelay = image.get_int("gif-delay");
}
if (image.get_typeof("gif-loop") == G_TYPE_INT) {
baton->pageLoop = image.get_int("gif-loop");
}
baton->hasProfile = sharp::HasProfile(image);
// Derived attributes
baton->hasAlpha = sharp::HasAlpha(image);
Expand Down Expand Up @@ -158,6 +164,12 @@ class MetadataWorker : public Nan::AsyncWorker {
if (baton->pageHeight > 0) {
Set(info, New("pageHeight").ToLocalChecked(), New<v8::Uint32>(baton->pageHeight));
}
if (baton->pageDelay > -1) {
Set(info, New("pageDelay").ToLocalChecked(), New<v8::Uint32>(baton->pageDelay));
}
if (baton->pageLoop > -1) {
Set(info, New("pageLoop").ToLocalChecked(), New<v8::Uint32>(baton->pageLoop));
}
Set(info, New("hasProfile").ToLocalChecked(), New<v8::Boolean>(baton->hasProfile));
Set(info, New("hasAlpha").ToLocalChecked(), New<v8::Boolean>(baton->hasAlpha));
if (baton->orientation > 0) {
Expand Down
4 changes: 4 additions & 0 deletions src/metadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ struct MetadataBaton {
int paletteBitDepth;
int pages;
int pageHeight;
int pageDelay;
int pageLoop;
bool hasProfile;
bool hasAlpha;
int orientation;
Expand All @@ -59,6 +61,8 @@ struct MetadataBaton {
paletteBitDepth(0),
pages(0),
pageHeight(0),
pageDelay(-1),
pageLoop(-1),
hasProfile(false),
hasAlpha(false),
orientation(0),
Expand Down
67 changes: 64 additions & 3 deletions src/pipeline.cc
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,31 @@ class PipelineWorker : public Nan::AsyncWorker {
baton->channels = image.bands();
baton->width = image.width();
baton->height = image.height();

// Animated image properties
if (baton->pageHeight == 0) {
if (image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) {
baton->pageHeight = image.get_int(VIPS_META_PAGE_HEIGHT);
} else {
baton->pageHeight = baton->height;
}
}
if (baton->pageDelay == -1) {
if (image.get_typeof("gif-delay") == G_TYPE_INT) {
baton->pageDelay = image.get_int("gif-delay");
} else {
baton->pageDelay = 0;
}
}
if (baton->pageLoop == -1) {
if (image.get_typeof("gif-loop") == G_TYPE_INT) {
baton->pageLoop = image.get_int("gif-loop");
} else {
baton->pageLoop = 0;
}
}
baton->pageLoop = baton->pageLoop + 1; // TODO: https://github.com/libvips/libvips/issues/1302

// Output
if (baton->fileOut.empty()) {
// Buffer output
Expand Down Expand Up @@ -722,7 +747,7 @@ class PipelineWorker : public Nan::AsyncWorker {
baton->channels = std::min(baton->channels, 3);
}
} else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
(inputImageType == ImageType::PNG || inputImageType == ImageType::GIF || inputImageType == ImageType::SVG))) {
(inputImageType == ImageType::PNG || inputImageType == ImageType::SVG))) {
// Write PNG to buffer
sharp::AssertImageTypeDimensions(image, ImageType::PNG);
VipsArea *area = VIPS_AREA(image.pngsave_buffer(VImage::option()
Expand Down Expand Up @@ -753,6 +778,20 @@ class PipelineWorker : public Nan::AsyncWorker {
area->free_fn = nullptr;
vips_area_unref(area);
baton->formatOut = "webp";
} else if (baton->formatOut == "gif" || (baton->formatOut == "input" && inputImageType == ImageType::GIF)) {
// Write GIF to buffer
image.set("page-height", baton->pageHeight);
image.set("gif-delay", baton->pageDelay);
image.set("gif-loop", baton->pageLoop);
sharp::AssertImageTypeDimensions(image, ImageType::GIF);
VipsArea *area = VIPS_AREA(image.magicksave_buffer(VImage::option()
->set("strip", !baton->withMetadata)
->set("format", "gif")));
baton->bufferOut = static_cast<char*>(area->data);
baton->bufferOutLength = area->length;
area->free_fn = nullptr;
vips_area_unref(area);
baton->formatOut = "gif";
} else if (baton->formatOut == "tiff" || (baton->formatOut == "input" && inputImageType == ImageType::TIFF)) {
// Write TIFF to buffer
if (baton->tiffCompression == VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) {
Expand Down Expand Up @@ -813,12 +852,13 @@ class PipelineWorker : public Nan::AsyncWorker {
bool const isJpeg = sharp::IsJpeg(baton->fileOut);
bool const isPng = sharp::IsPng(baton->fileOut);
bool const isWebp = sharp::IsWebp(baton->fileOut);
bool const isGif = sharp::IsGif(baton->fileOut);
bool const isTiff = sharp::IsTiff(baton->fileOut);
bool const isDz = sharp::IsDz(baton->fileOut);
bool const isDzZip = sharp::IsDzZip(baton->fileOut);
bool const isV = sharp::IsV(baton->fileOut);
bool const mightMatchInput = baton->formatOut == "input";
bool const willMatchInput = mightMatchInput && !(isJpeg || isPng || isWebp || isTiff || isDz || isDzZip || isV);
bool const willMatchInput = mightMatchInput && !(isJpeg || isPng || isWebp || isGif || isTiff || isDz || isDzZip || isV);
if (baton->formatOut == "jpeg" || (mightMatchInput && isJpeg) ||
(willMatchInput && inputImageType == ImageType::JPEG)) {
// Write JPEG to file
Expand All @@ -836,7 +876,7 @@ class PipelineWorker : public Nan::AsyncWorker {
baton->formatOut = "jpeg";
baton->channels = std::min(baton->channels, 3);
} else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
(inputImageType == ImageType::PNG || inputImageType == ImageType::GIF || inputImageType == ImageType::SVG))) {
(inputImageType == ImageType::PNG || inputImageType == ImageType::SVG))) {
// Write PNG to file
sharp::AssertImageTypeDimensions(image, ImageType::PNG);
image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
Expand All @@ -860,6 +900,17 @@ class PipelineWorker : public Nan::AsyncWorker {
->set("near_lossless", baton->webpNearLossless)
->set("alpha_q", baton->webpAlphaQuality));
baton->formatOut = "webp";
} else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
(willMatchInput && inputImageType == ImageType::GIF)) {
// Write GIF to file
image.set("page-height", baton->pageHeight);
image.set("gif-delay", baton->pageDelay);
image.set("gif-loop", baton->pageLoop);
sharp::AssertImageTypeDimensions(image, ImageType::GIF);
image.magicksave(const_cast<char*>(baton->fileOut.data()), VImage::option()
->set("strip", !baton->withMetadata)
->set("format", "gif"));
baton->formatOut = "gif";
} else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
(willMatchInput && inputImageType == ImageType::TIFF)) {
// Write TIFF to file
Expand Down Expand Up @@ -1320,6 +1371,16 @@ NAN_METHOD(pipeline) {
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_TIFF_PREDICTOR,
AttrAsStr(options, "tiffPredictor").data()));

// Animated output
if (HasAttr(options, "pageHeight")) {
baton->pageHeight = AttrTo<uint32_t>(options, "pageHeight");
}
if (HasAttr(options, "pageDelay")) {
baton->pageDelay = AttrTo<int32_t>(options, "pageDelay");
}
if (HasAttr(options, "pageLoop")) {
baton->pageLoop = AttrTo<int32_t>(options, "pageLoop");
}
// Tile output
baton->tileSize = AttrTo<uint32_t>(options, "tileSize");
baton->tileOverlap = AttrTo<uint32_t>(options, "tileOverlap");
Expand Down
6 changes: 6 additions & 0 deletions src/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ struct PipelineBaton {
bool removeAlpha;
bool ensureAlpha;
VipsInterpretation colourspace;
int pageHeight;
int pageDelay;
int pageLoop;
int tileSize;
int tileOverlap;
VipsForeignDzContainer tileContainer;
Expand Down Expand Up @@ -260,6 +263,9 @@ struct PipelineBaton {
removeAlpha(false),
ensureAlpha(false),
colourspace(VIPS_INTERPRETATION_LAST),
pageHeight(0),
pageDelay(-1),
pageLoop(-1),
tileSize(256),
tileOverlap(0),
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
Expand Down

5 comments on commit f297c6b

@angelogulina
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello there :)
This is a very interesting topic - is there any update on this regard?

@deftomat
Copy link
Owner Author

@deftomat deftomat commented on f297c6b Aug 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@angelogulina
Hi, sorry that is taking so long but I did a few PRs to the underlying library (libvips) and one PR (libvips/libvips#1362) is still open. However, I was able to add support for animated GIFs and also for animated WEBPs. We are using it internally, but to be able to merge it and avoid the breaking changes/deprecations, I decided to wait until we have everything we need in libvips.

@angelogulina
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deftomat,
thanks a lot for the feedback and the awesome work.
I hope my question didn't sound as making pressure - was just asking for an info.
Such great work!
Cheers!

@rokumatsumoto
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deftomat
thanks, looking forward to your PR.

@deftomat
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support for WEBP & GIF animations: lovell#2012

Please sign in to comment.