mirror of
https://github.com/lovell/sharp.git
synced 2025-07-09 10:30:15 +02:00
Add support for animated WebP and GIF (via magick) (#2012)
This commit is contained in:
parent
45653ca2e7
commit
cb1baede87
@ -11,7 +11,8 @@ const formats = new Map([
|
|||||||
['png', 'png'],
|
['png', 'png'],
|
||||||
['raw', 'raw'],
|
['raw', 'raw'],
|
||||||
['tiff', 'tiff'],
|
['tiff', 'tiff'],
|
||||||
['webp', 'webp']
|
['webp', 'webp'],
|
||||||
|
['gif', 'gif']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -340,6 +341,9 @@ function png (options) {
|
|||||||
* @param {boolean} [options.nearLossless=false] - use near_lossless compression mode
|
* @param {boolean} [options.nearLossless=false] - use near_lossless compression mode
|
||||||
* @param {boolean} [options.smartSubsample=false] - use high quality chroma subsampling
|
* @param {boolean} [options.smartSubsample=false] - use high quality chroma subsampling
|
||||||
* @param {number} [options.reductionEffort=4] - level of CPU effort to reduce file size, integer 0-6
|
* @param {number} [options.reductionEffort=4] - level of CPU effort to reduce file size, integer 0-6
|
||||||
|
* @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.delay] - list of delays between animation frames (in milliseconds)
|
||||||
* @param {boolean} [options.force=true] - force WebP output, otherwise attempt to use input format
|
* @param {boolean} [options.force=true] - force WebP output, otherwise attempt to use input format
|
||||||
* @returns {Sharp}
|
* @returns {Sharp}
|
||||||
* @throws {Error} Invalid options
|
* @throws {Error} Invalid options
|
||||||
@ -375,9 +379,66 @@ function webp (options) {
|
|||||||
throw is.invalidParameterError('reductionEffort', 'integer between 0 and 6', options.reductionEffort);
|
throw is.invalidParameterError('reductionEffort', 'integer between 0 and 6', options.reductionEffort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trySetAnimationOptions(options, this.options);
|
||||||
return this._updateFormatOut('webp', options);
|
return this._updateFormatOut('webp', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use these GIF options for output image.
|
||||||
|
*
|
||||||
|
* Requires a custom, globally-installed libvips compiled with support for imageMagick.
|
||||||
|
*
|
||||||
|
* @param {Object} [options] - output options
|
||||||
|
* @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.delay] - list of delays between animation frames (in milliseconds)
|
||||||
|
* @param {boolean} [options.force=true] - force GIF output, otherwise attempt to use input format
|
||||||
|
* @returns {Sharp}
|
||||||
|
* @throws {Error} Invalid options
|
||||||
|
*/
|
||||||
|
function gif (options) {
|
||||||
|
trySetAnimationOptions(options, this.options);
|
||||||
|
return this._updateFormatOut('gif', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set animation options if available.
|
||||||
|
*
|
||||||
|
* @param {Object} [source] - output options
|
||||||
|
* @param {number} [source.pageHeight] - page height for animated output
|
||||||
|
* @param {number} [source.loop=0] - number of animation iterations, use 0 for infinite animation
|
||||||
|
* @param {number[]} [source.delay] - list of delays between animation frames (in milliseconds)
|
||||||
|
* @param {Object} [target] - target object for valid options
|
||||||
|
* @throws {Error} Invalid options
|
||||||
|
*/
|
||||||
|
function trySetAnimationOptions (source, target) {
|
||||||
|
if (is.object(source) && is.defined(source.pageHeight)) {
|
||||||
|
if (is.integer(source.pageHeight) && source.pageHeight > 0) {
|
||||||
|
target.pageHeight = source.pageHeight;
|
||||||
|
} else {
|
||||||
|
throw is.invalidParameterError('pageHeight', 'integer larger than 0', source.pageHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is.object(source) && is.defined(source.loop)) {
|
||||||
|
if (is.integer(source.loop) && is.inRange(source.loop, 0, 65535)) {
|
||||||
|
target.loop = source.loop;
|
||||||
|
} else {
|
||||||
|
throw is.invalidParameterError('loop', 'integer between 0 and 65535', source.loop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is.object(source) && is.defined(source.delay)) {
|
||||||
|
if (
|
||||||
|
Array.isArray(source.delay) &&
|
||||||
|
source.delay.every(is.integer) &&
|
||||||
|
source.delay.every(v => is.inRange(v, 0, 65535))) {
|
||||||
|
target.delay = source.delay;
|
||||||
|
} else {
|
||||||
|
throw is.invalidParameterError('delay', 'array of integers between 0 and 65535', source.delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use these TIFF options for output image.
|
* Use these TIFF options for output image.
|
||||||
*
|
*
|
||||||
@ -808,6 +869,7 @@ module.exports = function (Sharp) {
|
|||||||
webp,
|
webp,
|
||||||
tiff,
|
tiff,
|
||||||
heif,
|
heif,
|
||||||
|
gif,
|
||||||
raw,
|
raw,
|
||||||
tile,
|
tile,
|
||||||
// Private
|
// Private
|
||||||
|
@ -67,7 +67,8 @@
|
|||||||
"Brendan Kennedy <brenwken@gmail.com>",
|
"Brendan Kennedy <brenwken@gmail.com>",
|
||||||
"Brychan Bennett-Odlum <git@brychan.io>",
|
"Brychan Bennett-Odlum <git@brychan.io>",
|
||||||
"Edward Silverton <e.silverton@gmail.com>",
|
"Edward Silverton <e.silverton@gmail.com>",
|
||||||
"Roman Malieiev <aromaleev@gmail.com>"
|
"Roman Malieiev <aromaleev@gmail.com>",
|
||||||
|
"Tomas Szabo <tomas.szabo@deftomat.com>"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",
|
"install": "(node install/libvips && node install/dll-copy && prebuild-install) || (node-gyp rebuild && node install/dll-copy)",
|
||||||
|
@ -42,6 +42,9 @@ namespace sharp {
|
|||||||
int32_t AttrAsInt32(Napi::Object obj, std::string attr) {
|
int32_t AttrAsInt32(Napi::Object obj, std::string attr) {
|
||||||
return obj.Get(attr).As<Napi::Number>().Int32Value();
|
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) {
|
double AttrAsDouble(Napi::Object obj, std::string attr) {
|
||||||
return obj.Get(attr).As<Napi::Number>().DoubleValue();
|
return obj.Get(attr).As<Napi::Number>().DoubleValue();
|
||||||
}
|
}
|
||||||
@ -59,6 +62,14 @@ namespace sharp {
|
|||||||
}
|
}
|
||||||
return rgba;
|
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
|
// Create an InputDescriptor instance from a Napi::Object describing an input image
|
||||||
InputDescriptor* CreateInputDescriptor(Napi::Object input) {
|
InputDescriptor* CreateInputDescriptor(Napi::Object input) {
|
||||||
@ -126,6 +137,9 @@ namespace sharp {
|
|||||||
bool IsWebp(std::string const &str) {
|
bool IsWebp(std::string const &str) {
|
||||||
return EndsWith(str, ".webp") || EndsWith(str, ".WEBP");
|
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) {
|
bool IsTiff(std::string const &str) {
|
||||||
return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
|
return EndsWith(str, ".tif") || EndsWith(str, ".tiff") || EndsWith(str, ".TIF") || EndsWith(str, ".TIFF");
|
||||||
}
|
}
|
||||||
@ -239,6 +253,7 @@ namespace sharp {
|
|||||||
*/
|
*/
|
||||||
bool ImageTypeSupportsPage(ImageType imageType) {
|
bool ImageTypeSupportsPage(ImageType imageType) {
|
||||||
return
|
return
|
||||||
|
imageType == ImageType::WEBP ||
|
||||||
imageType == ImageType::MAGICK ||
|
imageType == ImageType::MAGICK ||
|
||||||
imageType == ImageType::GIF ||
|
imageType == ImageType::GIF ||
|
||||||
imageType == ImageType::TIFF ||
|
imageType == ImageType::TIFF ||
|
||||||
@ -408,6 +423,38 @@ namespace sharp {
|
|||||||
return copy;
|
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?
|
Does this image have a non-default density?
|
||||||
*/
|
*/
|
||||||
@ -446,6 +493,11 @@ namespace sharp {
|
|||||||
if (image.width() > 16383 || image.height() > 16383) {
|
if (image.width() > 16383 || image.height() > 16383) {
|
||||||
throw vips::VError("Processed image is too large for the WebP format");
|
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);
|
std::string AttrAsStr(Napi::Object obj, std::string attr);
|
||||||
uint32_t AttrAsUint32(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, 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, std::string attr);
|
||||||
double AttrAsDouble(Napi::Object obj, unsigned int const attr);
|
double AttrAsDouble(Napi::Object obj, unsigned int const attr);
|
||||||
bool AttrAsBool(Napi::Object obj, std::string attr);
|
bool AttrAsBool(Napi::Object obj, std::string attr);
|
||||||
std::vector<double> AttrAsRgba(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
|
// Create an InputDescriptor instance from a Napi::Object describing an input image
|
||||||
InputDescriptor* CreateInputDescriptor(Napi::Object input);
|
InputDescriptor* CreateInputDescriptor(Napi::Object input);
|
||||||
@ -125,6 +127,7 @@ namespace sharp {
|
|||||||
bool IsJpeg(std::string const &str);
|
bool IsJpeg(std::string const &str);
|
||||||
bool IsPng(std::string const &str);
|
bool IsPng(std::string const &str);
|
||||||
bool IsWebp(std::string const &str);
|
bool IsWebp(std::string const &str);
|
||||||
|
bool IsGif(std::string const &str);
|
||||||
bool IsTiff(std::string const &str);
|
bool IsTiff(std::string const &str);
|
||||||
bool IsHeic(std::string const &str);
|
bool IsHeic(std::string const &str);
|
||||||
bool IsHeif(std::string const &str);
|
bool IsHeif(std::string const &str);
|
||||||
@ -184,6 +187,12 @@ namespace sharp {
|
|||||||
*/
|
*/
|
||||||
VImage RemoveExifOrientation(VImage image);
|
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?
|
Does this image have a non-default density?
|
||||||
*/
|
*/
|
||||||
|
@ -693,6 +693,16 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
baton->channels = image.bands();
|
baton->channels = image.bands();
|
||||||
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,
|
||||||
|
baton->pageHeight,
|
||||||
|
baton->delay,
|
||||||
|
baton->loop);
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
if (baton->fileOut.empty()) {
|
if (baton->fileOut.empty()) {
|
||||||
// Buffer output
|
// Buffer output
|
||||||
@ -722,7 +732,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
baton->channels = std::min(baton->channels, 3);
|
baton->channels = std::min(baton->channels, 3);
|
||||||
}
|
}
|
||||||
} else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
|
} else if (baton->formatOut == "png" || (baton->formatOut == "input" &&
|
||||||
(inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::GIF ||
|
(inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
|
||||||
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);
|
||||||
@ -757,6 +767,18 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
area->free_fn = nullptr;
|
area->free_fn = nullptr;
|
||||||
vips_area_unref(area);
|
vips_area_unref(area);
|
||||||
baton->formatOut = "webp";
|
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" ||
|
} else if (baton->formatOut == "tiff" ||
|
||||||
(baton->formatOut == "input" && inputImageType == sharp::ImageType::TIFF)) {
|
(baton->formatOut == "input" && inputImageType == sharp::ImageType::TIFF)) {
|
||||||
// Write TIFF to buffer
|
// Write TIFF to buffer
|
||||||
@ -832,13 +854,16 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
bool const isJpeg = sharp::IsJpeg(baton->fileOut);
|
bool const isJpeg = sharp::IsJpeg(baton->fileOut);
|
||||||
bool const isPng = sharp::IsPng(baton->fileOut);
|
bool const isPng = sharp::IsPng(baton->fileOut);
|
||||||
bool const isWebp = sharp::IsWebp(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 isTiff = sharp::IsTiff(baton->fileOut);
|
||||||
bool const isHeif = sharp::IsHeif(baton->fileOut);
|
bool const isHeif = sharp::IsHeif(baton->fileOut);
|
||||||
bool const isDz = sharp::IsDz(baton->fileOut);
|
bool const isDz = sharp::IsDz(baton->fileOut);
|
||||||
bool const isDzZip = sharp::IsDzZip(baton->fileOut);
|
bool const isDzZip = sharp::IsDzZip(baton->fileOut);
|
||||||
bool const isV = sharp::IsV(baton->fileOut);
|
bool const isV = sharp::IsV(baton->fileOut);
|
||||||
bool const mightMatchInput = baton->formatOut == "input";
|
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) ||
|
if (baton->formatOut == "jpeg" || (mightMatchInput && isJpeg) ||
|
||||||
(willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
|
(willMatchInput && inputImageType == sharp::ImageType::JPEG)) {
|
||||||
// Write JPEG to file
|
// Write JPEG to file
|
||||||
@ -858,7 +883,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
baton->formatOut = "jpeg";
|
baton->formatOut = "jpeg";
|
||||||
baton->channels = std::min(baton->channels, 3);
|
baton->channels = std::min(baton->channels, 3);
|
||||||
} else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
|
} else if (baton->formatOut == "png" || (mightMatchInput && isPng) || (willMatchInput &&
|
||||||
(inputImageType == sharp::ImageType::PNG || inputImageType == sharp::ImageType::GIF ||
|
(inputImageType == sharp::ImageType::PNG || (inputImageType == sharp::ImageType::GIF && !supportsGifOutput) ||
|
||||||
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);
|
||||||
@ -885,6 +910,14 @@ class PipelineWorker : public Napi::AsyncWorker {
|
|||||||
->set("reduction_effort", baton->webpReductionEffort)
|
->set("reduction_effort", baton->webpReductionEffort)
|
||||||
->set("alpha_q", baton->webpAlphaQuality));
|
->set("alpha_q", baton->webpAlphaQuality));
|
||||||
baton->formatOut = "webp";
|
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) ||
|
} else if (baton->formatOut == "tiff" || (mightMatchInput && isTiff) ||
|
||||||
(willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
|
(willMatchInput && inputImageType == sharp::ImageType::TIFF)) {
|
||||||
// Write TIFF to file
|
// Write TIFF to file
|
||||||
@ -1328,6 +1361,18 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
|||||||
baton->heifCompression = static_cast<VipsForeignHeifCompression>(
|
baton->heifCompression = static_cast<VipsForeignHeifCompression>(
|
||||||
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
|
vips_enum_from_nick(nullptr, VIPS_TYPE_FOREIGN_HEIF_COMPRESSION,
|
||||||
sharp::AttrAsStr(options, "heifCompression").data()));
|
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
|
// Tile output
|
||||||
baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
|
baton->tileSize = sharp::AttrAsUint32(options, "tileSize");
|
||||||
baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");
|
baton->tileOverlap = sharp::AttrAsUint32(options, "tileOverlap");
|
||||||
|
@ -167,6 +167,9 @@ struct PipelineBaton {
|
|||||||
bool removeAlpha;
|
bool removeAlpha;
|
||||||
bool ensureAlpha;
|
bool ensureAlpha;
|
||||||
VipsInterpretation colourspace;
|
VipsInterpretation colourspace;
|
||||||
|
int pageHeight;
|
||||||
|
std::vector<int> delay;
|
||||||
|
int loop;
|
||||||
int tileSize;
|
int tileSize;
|
||||||
int tileOverlap;
|
int tileOverlap;
|
||||||
VipsForeignDzContainer tileContainer;
|
VipsForeignDzContainer tileContainer;
|
||||||
@ -273,6 +276,9 @@ struct PipelineBaton {
|
|||||||
removeAlpha(false),
|
removeAlpha(false),
|
||||||
ensureAlpha(false),
|
ensureAlpha(false),
|
||||||
colourspace(VIPS_INTERPRETATION_LAST),
|
colourspace(VIPS_INTERPRETATION_LAST),
|
||||||
|
pageHeight(0),
|
||||||
|
delay{-1},
|
||||||
|
loop(-1),
|
||||||
tileSize(256),
|
tileSize(256),
|
||||||
tileOverlap(0),
|
tileOverlap(0),
|
||||||
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
|
tileContainer(VIPS_FOREIGN_DZ_CONTAINER_FS),
|
||||||
|
BIN
test/fixtures/animated-loop-3.webp
vendored
Normal file
BIN
test/fixtures/animated-loop-3.webp
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.2 KiB |
2
test/fixtures/index.js
vendored
2
test/fixtures/index.js
vendored
@ -93,6 +93,8 @@ module.exports = {
|
|||||||
|
|
||||||
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
|
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
|
||||||
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
|
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
|
||||||
|
inputWebPAnimated: getPath('rotating-squares.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
|
||||||
|
inputWebPAnimatedLoop3: getPath('animated-loop-3.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
|
||||||
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
|
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
|
||||||
inputTiffMultipage: getPath('G31D_MULTI.TIF'), // gm convert G31D.TIF -resize 50% G31D_2.TIF ; tiffcp G31D.TIF G31D_2.TIF G31D_MULTI.TIF
|
inputTiffMultipage: getPath('G31D_MULTI.TIF'), // gm convert G31D.TIF -resize 50% G31D_2.TIF ; tiffcp G31D.TIF G31D_2.TIF G31D_MULTI.TIF
|
||||||
inputTiffCielab: getPath('cielab-dagams.tiff'), // https://github.com/lovell/sharp/issues/646
|
inputTiffCielab: getPath('cielab-dagams.tiff'), // https://github.com/lovell/sharp/issues/646
|
||||||
|
BIN
test/fixtures/rotating-squares.webp
vendored
Normal file
BIN
test/fixtures/rotating-squares.webp
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
@ -61,4 +61,41 @@ describe('GIF input', () => {
|
|||||||
assert.strictEqual(4, info.channels);
|
assert.strictEqual(4, info.channels);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!sharp.format.magick.input.buffer) {
|
||||||
|
it('Animated GIF output should fail due to missing ImageMagick', () =>
|
||||||
|
assert.rejects(() =>
|
||||||
|
sharp(fixtures.inputGifAnimated, { pages: -1 })
|
||||||
|
.gif({ loop: 2, delay: [...Array(10).fill(100)], pageHeight: 10 })
|
||||||
|
.toBuffer(),
|
||||||
|
/VipsOperation: class "magicksave_buffer" not found/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('invalid pageHeight throws', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().gif({ pageHeight: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalid loop throws', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().gif({ loop: -1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().gif({ loop: 65536 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalid delay throws', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().gif({ delay: [-1] });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().gif({ delay: [65536] });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -192,6 +192,54 @@ describe('Image metadata', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Animated WebP', () =>
|
||||||
|
sharp(fixtures.inputWebPAnimated)
|
||||||
|
.metadata()
|
||||||
|
.then(({
|
||||||
|
format, width, height, space, channels, depth,
|
||||||
|
isProgressive, pages, pageHeight, loop, delay,
|
||||||
|
hasProfile, hasAlpha
|
||||||
|
}) => {
|
||||||
|
assert.strictEqual(format, 'webp');
|
||||||
|
assert.strictEqual(width, 80);
|
||||||
|
assert.strictEqual(height, 80);
|
||||||
|
assert.strictEqual(space, 'srgb');
|
||||||
|
assert.strictEqual(channels, 4);
|
||||||
|
assert.strictEqual(depth, 'uchar');
|
||||||
|
assert.strictEqual(isProgressive, false);
|
||||||
|
assert.strictEqual(pages, 9);
|
||||||
|
assert.strictEqual(pageHeight, 80);
|
||||||
|
assert.strictEqual(loop, 0);
|
||||||
|
assert.deepStrictEqual(delay, [120, 120, 90, 120, 120, 90, 120, 90, 30]);
|
||||||
|
assert.strictEqual(hasProfile, false);
|
||||||
|
assert.strictEqual(hasAlpha, true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('Animated WebP with limited looping', () =>
|
||||||
|
sharp(fixtures.inputWebPAnimatedLoop3)
|
||||||
|
.metadata()
|
||||||
|
.then(({
|
||||||
|
format, width, height, space, channels, depth,
|
||||||
|
isProgressive, pages, pageHeight, loop, delay,
|
||||||
|
hasProfile, hasAlpha
|
||||||
|
}) => {
|
||||||
|
assert.strictEqual(format, 'webp');
|
||||||
|
assert.strictEqual(width, 370);
|
||||||
|
assert.strictEqual(height, 285);
|
||||||
|
assert.strictEqual(space, 'srgb');
|
||||||
|
assert.strictEqual(channels, 4);
|
||||||
|
assert.strictEqual(depth, 'uchar');
|
||||||
|
assert.strictEqual(isProgressive, false);
|
||||||
|
assert.strictEqual(pages, 10);
|
||||||
|
assert.strictEqual(pageHeight, 285);
|
||||||
|
assert.strictEqual(loop, 3);
|
||||||
|
assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]);
|
||||||
|
assert.strictEqual(hasProfile, false);
|
||||||
|
assert.strictEqual(hasAlpha, true);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
it('GIF via giflib', function (done) {
|
it('GIF via giflib', function (done) {
|
||||||
sharp(fixtures.inputGif).metadata(function (err, metadata) {
|
sharp(fixtures.inputGif).metadata(function (err, metadata) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
|
@ -125,4 +125,63 @@ describe('WebP', function () {
|
|||||||
sharp().webp({ reductionEffort: -1 });
|
sharp().webp({ reductionEffort: -1 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('invalid pageHeight throws', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().webp({ pageHeight: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalid loop throws', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().webp({ loop: -1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().webp({ loop: 65536 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invalid delay throws', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().webp({ delay: [-1] });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
sharp().webp({ delay: [65536] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should double the number of frames with default delay', async () => {
|
||||||
|
const original = await sharp(fixtures.inputWebPAnimated, { pages: -1 }).metadata();
|
||||||
|
const updated = await sharp(fixtures.inputWebPAnimated, { pages: -1 })
|
||||||
|
.webp({ pageHeight: original.pageHeight / 2 })
|
||||||
|
.toBuffer()
|
||||||
|
.then(data => sharp(data, { pages: -1 }).metadata());
|
||||||
|
|
||||||
|
assert.strictEqual(updated.pages, original.pages * 2);
|
||||||
|
assert.strictEqual(updated.pageHeight, original.pageHeight / 2);
|
||||||
|
assert.deepStrictEqual(updated.delay, [...original.delay, ...Array(9).fill(120)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should limit animation loop', async () => {
|
||||||
|
const updated = await sharp(fixtures.inputWebPAnimated, { pages: -1 })
|
||||||
|
.webp({ loop: 3 })
|
||||||
|
.toBuffer()
|
||||||
|
.then(data => sharp(data, { pages: -1 }).metadata());
|
||||||
|
|
||||||
|
assert.strictEqual(updated.loop, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change delay between frames', async () => {
|
||||||
|
const original = await sharp(fixtures.inputWebPAnimated, { pages: -1 }).metadata();
|
||||||
|
|
||||||
|
const expectedDelay = [...Array(original.pages).fill(40)];
|
||||||
|
const updated = await sharp(fixtures.inputWebPAnimated, { pages: -1 })
|
||||||
|
.webp({ delay: expectedDelay })
|
||||||
|
.toBuffer()
|
||||||
|
.then(data => sharp(data, { pages: -1 }).metadata());
|
||||||
|
|
||||||
|
assert.deepStrictEqual(updated.delay, expectedDelay);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user