mirror of
https://github.com/lovell/sharp.git
synced 2025-12-19 07:15:08 +01:00
Add support for animated WebP and GIF (via magick) (#2012)
This commit is contained in:
@@ -42,6 +42,9 @@ namespace sharp {
|
||||
int32_t AttrAsInt32(Napi::Object obj, std::string attr) {
|
||||
return obj.Get(attr).As<Napi::Number>().Int32Value();
|
||||
}
|
||||
int32_t AttrAsInt32(Napi::Object obj, unsigned int const attr) {
|
||||
return obj.Get(attr).As<Napi::Number>().Int32Value();
|
||||
}
|
||||
double AttrAsDouble(Napi::Object obj, std::string attr) {
|
||||
return obj.Get(attr).As<Napi::Number>().DoubleValue();
|
||||
}
|
||||
@@ -59,6 +62,14 @@ namespace sharp {
|
||||
}
|
||||
return rgba;
|
||||
}
|
||||
std::vector<int32_t> AttrAsInt32Vector(Napi::Object obj, std::string attr) {
|
||||
Napi::Array array = obj.Get(attr).As<Napi::Array>();
|
||||
std::vector<int32_t> vector(array.Length());
|
||||
for (unsigned int i = 0; i < array.Length(); i++) {
|
||||
vector[i] = AttrAsInt32(array, i);
|
||||
}
|
||||
return vector;
|
||||
}
|
||||
|
||||
// Create an InputDescriptor instance from a Napi::Object describing an input image
|
||||
InputDescriptor* CreateInputDescriptor(Napi::Object input) {
|
||||
@@ -126,6 +137,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");
|
||||
}
|
||||
@@ -239,6 +253,7 @@ namespace sharp {
|
||||
*/
|
||||
bool ImageTypeSupportsPage(ImageType imageType) {
|
||||
return
|
||||
imageType == ImageType::WEBP ||
|
||||
imageType == ImageType::MAGICK ||
|
||||
imageType == ImageType::GIF ||
|
||||
imageType == ImageType::TIFF ||
|
||||
@@ -408,6 +423,38 @@ namespace sharp {
|
||||
return copy;
|
||||
}
|
||||
|
||||
/*
|
||||
Set animation properties if necessary.
|
||||
Non-provided properties will be loaded from image.
|
||||
*/
|
||||
VImage SetAnimationProperties(VImage image, int pageHeight, std::vector<int> delay, int loop) {
|
||||
bool hasDelay = delay.size() != 1 || delay.front() != -1;
|
||||
|
||||
if (pageHeight == 0 && image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) {
|
||||
pageHeight = image.get_int(VIPS_META_PAGE_HEIGHT);
|
||||
}
|
||||
|
||||
if (!hasDelay && image.get_typeof("delay") == VIPS_TYPE_ARRAY_INT) {
|
||||
delay = image.get_array_int("delay");
|
||||
hasDelay = true;
|
||||
}
|
||||
|
||||
if (loop == -1 && image.get_typeof("loop") == G_TYPE_INT) {
|
||||
loop = image.get_int("loop");
|
||||
}
|
||||
|
||||
if (pageHeight == 0) return image;
|
||||
|
||||
// It is necessary to create the copy as otherwise, pageHeight will be ignored!
|
||||
VImage copy = image.copy();
|
||||
|
||||
copy.set(VIPS_META_PAGE_HEIGHT, pageHeight);
|
||||
if (hasDelay) copy.set("delay", delay);
|
||||
if (loop != -1) copy.set("loop", loop);
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/*
|
||||
Does this image have a non-default density?
|
||||
*/
|
||||
@@ -446,6 +493,11 @@ 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) {
|
||||
const int height = image.get_typeof("pageHeight") == G_TYPE_INT ? image.get_int("pageHeight") : image.height();
|
||||
if (image.width() > 65535 || height > 65535) {
|
||||
throw vips::VError("Processed image is too large for the GIF format");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,10 +88,12 @@ namespace sharp {
|
||||
std::string AttrAsStr(Napi::Object obj, std::string attr);
|
||||
uint32_t AttrAsUint32(Napi::Object obj, std::string attr);
|
||||
int32_t AttrAsInt32(Napi::Object obj, std::string attr);
|
||||
int32_t AttrAsInt32(Napi::Object obj, unsigned int const attr);
|
||||
double AttrAsDouble(Napi::Object obj, std::string attr);
|
||||
double AttrAsDouble(Napi::Object obj, unsigned int const attr);
|
||||
bool AttrAsBool(Napi::Object obj, std::string attr);
|
||||
std::vector<double> AttrAsRgba(Napi::Object obj, std::string attr);
|
||||
std::vector<int32_t> AttrAsInt32Vector(Napi::Object obj, std::string attr);
|
||||
|
||||
// Create an InputDescriptor instance from a Napi::Object describing an input image
|
||||
InputDescriptor* CreateInputDescriptor(Napi::Object input);
|
||||
@@ -125,6 +127,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 IsHeic(std::string const &str);
|
||||
bool IsHeif(std::string const &str);
|
||||
@@ -184,6 +187,12 @@ namespace sharp {
|
||||
*/
|
||||
VImage RemoveExifOrientation(VImage image);
|
||||
|
||||
/*
|
||||
Set animation properties if necessary.
|
||||
Non-provided properties will be loaded from image.
|
||||
*/
|
||||
VImage SetAnimationProperties(VImage image, int pageHeight, std::vector<int> delay, int loop);
|
||||
|
||||
/*
|
||||
Does this image have a non-default density?
|
||||
*/
|
||||
|
||||
@@ -693,6 +693,16 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
baton->channels = image.bands();
|
||||
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,
|
||||
baton->delay,
|
||||
baton->loop);
|
||||
|
||||
// Output
|
||||
if (baton->fileOut.empty()) {
|
||||
// Buffer output
|
||||
@@ -722,8 +732,8 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
baton->channels = std::min(baton->channels, 3);
|
||||
}
|
||||
} else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
|
||||
(inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::GIF ||
|
||||
inputImageType == sharp::ImageType::SVG))) {
|
||||
(inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
|
||||
inputImageType == sharp::ImageType::SVG))) {
|
||||
// Write PNG to buffer
|
||||
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
|
||||
VipsArea *area = VIPS_AREA(image.pngsave_buffer(VImage::option()
|
||||
@@ -757,6 +767,18 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
area->free_fn = nullptr;
|
||||
vips_area_unref(area);
|
||||
baton->formatOut = "webp";
|
||||
} else if (baton->formatOut == "gif" ||
|
||||
(baton->formatOut == "input" && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
|
||||
// Write GIF to buffer
|
||||
sharp::AssertImageTypeDimensions(image, sharp::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 == sharp::ImageType::TIFF)) {
|
||||
// Write TIFF to buffer
|
||||
@@ -832,13 +854,16 @@ class PipelineWorker : public Napi::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 isHeif = sharp::IsHeif(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 == sharp::ImageType::JPEG)) {
|
||||
// Write JPEG to file
|
||||
@@ -858,8 +883,8 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
baton->formatOut = "jpeg";
|
||||
baton->channels = std::min(baton->channels, 3);
|
||||
} else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
|
||||
(inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::GIF ||
|
||||
inputImageType == sharp::ImageType::SVG))) {
|
||||
(inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
|
||||
inputImageType == sharp::ImageType::SVG))) {
|
||||
// Write PNG to file
|
||||
sharp::AssertImageTypeDimensions(image, sharp::ImageType::PNG);
|
||||
image.pngsave(const_cast<char*>(baton->fileOut.data()), VImage::option()
|
||||
@@ -885,6 +910,14 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
->set("reduction_effort", baton->webpReductionEffort)
|
||||
->set("alpha_q", baton->webpAlphaQuality));
|
||||
baton->formatOut = "webp";
|
||||
} else if (baton->formatOut == "gif" || (mightMatchInput && isGif) ||
|
||||
(willMatchInput && inputImageType == sharp::ImageType::GIF && supportsGifOutput)) {
|
||||
// Write GIF to file
|
||||
sharp::AssertImageTypeDimensions(image, sharp::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 == sharp::ImageType::TIFF)) {
|
||||
// Write TIFF to file
|
||||
@@ -1328,6 +1361,18 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
||||
baton->heifCompression = static_cast<VipsForeignHeifCompression>(
|
||||
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
|
||||
sharp::AttrAsStr(options, "heifCompression").data()));
|
||||
|
||||
// Animated output
|
||||
if (sharp::HasAttr(options, "pageHeight")) {
|
||||
baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");
|
||||
}
|
||||
if (sharp::HasAttr(options, "loop")) {
|
||||
baton->loop = sharp::AttrAsUint32(options, "loop");
|
||||
}
|
||||
if (sharp::HasAttr(options, "delay")) {
|
||||
baton->delay = sharp::AttrAsInt32Vector(options, "delay");
|
||||
}
|
||||
|
||||
// Tile output
|
||||
baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
|
||||
baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");
|
||||
|
||||
@@ -167,6 +167,9 @@ struct PipelineBaton {
|
||||
bool removeAlpha;
|
||||
bool ensureAlpha;
|
||||
VipsInterpretation colourspace;
|
||||
int pageHeight;
|
||||
std::vector<int> delay;
|
||||
int loop;
|
||||
int tileSize;
|
||||
int tileOverlap;
|
||||
VipsForeignDzContainer tileContainer;
|
||||
@@ -273,6 +276,9 @@ struct PipelineBaton {
|
||||
removeAlpha(false),
|
||||
ensureAlpha(false),
|
||||
colourspace(VIPS_INTERPRETATION_LAST),
|
||||
pageHeight(0),
|
||||
delay{-1},
|
||||
loop(-1),
|
||||
tileSize(256),
|
||||
tileOverlap(0),
|
||||
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
|
||||
|
||||
Reference in New Issue
Block a user