Improve multi-frame image resizing (#2789)

* Ports vips_thumbnail logic to sharp 
* Deprecates the pageHeight output option for WebP/GIF
This commit is contained in:
Kleis Auke Wolthuizen 2021-12-10 21:32:04 +01:00 committed by GitHub
parent 659cdabd8e
commit 513fb40f40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 619 additions and 334 deletions

View File

@ -458,9 +458,8 @@ 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.effort=4] - CPU effort, between 0 (fastest) and 6 (slowest) * @param {number} [options.effort=4] - CPU effort, between 0 (fastest) and 6 (slowest)
* @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|number[]} [options.delay] - delay(s) 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
@ -527,10 +526,7 @@ function webp (options) {
* width: 128, * width: 128,
* height: 128 * pages * height: 128 * pages
* }) * })
* .gif({ * .gif({ dither: 0 })
* pageHeight: 128,
* dither: 0
* })
* .toBuffer(); * .toBuffer();
* *
* @param {Object} [options] - output options * @param {Object} [options] - output options
@ -538,9 +534,8 @@ function webp (options) {
* @param {number} [options.colors=256] - alternative spelling of `options.colours` * @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.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.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.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|number[]} [options.delay] - delay(s) between animation frames (in milliseconds)
* @param {boolean} [options.force=true] - force GIF output, otherwise attempt to use input format * @param {boolean} [options.force=true] - force GIF output, otherwise attempt to use input format
* @returns {Sharp} * @returns {Sharp}
* @throws {Error} Invalid options * @throws {Error} Invalid options
@ -657,20 +652,12 @@ function jp2 (options) {
* @private * @private
* *
* @param {Object} [source] - output options * @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.loop=0] - number of animation iterations, use 0 for infinite animation
* @param {number[]} [source.delay] - list of delays between animation frames (in milliseconds) * @param {number[]} [source.delay] - list of delays between animation frames (in milliseconds)
* @param {Object} [target] - target object for valid options * @param {Object} [target] - target object for valid options
* @throws {Error} Invalid options * @throws {Error} Invalid options
*/ */
function trySetAnimationOptions (source, target) { 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.object(source) && is.defined(source.loop)) {
if (is.integer(source.loop) && is.inRange(source.loop, 0, 65535)) { if (is.integer(source.loop) && is.inRange(source.loop, 0, 65535)) {
target.loop = source.loop; target.loop = source.loop;
@ -679,13 +666,16 @@ function trySetAnimationOptions (source, target) {
} }
} }
if (is.object(source) && is.defined(source.delay)) { if (is.object(source) && is.defined(source.delay)) {
if ( // We allow singular values as well
if (is.integer(source.delay) && is.inRange(source.delay, 0, 65535)) {
target.delay = [source.delay];
} else if (
Array.isArray(source.delay) && Array.isArray(source.delay) &&
source.delay.every(is.integer) && source.delay.every(is.integer) &&
source.delay.every(v => is.inRange(v, 0, 65535))) { source.delay.every(v => is.inRange(v, 0, 65535))) {
target.delay = source.delay; target.delay = source.delay;
} else { } else {
throw is.invalidParameterError('delay', 'array of integers between 0 and 65535', source.delay); throw is.invalidParameterError('delay', 'integer or an array of integers between 0 and 65535', source.delay);
} }
} }
} }

View File

@ -490,30 +490,24 @@ namespace sharp {
/* /*
Set animation properties if necessary. 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) { VImage SetAnimationProperties(VImage image, int nPages, int pageHeight, std::vector<int> delay, int loop) {
bool hasDelay = delay.size() != 1 || delay.front() != -1; bool hasDelay = !delay.empty();
if (pageHeight == 0 && image.get_typeof(VIPS_META_PAGE_HEIGHT) == G_TYPE_INT) { // Avoid a copy if none of the animation properties are needed.
pageHeight = image.get_int(VIPS_META_PAGE_HEIGHT); if (nPages == 1 && !hasDelay && loop == -1) return image;
if (delay.size() == 1) {
// We have just one delay, repeat that value for all frames.
delay.insert(delay.end(), nPages - 1, delay[0]);
} }
if (!hasDelay && image.get_typeof("delay") == VIPS_TYPE_ARRAY_INT) { // Attaching metadata, need to copy the image.
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(); VImage copy = image.copy();
copy.set(VIPS_META_PAGE_HEIGHT, pageHeight); // Only set page-height if we have more than one page, or this could
// accidentally turn into an animated image later.
if (nPages > 1) copy.set(VIPS_META_PAGE_HEIGHT, pageHeight);
if (hasDelay) copy.set("delay", delay); if (hasDelay) copy.set("delay", delay);
if (loop != -1) copy.set("loop", loop); if (loop != -1) copy.set("loop", loop);
@ -556,6 +550,14 @@ namespace sharp {
return copy; return copy;
} }
/*
Multi-page images can have a page height. Fetch it, and sanity check it.
If page-height is not set, it defaults to the image height
*/
int GetPageHeight(VImage image) {
return vips_image_get_page_height(image.get_image());
}
/* /*
Check the proposed format supports the current dimensions. Check the proposed format supports the current dimensions.
*/ */
@ -882,4 +884,74 @@ namespace sharp {
return image; return image;
} }
std::pair<double, double> ResolveShrink(int width, int height, int targetWidth, int targetHeight,
Canvas canvas, bool swap, bool withoutEnlargement) {
if (swap) {
// Swap input width and height when requested.
std::swap(width, height);
}
double hshrink = 1.0;
double vshrink = 1.0;
if (targetWidth > 0 && targetHeight > 0) {
// Fixed width and height
hshrink = static_cast<double>(width) / targetWidth;
vshrink = static_cast<double>(height) / targetHeight;
switch (canvas) {
case Canvas::CROP:
case Canvas::MIN:
if (hshrink < vshrink) {
vshrink = hshrink;
} else {
hshrink = vshrink;
}
break;
case Canvas::EMBED:
case Canvas::MAX:
if (hshrink > vshrink) {
vshrink = hshrink;
} else {
hshrink = vshrink;
}
break;
case Canvas::IGNORE_ASPECT:
if (swap) {
std::swap(hshrink, vshrink);
}
break;
}
} else if (targetWidth > 0) {
// Fixed width
hshrink = static_cast<double>(width) / targetWidth;
if (canvas != Canvas::IGNORE_ASPECT) {
// Auto height
vshrink = hshrink;
}
} else if (targetHeight > 0) {
// Fixed height
vshrink = static_cast<double>(height) / targetHeight;
if (canvas != Canvas::IGNORE_ASPECT) {
// Auto width
hshrink = vshrink;
}
}
// We should not enlarge (oversample) the output image,
// if withoutEnlargement is specified.
if (withoutEnlargement) {
hshrink = std::max(1.0, hshrink);
vshrink = std::max(1.0, vshrink);
}
// We don't want to shrink so much that we send an axis to 0
hshrink = std::min(hshrink, static_cast<double>(width));
vshrink = std::min(vshrink, static_cast<double>(height));
return std::make_pair(hshrink, vshrink);
}
} // namespace sharp } // namespace sharp

View File

@ -135,6 +135,14 @@ namespace sharp {
MISSING MISSING
}; };
enum class Canvas {
CROP,
EMBED,
MAX,
MIN,
IGNORE_ASPECT
};
// How many tasks are in the queue? // How many tasks are in the queue?
extern volatile int counterQueue; extern volatile int counterQueue;
@ -208,9 +216,8 @@ namespace sharp {
/* /*
Set animation properties if necessary. 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); VImage SetAnimationProperties(VImage image, int nPages, int pageHeight, std::vector<int> delay, int loop);
/* /*
Remove animation properties from image. Remove animation properties from image.
@ -232,6 +239,12 @@ namespace sharp {
*/ */
VImage SetDensity(VImage image, const double density); VImage SetDensity(VImage image, const double density);
/*
Multi-page images can have a page height. Fetch it, and sanity check it.
If page-height is not set, it defaults to the image height
*/
int GetPageHeight(VImage image);
/* /*
Check the proposed format supports the current dimensions. Check the proposed format supports the current dimensions.
*/ */
@ -325,6 +338,15 @@ namespace sharp {
*/ */
VImage EnsureAlpha(VImage image, double const value); VImage EnsureAlpha(VImage image, double const value);
/*
Calculate the shrink factor, taking into account auto-rotate, the canvas
mode, and so on. The hshrink/vshrink are the amount to shrink the input
image axes by in order for the output axes (ie. after rotation) to match
the required thumbnail width/height and canvas mode.
*/
std::pair<double, double> ResolveShrink(int width, int height, int targetWidth, int targetHeight,
Canvas canvas, bool swap, bool withoutEnlargement);
} // namespace sharp } // namespace sharp
#endif // SRC_COMMON_H_ #endif // SRC_COMMON_H_

View File

@ -180,8 +180,6 @@ class MetadataWorker : public Napi::AsyncWorker {
} }
if (baton->pageHeight > 0) { if (baton->pageHeight > 0) {
info.Set("pageHeight", baton->pageHeight); info.Set("pageHeight", baton->pageHeight);
} else if (baton->pages > 0) {
info.Set("pageHeight", baton->height);
} }
if (baton->loop >= 0) { if (baton->loop >= 0) {
info.Set("loop", baton->loop); info.Set("loop", baton->loop);

View File

@ -308,4 +308,98 @@ namespace sharp {
return image; return image;
} }
/*
* Split and crop each frame, reassemble, and update pageHeight.
*/
VImage CropMultiPage(VImage image, int left, int top, int width, int height,
int nPages, int *pageHeight) {
if (top == 0 && height == *pageHeight) {
// Fast path; no need to adjust the height of the multi-page image
return image.extract_area(left, 0, width, image.height());
} else {
std::vector<VImage> pages;
pages.reserve(nPages);
// Split the image into cropped frames
for (int i = 0; i < nPages; i++) {
pages.push_back(
image.extract_area(left, *pageHeight * i + top, width, height));
}
// Reassemble the frames into a tall, thin image
VImage assembled = VImage::arrayjoin(pages,
VImage::option()->set("across", 1));
// Update the page height
*pageHeight = height;
return assembled;
}
}
/*
* Split into frames, embed each frame, reassemble, and update pageHeight.
*/
VImage EmbedMultiPage(VImage image, int left, int top, int width, int height,
std::vector<double> background, int nPages, int *pageHeight) {
if (top == 0 && height == *pageHeight) {
// Fast path; no need to adjust the height of the multi-page image
return image.embed(left, 0, width, image.height(), VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background));
} else if (left == 0 && width == image.width()) {
// Fast path; no need to adjust the width of the multi-page image
std::vector<VImage> pages;
pages.reserve(nPages);
// Rearrange the tall image into a vertical grid
image = image.grid(*pageHeight, nPages, 1);
// Do the embed on the wide image
image = image.embed(0, top, image.width(), height, VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background));
// Split the wide image into frames
for (int i = 0; i < nPages; i++) {
pages.push_back(
image.extract_area(width * i, 0, width, height));
}
// Reassemble the frames into a tall, thin image
VImage assembled = VImage::arrayjoin(pages,
VImage::option()->set("across", 1));
// Update the page height
*pageHeight = height;
return assembled;
} else {
std::vector<VImage> pages;
pages.reserve(nPages);
// Split the image into frames
for (int i = 0; i < nPages; i++) {
pages.push_back(
image.extract_area(0, *pageHeight * i, image.width(), *pageHeight));
}
// Embed each frame in the target size
for (int i = 0; i < nPages; i++) {
pages[i] = pages[i].embed(left, top, width, height, VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background));
}
// Reassemble the frames into a tall, thin image
VImage assembled = VImage::arrayjoin(pages,
VImage::option()->set("across", 1));
// Update the page height
*pageHeight = height;
return assembled;
}
}
} // namespace sharp } // namespace sharp

View File

@ -108,6 +108,18 @@ namespace sharp {
*/ */
VImage EnsureColourspace(VImage image, VipsInterpretation colourspace); VImage EnsureColourspace(VImage image, VipsInterpretation colourspace);
/*
* Split and crop each frame, reassemble, and update pageHeight.
*/
VImage CropMultiPage(VImage image, int left, int top, int width, int height,
int nPages, int *pageHeight);
/*
* Split into frames, embed each frame, reassemble, and update pageHeight.
*/
VImage EmbedMultiPage(VImage image, int left, int top, int width, int height,
std::vector<double> background, int nPages, int *pageHeight);
} // namespace sharp } // namespace sharp
#endif // SRC_OPERATIONS_H_ #endif // SRC_OPERATIONS_H_

View File

@ -69,6 +69,15 @@ class PipelineWorker : public Napi::AsyncWorker {
std::tie(image, inputImageType) = sharp::OpenInput(baton->input); std::tie(image, inputImageType) = sharp::OpenInput(baton->input);
image = sharp::EnsureColourspace(image, baton->colourspaceInput); image = sharp::EnsureColourspace(image, baton->colourspaceInput);
int nPages = baton->input->pages;
if (nPages == -1) {
// Resolve the number of pages if we need to render until the end of the document
nPages = image.get_typeof(VIPS_META_N_PAGES) != 0 ? image.get_int(VIPS_META_N_PAGES) : 1;
}
// Get pre-resize page height
int pageHeight = sharp::GetPageHeight(image);
// Calculate angle of rotation // Calculate angle of rotation
VipsAngle rotation; VipsAngle rotation;
if (baton->useExifOrientation) { if (baton->useExifOrientation) {
@ -104,194 +113,171 @@ class PipelineWorker : public Napi::AsyncWorker {
// Pre extraction // Pre extraction
if (baton->topOffsetPre != -1) { if (baton->topOffsetPre != -1) {
image = image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre); image = nPages > 1
? sharp::CropMultiPage(image,
baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, nPages, &pageHeight)
: image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre);
} }
// Get pre-resize image width and height // Get pre-resize image width and height
int inputWidth = image.width(); int inputWidth = image.width();
int inputHeight = image.height(); int inputHeight = image.height();
if (!baton->rotateBeforePreExtract &&
(rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) {
// Swap input output width and height when rotating by 90 or 270 degrees
std::swap(inputWidth, inputHeight);
}
// If withoutEnlargement is specified, // Is there just one page? Shrink to inputHeight instead
// Override target width and height if exceeds respective value from input file if (nPages == 1) {
if (baton->withoutEnlargement) { pageHeight = inputHeight;
if (baton->width > inputWidth) {
baton->width = inputWidth;
}
if (baton->height > inputHeight) {
baton->height = inputHeight;
}
} }
// Scaling calculations // Scaling calculations
double xfactor = 1.0; double hshrink;
double yfactor = 1.0; double vshrink;
int targetResizeWidth = baton->width; int targetResizeWidth = baton->width;
int targetResizeHeight = baton->height; int targetResizeHeight = baton->height;
if (baton->width > 0 && baton->height > 0) {
// Fixed width and height
xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
switch (baton->canvas) {
case Canvas::CROP:
if (xfactor < yfactor) {
targetResizeHeight = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
yfactor = xfactor;
} else {
targetResizeWidth = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
xfactor = yfactor;
}
break;
case Canvas::EMBED:
if (xfactor > yfactor) {
targetResizeHeight = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
yfactor = xfactor;
} else {
targetResizeWidth = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
xfactor = yfactor;
}
break;
case Canvas::MAX:
if (xfactor > yfactor) {
targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
yfactor = xfactor;
} else {
targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
xfactor = yfactor;
}
break;
case Canvas::MIN:
if (xfactor < yfactor) {
targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / xfactor));
yfactor = xfactor;
} else {
targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / yfactor));
xfactor = yfactor;
}
break;
case Canvas::IGNORE_ASPECT:
if (!baton->rotateBeforePreExtract &&
(rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) {
std::swap(xfactor, yfactor);
}
break;
}
} else if (baton->width > 0) {
// Fixed width
xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
if (baton->canvas == Canvas::IGNORE_ASPECT) {
targetResizeHeight = baton->height = inputHeight;
} else {
// Auto height
yfactor = xfactor;
targetResizeHeight = baton->height = static_cast<int>(round(static_cast<double>(inputHeight) / yfactor));
}
} else if (baton->height > 0) {
// Fixed height
yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
if (baton->canvas == Canvas::IGNORE_ASPECT) {
targetResizeWidth = baton->width = inputWidth;
} else {
// Auto width
xfactor = yfactor;
targetResizeWidth = baton->width = static_cast<int>(round(static_cast<double>(inputWidth) / xfactor));
}
} else {
// Identity transform
baton->width = inputWidth;
baton->height = inputHeight;
}
// Calculate integral box shrink // Swap input output width and height when rotating by 90 or 270 degrees
int xshrink = std::max(1, static_cast<int>(floor(xfactor))); bool swap = !baton->rotateBeforePreExtract && (rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270);
int yshrink = std::max(1, static_cast<int>(floor(yfactor)));
// Calculate residual float affine transformation // Shrink to pageHeight, so we work for multi-page images
double xresidual = static_cast<double>(xshrink) / xfactor; std::tie(hshrink, vshrink) = sharp::ResolveShrink(
double yresidual = static_cast<double>(yshrink) / yfactor; inputWidth, pageHeight, targetResizeWidth, targetResizeHeight,
baton->canvas, swap, baton->withoutEnlargement);
// If integral x and y shrink are equal, try to use shrink-on-load for JPEG and WebP, // The jpeg preload shrink.
// but not when applying gamma correction, pre-resize extract, trim or input colourspace int jpegShrinkOnLoad = 1;
int shrink_on_load = 1;
int shrink_on_load_factor = 1; // WebP, PDF, SVG scale
// Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false double scale = 1.0;
// for more consistent results and avoid occasional small image shifting
if (!baton->fastShrinkOnLoad) { // Try to reload input using shrink-on-load for JPEG, WebP, SVG and PDF, when:
shrink_on_load_factor = 2; // - the width or height parameters are specified;
} // - gamma correction doesn't need to be applied;
if ( // - trimming or pre-resize extract isn't required;
xshrink == yshrink && xshrink >= 2 * shrink_on_load_factor && // - input colourspace is not specified;
(inputImageType == sharp::ImageType::JPEG || inputImageType == sharp::ImageType::WEBP) && bool const shouldPreShrink = (targetResizeWidth > 0 || targetResizeHeight > 0) &&
baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 && baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 &&
baton->colourspaceInput == VIPS_INTERPRETATION_LAST && baton->colourspaceInput == VIPS_INTERPRETATION_LAST;
image.width() > 3 && image.height() > 3 && baton->input->pages == 1
) { if (shouldPreShrink) {
if (xshrink >= 8 * shrink_on_load_factor) { // The common part of the shrink: the bit by which both axes must be shrunk
xfactor = xfactor / 8; double shrink = std::min(hshrink, vshrink);
yfactor = yfactor / 8;
shrink_on_load = 8; if (inputImageType == sharp::ImageType::JPEG) {
} else if (xshrink >= 4 * shrink_on_load_factor) { // Leave at least a factor of two for the final resize step, when fastShrinkOnLoad: false
xfactor = xfactor / 4; // for more consistent results and avoid occasional small image shifting
yfactor = yfactor / 4; int factor = baton->fastShrinkOnLoad ? 1 : 2;
shrink_on_load = 4; if (shrink >= 8 * factor) {
} else if (xshrink >= 2 * shrink_on_load_factor) { jpegShrinkOnLoad = 8;
xfactor = xfactor / 2; } else if (shrink >= 4 * factor) {
yfactor = yfactor / 2; jpegShrinkOnLoad = 4;
shrink_on_load = 2; } else if (shrink >= 2 * factor) {
jpegShrinkOnLoad = 2;
}
} else if (inputImageType == sharp::ImageType::WEBP ||
inputImageType == sharp::ImageType::SVG ||
inputImageType == sharp::ImageType::PDF) {
scale = 1.0 / shrink;
} }
} }
// Help ensure a final kernel-based reduction to prevent shrink aliasing
if (shrink_on_load > 1 && (xresidual == 1.0 || yresidual == 1.0)) { // Reload input using shrink-on-load, it'll be an integer shrink
shrink_on_load = shrink_on_load / 2; // factor for jpegload*, a double scale factor for webpload*,
xfactor = xfactor * 2; // pdfload* and svgload*
yfactor = yfactor * 2; if (jpegShrinkOnLoad > 1) {
}
if (shrink_on_load > 1) {
// Reload input using shrink-on-load
vips::VOption *option = VImage::option() vips::VOption *option = VImage::option()
->set("access", baton->input->access) ->set("access", baton->input->access)
->set("shrink", shrink_on_load) ->set("shrink", jpegShrinkOnLoad)
->set("fail", baton->input->failOnError); ->set("fail", baton->input->failOnError);
if (baton->input->buffer != nullptr) { if (baton->input->buffer != nullptr) {
// Reload JPEG buffer
VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength); VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
if (inputImageType == sharp::ImageType::JPEG) { image = VImage::jpegload_buffer(blob, option);
// Reload JPEG buffer
image = VImage::jpegload_buffer(blob, option);
} else {
// Reload WebP buffer
image = VImage::webpload_buffer(blob, option);
}
vips_area_unref(reinterpret_cast<VipsArea*>(blob)); vips_area_unref(reinterpret_cast<VipsArea*>(blob));
} else { } else {
if (inputImageType == sharp::ImageType::JPEG) { // Reload JPEG file
// Reload JPEG file image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option);
image = VImage::jpegload(const_cast<char*>(baton->input->file.data()), option); }
} else if (scale != 1.0) {
vips::VOption *option = VImage::option()
->set("access", baton->input->access)
->set("scale", scale)
->set("fail", baton->input->failOnError);
if (inputImageType == sharp::ImageType::WEBP) {
option->set("n", baton->input->pages);
option->set("page", baton->input->page);
if (baton->input->buffer != nullptr) {
// Reload WebP buffer
VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
image = VImage::webpload_buffer(blob, option);
vips_area_unref(reinterpret_cast<VipsArea*>(blob));
} else { } else {
// Reload WebP file // Reload WebP file
image = VImage::webpload(const_cast<char*>(baton->input->file.data()), option); image = VImage::webpload(const_cast<char*>(baton->input->file.data()), option);
} }
} } else if (inputImageType == sharp::ImageType::SVG) {
// Recalculate integral shrink and double residual option->set("unlimited", baton->input->unlimited);
int const shrunkOnLoadWidth = image.width(); option->set("dpi", baton->input->density);
int const shrunkOnLoadHeight = image.height();
if (!baton->rotateBeforePreExtract && if (baton->input->buffer != nullptr) {
(rotation == VIPS_ANGLE_D90 || rotation == VIPS_ANGLE_D270)) { // Reload SVG buffer
// Swap when rotating by 90 or 270 degrees VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
xfactor = static_cast<double>(shrunkOnLoadWidth) / static_cast<double>(targetResizeHeight); image = VImage::svgload_buffer(blob, option);
yfactor = static_cast<double>(shrunkOnLoadHeight) / static_cast<double>(targetResizeWidth); vips_area_unref(reinterpret_cast<VipsArea*>(blob));
} else { } else {
xfactor = static_cast<double>(shrunkOnLoadWidth) / static_cast<double>(targetResizeWidth); // Reload SVG file
yfactor = static_cast<double>(shrunkOnLoadHeight) / static_cast<double>(targetResizeHeight); image = VImage::svgload(const_cast<char*>(baton->input->file.data()), option);
}
sharp::SetDensity(image, baton->input->density);
} else if (inputImageType == sharp::ImageType::PDF) {
option->set("n", baton->input->pages);
option->set("page", baton->input->page);
option->set("dpi", baton->input->density);
if (baton->input->buffer != nullptr) {
// Reload PDF buffer
VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength);
image = VImage::pdfload_buffer(blob, option);
vips_area_unref(reinterpret_cast<VipsArea*>(blob));
} else {
// Reload PDF file
image = VImage::pdfload(const_cast<char*>(baton->input->file.data()), option);
}
sharp::SetDensity(image, baton->input->density);
} }
} }
// Remove animation properties from single page images
if (baton->input->pages == 1) { // Any pre-shrinking may already have been done
image = sharp::RemoveAnimationProperties(image); int thumbWidth = image.width();
int thumbHeight = image.height();
// After pre-shrink, but before the main shrink stage
// Reuse the initial pageHeight if we didn't pre-shrink
int preshrunkPageHeight = shouldPreShrink ? sharp::GetPageHeight(image) : pageHeight;
if (baton->fastShrinkOnLoad && jpegShrinkOnLoad > 1) {
// JPEG shrink-on-load rounds the output dimensions down, which
// may cause incorrect dimensions when fastShrinkOnLoad is enabled
// Just recalculate vshrink / hshrink on the main image instead of
// the pre-shrunk image when this is the case
hshrink = static_cast<double>(thumbWidth) / (static_cast<double>(inputWidth) / hshrink);
vshrink = static_cast<double>(preshrunkPageHeight) / (static_cast<double>(pageHeight) / vshrink);
} else {
// Shrink to preshrunkPageHeight, so we work for multi-page images
std::tie(hshrink, vshrink) = sharp::ResolveShrink(
thumbWidth, preshrunkPageHeight, targetResizeWidth, targetResizeHeight,
baton->canvas, swap, baton->withoutEnlargement);
}
int targetHeight = static_cast<int>(std::rint(static_cast<double>(preshrunkPageHeight) / vshrink));
int targetPageHeight = targetHeight;
// In toilet-roll mode, we must adjust vshrink so that we exactly hit
// preshrunkPageHeight or we'll have pixels straddling pixel boundaries
if (thumbHeight > preshrunkPageHeight) {
targetHeight *= nPages;
vshrink = static_cast<double>(thumbHeight) / targetHeight;
} }
// Ensure we're using a device-independent colour space // Ensure we're using a device-independent colour space
@ -345,7 +331,7 @@ class PipelineWorker : public Napi::AsyncWorker {
image = image.colourspace(VIPS_INTERPRETATION_B_W); image = image.colourspace(VIPS_INTERPRETATION_B_W);
} }
bool const shouldResize = xfactor != 1.0 || yfactor != 1.0; bool const shouldResize = hshrink != 1.0 || vshrink != 1.0;
bool const shouldBlur = baton->blurSigma != 0.0; bool const shouldBlur = baton->blurSigma != 0.0;
bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0; bool const shouldConv = baton->convKernelWidth * baton->convKernelHeight > 0;
bool const shouldSharpen = baton->sharpenSigma != 0.0; bool const shouldSharpen = baton->sharpenSigma != 0.0;
@ -379,21 +365,8 @@ class PipelineWorker : public Napi::AsyncWorker {
) { ) {
throw vips::VError("Unknown kernel"); throw vips::VError("Unknown kernel");
} }
// Ensure shortest edge is at least 1 pixel image = image.resize(1.0 / hshrink, VImage::option()
if (image.width() / xfactor < 0.5) { ->set("vscale", 1.0 / vshrink)
xfactor = 2 * image.width();
if (baton->canvas != Canvas::EMBED) {
baton->width = 1;
}
}
if (image.height() / yfactor < 0.5) {
yfactor = 2 * image.height();
if (baton->canvas != Canvas::EMBED) {
baton->height = 1;
}
}
image = image.resize(1.0 / xfactor, VImage::option()
->set("vscale", 1.0 / yfactor)
->set("kernel", kernel)); ->set("kernel", kernel));
} }
@ -429,52 +402,67 @@ class PipelineWorker : public Napi::AsyncWorker {
image = image.copy(VImage::option()->set("interpretation", baton->colourspace)); image = image.copy(VImage::option()->set("interpretation", baton->colourspace));
} }
inputWidth = image.width();
inputHeight = nPages > 1 ? targetPageHeight : image.height();
// Resolve dimensions
if (baton->width <= 0) {
baton->width = inputWidth;
}
if (baton->height <= 0) {
baton->height = inputHeight;
}
// Crop/embed // Crop/embed
if (image.width() != baton->width || image.height() != baton->height) { if (inputWidth != baton->width || inputHeight != baton->height) {
if (baton->canvas == Canvas::EMBED) { if (baton->canvas == sharp::Canvas::EMBED) {
std::vector<double> background; std::vector<double> background;
std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha); std::tie(image, background) = sharp::ApplyAlpha(image, baton->resizeBackground, shouldPremultiplyAlpha);
// Embed // Embed
// Calculate where to position the embeded image if gravity specified, else center. // Calculate where to position the embedded image if gravity specified, else center.
int left; int left;
int top; int top;
left = static_cast<int>(round((baton->width - image.width()) / 2)); left = static_cast<int>(round((baton->width - inputWidth) / 2));
top = static_cast<int>(round((baton->height - image.height()) / 2)); top = static_cast<int>(round((baton->height - inputHeight) / 2));
int width = std::max(image.width(), baton->width); int width = std::max(inputWidth, baton->width);
int height = std::max(image.height(), baton->height); int height = std::max(inputHeight, baton->height);
std::tie(left, top) = sharp::CalculateEmbedPosition( std::tie(left, top) = sharp::CalculateEmbedPosition(
image.width(), image.height(), baton->width, baton->height, baton->position); inputWidth, inputHeight, baton->width, baton->height, baton->position);
image = image.embed(left, top, width, height, VImage::option() image = nPages > 1
->set("extend", VIPS_EXTEND_BACKGROUND) ? sharp::EmbedMultiPage(image,
->set("background", background)); left, top, width, height, background, nPages, &targetPageHeight)
: image.embed(left, top, width, height, VImage::option()
->set("extend", VIPS_EXTEND_BACKGROUND)
->set("background", background));
} else if (baton->canvas == sharp::Canvas::CROP) {
if (baton->width > inputWidth) {
baton->width = inputWidth;
}
if (baton->height > inputHeight) {
baton->height = inputHeight;
}
} else if ( // Crop
baton->canvas != Canvas::IGNORE_ASPECT &&
(image.width() > baton->width || image.height() > baton->height)
) {
// Crop/max/min
if (baton->position < 9) { if (baton->position < 9) {
// Gravity-based crop // Gravity-based crop
int left; int left;
int top; int top;
std::tie(left, top) = sharp::CalculateCrop( std::tie(left, top) = sharp::CalculateCrop(
image.width(), image.height(), baton->width, baton->height, baton->position); inputWidth, inputHeight, baton->width, baton->height, baton->position);
int width = std::min(image.width(), baton->width); int width = std::min(inputWidth, baton->width);
int height = std::min(image.height(), baton->height); int height = std::min(inputHeight, baton->height);
image = image.extract_area(left, top, width, height);
} else { image = nPages > 1
? sharp::CropMultiPage(image,
left, top, width, height, nPages, &targetPageHeight)
: image.extract_area(left, top, width, height);
} else if (nPages == 1) { // Skip smart crop for multi-page images
// Attention-based or Entropy-based crop // Attention-based or Entropy-based crop
if (baton->width > image.width()) {
baton->width = image.width();
}
if (baton->height > image.height()) {
baton->height = image.height();
}
image = image.tilecache(VImage::option() image = image.tilecache(VImage::option()
->set("access", VIPS_ACCESS_RANDOM) ->set("access", VIPS_ACCESS_RANDOM)
->set("threaded", TRUE)); ->set("threaded", TRUE));
@ -496,8 +484,17 @@ class PipelineWorker : public Napi::AsyncWorker {
// Post extraction // Post extraction
if (baton->topOffsetPost != -1) { if (baton->topOffsetPost != -1) {
image = image.extract_area( if (nPages > 1) {
baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost); image = sharp::CropMultiPage(image,
baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost,
nPages, &targetPageHeight);
// heightPost is used in the info object, so update to reflect the number of pages
baton->heightPost *= nPages;
} else {
image = image.extract_area(
baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost);
}
} }
// Affine transform // Affine transform
@ -519,10 +516,13 @@ class PipelineWorker : public Napi::AsyncWorker {
// Embed // Embed
baton->width = image.width() + baton->extendLeft + baton->extendRight; baton->width = image.width() + baton->extendLeft + baton->extendRight;
baton->height = image.height() + baton->extendTop + baton->extendBottom; baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height, image = nPages > 1
VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background)); ? sharp::EmbedMultiPage(image,
baton->extendLeft, baton->extendTop, baton->width, baton->height, background, nPages, &targetPageHeight)
: image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
} }
// Median - must happen before blurring, due to the utility of blurring after thresholding // Median - must happen before blurring, due to the utility of blurring after thresholding
if (shouldApplyMedian) { if (shouldApplyMedian) {
@ -763,10 +763,7 @@ class PipelineWorker : public Napi::AsyncWorker {
baton->height = image.height(); baton->height = image.height();
image = sharp::SetAnimationProperties( image = sharp::SetAnimationProperties(
image, image, nPages, targetPageHeight, baton->delay, baton->loop);
baton->pageHeight,
baton->delay,
baton->loop);
// Output // Output
sharp::SetTimeout(image, baton->timeoutSeconds); sharp::SetTimeout(image, baton->timeoutSeconds);
@ -1317,15 +1314,15 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
// Canvas option // Canvas option
std::string canvas = sharp::AttrAsStr(options, "canvas"); std::string canvas = sharp::AttrAsStr(options, "canvas");
if (canvas == "crop") { if (canvas == "crop") {
baton->canvas = Canvas::CROP; baton->canvas = sharp::Canvas::CROP;
} else if (canvas == "embed") { } else if (canvas == "embed") {
baton->canvas = Canvas::EMBED; baton->canvas = sharp::Canvas::EMBED;
} else if (canvas == "max") { } else if (canvas == "max") {
baton->canvas = Canvas::MAX; baton->canvas = sharp::Canvas::MAX;
} else if (canvas == "min") { } else if (canvas == "min") {
baton->canvas = Canvas::MIN; baton->canvas = sharp::Canvas::MIN;
} else if (canvas == "ignore_aspect") { } else if (canvas == "ignore_aspect") {
baton->canvas = Canvas::IGNORE_ASPECT; baton->canvas = sharp::Canvas::IGNORE_ASPECT;
} }
// Tint chroma // Tint chroma
baton->tintA = sharp::AttrAsDouble(options, "tintA"); baton->tintA = sharp::AttrAsDouble(options, "tintA");
@ -1520,10 +1517,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT, vips_enum_from_nick(nullptr, VIPS_TYPE_BAND_FORMAT,
sharp::AttrAsStr(options, "rawDepth").data())); sharp::AttrAsStr(options, "rawDepth").data()));
// Animated output // Animated output properties
if (sharp::HasAttr(options, "pageHeight")) {
baton->pageHeight = sharp::AttrAsUint32(options, "pageHeight");
}
if (sharp::HasAttr(options, "loop")) { if (sharp::HasAttr(options, "loop")) {
baton->loop = sharp::AttrAsUint32(options, "loop"); baton->loop = sharp::AttrAsUint32(options, "loop");
} }

View File

@ -27,14 +27,6 @@
Napi::Value pipeline(const Napi::CallbackInfo& info); Napi::Value pipeline(const Napi::CallbackInfo& info);
enum class Canvas {
CROP,
EMBED,
MAX,
MIN,
IGNORE_ASPECT
};
struct Composite { struct Composite {
sharp::InputDescriptor *input; sharp::InputDescriptor *input;
VipsBlendMode mode; VipsBlendMode mode;
@ -75,7 +67,7 @@ struct PipelineBaton {
int width; int width;
int height; int height;
int channels; int channels;
Canvas canvas; sharp::Canvas canvas;
int position; int position;
std::vector<double> resizeBackground; std::vector<double> resizeBackground;
bool hasCropOffset; bool hasCropOffset;
@ -200,7 +192,6 @@ struct PipelineBaton {
double ensureAlpha; double ensureAlpha;
VipsInterpretation colourspaceInput; VipsInterpretation colourspaceInput;
VipsInterpretation colourspace; VipsInterpretation colourspace;
int pageHeight;
std::vector<int> delay; std::vector<int> delay;
int loop; int loop;
int tileSize; int tileSize;
@ -221,7 +212,7 @@ struct PipelineBaton {
topOffsetPre(-1), topOffsetPre(-1),
topOffsetPost(-1), topOffsetPost(-1),
channels(0), channels(0),
canvas(Canvas::CROP), canvas(sharp::Canvas::CROP),
position(0), position(0),
resizeBackground{ 0.0, 0.0, 0.0, 255.0 }, resizeBackground{ 0.0, 0.0, 0.0, 255.0 },
hasCropOffset(false), hasCropOffset(false),
@ -334,7 +325,6 @@ struct PipelineBaton {
ensureAlpha(-1.0), ensureAlpha(-1.0),
colourspaceInput(VIPS_INTERPRETATION_LAST), colourspaceInput(VIPS_INTERPRETATION_LAST),
colourspace(VIPS_INTERPRETATION_LAST), colourspace(VIPS_INTERPRETATION_LAST),
pageHeight(0),
delay{-1}, delay{-1},
loop(-1), loop(-1),
tileSize(256), tileSize(256),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 KiB

After

Width:  |  Height:  |  Size: 943 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -27,7 +27,7 @@ describe('AVIF', () => {
format: 'jpeg', format: 'jpeg',
hasAlpha: false, hasAlpha: false,
hasProfile: false, hasProfile: false,
height: 13, height: 14,
isProgressive: false, isProgressive: false,
space: 'srgb', space: 'srgb',
width: 32 width: 32
@ -50,7 +50,6 @@ describe('AVIF', () => {
hasProfile: false, hasProfile: false,
height: 26, height: 26,
isProgressive: false, isProgressive: false,
pageHeight: 26,
pagePrimary: 0, pagePrimary: 0,
pages: 1, pages: 1,
space: 'srgb', space: 'srgb',
@ -71,9 +70,8 @@ describe('AVIF', () => {
format: 'heif', format: 'heif',
hasAlpha: false, hasAlpha: false,
hasProfile: false, hasProfile: false,
height: 12, height: 14,
isProgressive: false, isProgressive: false,
pageHeight: 12,
pagePrimary: 0, pagePrimary: 0,
pages: 1, pages: 1,
space: 'srgb', space: 'srgb',
@ -97,7 +95,6 @@ describe('AVIF', () => {
hasProfile: false, hasProfile: false,
height: 300, height: 300,
isProgressive: false, isProgressive: false,
pageHeight: 300,
pagePrimary: 0, pagePrimary: 0,
pages: 1, pages: 1,
space: 'srgb', space: 'srgb',

View File

@ -6,16 +6,30 @@ const sharp = require('../../');
const fixtures = require('../fixtures'); const fixtures = require('../fixtures');
describe('Extend', function () { describe('Extend', function () {
it('extend all sides equally via a single value', function (done) { describe('extend all sides equally via a single value', function () {
sharp(fixtures.inputJpg) it('JPEG', function (done) {
.resize(120) sharp(fixtures.inputJpg)
.extend(10) .resize(120)
.toBuffer(function (err, data, info) { .extend(10)
if (err) throw err; .toBuffer(function (err, data, info) {
assert.strictEqual(140, info.width); if (err) throw err;
assert.strictEqual(118, info.height); assert.strictEqual(140, info.width);
fixtures.assertSimilar(fixtures.expected('extend-equal-single.jpg'), data, done); assert.strictEqual(118, info.height);
}); fixtures.assertSimilar(fixtures.expected('extend-equal-single.jpg'), data, done);
});
});
it('Animated WebP', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(120)
.extend(10)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(140, info.width);
assert.strictEqual(140 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('extend-equal-single.webp'), data, done);
});
});
}); });
it('extend all sides equally with RGB', function (done) { it('extend all sides equally with RGB', function (done) {

View File

@ -39,10 +39,35 @@ describe('Partial image extraction', function () {
}); });
}); });
describe('Animated WebP', function () {
it('Before resize', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.extract({ left: 0, top: 30, width: 80, height: 20 })
.resize(320, 80)
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-center-height.webp'), data, done);
});
});
it('After resize', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(320, 320)
.extract({ left: 0, top: 120, width: 320, height: 80 })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-center-height.webp'), data, done);
});
});
});
it('TIFF', function (done) { it('TIFF', function (done) {
sharp(fixtures.inputTiff) sharp(fixtures.inputTiff)
.extract({ left: 34, top: 63, width: 341, height: 529 }) .extract({ left: 34, top: 63, width: 341, height: 529 })
.jpeg()
.toBuffer(function (err, data, info) { .toBuffer(function (err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(341, info.width); assert.strictEqual(341, info.width);

View File

@ -80,12 +80,6 @@ describe('GIF input', () => {
assert.strictEqual(true, reduced.length < original.length); assert.strictEqual(true, reduced.length < original.length);
}); });
it('invalid pageHeight throws', () => {
assert.throws(() => {
sharp().gif({ pageHeight: 0 });
});
});
it('invalid loop throws', () => { it('invalid loop throws', () => {
assert.throws(() => { assert.throws(() => {
sharp().gif({ loop: -1 }); sharp().gif({ loop: -1 });
@ -97,7 +91,7 @@ describe('GIF input', () => {
it('invalid delay throws', () => { it('invalid delay throws', () => {
assert.throws(() => { assert.throws(() => {
sharp().gif({ delay: [-1] }); sharp().gif({ delay: -1 });
}); });
assert.throws(() => { assert.throws(() => {
sharp().gif({ delay: [65536] }); sharp().gif({ delay: [65536] });

View File

@ -194,6 +194,29 @@ describe('Image metadata', function () {
it('Animated WebP', () => it('Animated WebP', () =>
sharp(fixtures.inputWebPAnimated) sharp(fixtures.inputWebPAnimated)
.metadata()
.then(({
format, width, height, space, channels, depth,
isProgressive, pages, 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(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 all pages', () =>
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.metadata() .metadata()
.then(({ .then(({
format, width, height, space, channels, depth, format, width, height, space, channels, depth,
@ -202,7 +225,7 @@ describe('Image metadata', function () {
}) => { }) => {
assert.strictEqual(format, 'webp'); assert.strictEqual(format, 'webp');
assert.strictEqual(width, 80); assert.strictEqual(width, 80);
assert.strictEqual(height, 80); assert.strictEqual(height, 720);
assert.strictEqual(space, 'srgb'); assert.strictEqual(space, 'srgb');
assert.strictEqual(channels, 4); assert.strictEqual(channels, 4);
assert.strictEqual(depth, 'uchar'); assert.strictEqual(depth, 'uchar');
@ -221,8 +244,8 @@ describe('Image metadata', function () {
.metadata() .metadata()
.then(({ .then(({
format, width, height, space, channels, depth, format, width, height, space, channels, depth,
isProgressive, pages, pageHeight, loop, delay, isProgressive, pages, loop, delay, hasProfile,
hasProfile, hasAlpha hasAlpha
}) => { }) => {
assert.strictEqual(format, 'webp'); assert.strictEqual(format, 'webp');
assert.strictEqual(width, 370); assert.strictEqual(width, 370);
@ -232,7 +255,6 @@ describe('Image metadata', function () {
assert.strictEqual(depth, 'uchar'); assert.strictEqual(depth, 'uchar');
assert.strictEqual(isProgressive, false); assert.strictEqual(isProgressive, false);
assert.strictEqual(pages, 10); assert.strictEqual(pages, 10);
assert.strictEqual(pageHeight, 285);
assert.strictEqual(loop, 3); assert.strictEqual(loop, 3);
assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]); assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]);
assert.strictEqual(hasProfile, false); assert.strictEqual(hasProfile, false);
@ -285,8 +307,8 @@ describe('Image metadata', function () {
.metadata() .metadata()
.then(({ .then(({
format, width, height, space, channels, depth, format, width, height, space, channels, depth,
isProgressive, pages, pageHeight, loop, delay, isProgressive, pages, loop, delay, background,
background, hasProfile, hasAlpha hasProfile, hasAlpha
}) => { }) => {
assert.strictEqual(format, 'gif'); assert.strictEqual(format, 'gif');
assert.strictEqual(width, 80); assert.strictEqual(width, 80);
@ -296,7 +318,6 @@ describe('Image metadata', function () {
assert.strictEqual(depth, 'uchar'); assert.strictEqual(depth, 'uchar');
assert.strictEqual(isProgressive, false); assert.strictEqual(isProgressive, false);
assert.strictEqual(pages, 30); assert.strictEqual(pages, 30);
assert.strictEqual(pageHeight, 80);
assert.strictEqual(loop, 0); assert.strictEqual(loop, 0);
assert.deepStrictEqual(delay, Array(30).fill(30)); assert.deepStrictEqual(delay, Array(30).fill(30));
assert.deepStrictEqual(background, { r: 0, g: 0, b: 0 }); assert.deepStrictEqual(background, { r: 0, g: 0, b: 0 });
@ -310,8 +331,8 @@ describe('Image metadata', function () {
.metadata() .metadata()
.then(({ .then(({
format, width, height, space, channels, depth, format, width, height, space, channels, depth,
isProgressive, pages, pageHeight, loop, delay, isProgressive, pages, loop, delay, hasProfile,
hasProfile, hasAlpha hasAlpha
}) => { }) => {
assert.strictEqual(format, 'gif'); assert.strictEqual(format, 'gif');
assert.strictEqual(width, 370); assert.strictEqual(width, 370);
@ -321,7 +342,6 @@ describe('Image metadata', function () {
assert.strictEqual(depth, 'uchar'); assert.strictEqual(depth, 'uchar');
assert.strictEqual(isProgressive, false); assert.strictEqual(isProgressive, false);
assert.strictEqual(pages, 10); assert.strictEqual(pages, 10);
assert.strictEqual(pageHeight, 285);
assert.strictEqual(loop, 2); assert.strictEqual(loop, 2);
assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]); assert.deepStrictEqual(delay, [...Array(9).fill(3000), 15000]);
assert.strictEqual(hasProfile, false); assert.strictEqual(hasProfile, false);
@ -522,7 +542,7 @@ describe('Image metadata', function () {
assert.strictEqual('Relative', profile.intent); assert.strictEqual('Relative', profile.intent);
assert.strictEqual('Printer', profile.deviceClass); assert.strictEqual('Printer', profile.deviceClass);
}); });
fixtures.assertSimilar(output, fixtures.path('expected/icc-cmyk.jpg'), { threshold: 0 }, done); fixtures.assertSimilar(output, fixtures.expected('icc-cmyk.jpg'), { threshold: 0 }, done);
}); });
}); });
@ -533,7 +553,7 @@ describe('Image metadata', function () {
.withMetadata({ icc: fixtures.path('hilutite.icm') }) .withMetadata({ icc: fixtures.path('hilutite.icm') })
.toFile(output, function (err, info) { .toFile(output, function (err, info) {
if (err) throw err; if (err) throw err;
fixtures.assertMaxColourDistance(output, fixtures.path('expected/hilutite.jpg'), 9); fixtures.assertMaxColourDistance(output, fixtures.expected('hilutite.jpg'), 9);
done(); done();
}); });
}); });
@ -737,7 +757,6 @@ describe('Image metadata', function () {
depth: 'uchar', depth: 'uchar',
isProgressive: false, isProgressive: false,
pages: 1, pages: 1,
pageHeight: 858,
pagePrimary: 0, pagePrimary: 0,
compression: 'av1', compression: 'av1',
hasProfile: false, hasProfile: false,

View File

@ -148,6 +148,42 @@ describe('Resize fit=contain', function () {
}); });
}); });
describe('Animated WebP', function () {
it('Width only', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(320, 240, {
fit: 'contain',
background: { r: 255, g: 0, b: 0 }
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240 * 9, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-animated-width.webp'), data, done);
});
});
it('Height only', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(240, 320, {
fit: 'contain',
background: { r: 255, g: 0, b: 0 }
})
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(240, info.width);
assert.strictEqual(320 * 9, info.height);
assert.strictEqual(4, info.channels);
fixtures.assertSimilar(fixtures.expected('embed-animated-height.webp'), data, done);
});
});
});
it('Invalid position values should fail', function () { it('Invalid position values should fail', function () {
[-1, 8.1, 9, 1000000, false, 'vallejo'].forEach(function (position) { [-1, 8.1, 9, 1000000, false, 'vallejo'].forEach(function (position) {
assert.throws(function () { assert.throws(function () {

View File

@ -269,6 +269,30 @@ describe('Resize fit=cover', function () {
}); });
}); });
describe('Animated WebP', function () {
it('Width only', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(80, 320, { fit: sharp.fit.cover })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-center-width.webp'), data, done);
});
});
it('Height only', function (done) {
sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize(320, 80, { fit: sharp.fit.cover })
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80 * 9, info.height);
fixtures.assertSimilar(fixtures.expected('gravity-center-height.webp'), data, done);
});
});
});
describe('Entropy-based strategy', function () { describe('Entropy-based strategy', function () {
it('JPEG', function (done) { it('JPEG', function (done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)

View File

@ -74,6 +74,27 @@ describe('SVG input', function () {
}); });
}); });
it('Convert SVG to PNG utilizing scale-on-load', function (done) {
const size = 1024;
sharp(fixtures.inputSvgSmallViewBox)
.resize(size)
.toFormat('png')
.toBuffer(function (err, data, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(size, info.width);
assert.strictEqual(size, info.height);
fixtures.assertSimilar(fixtures.expected('circle.png'), data, function (err) {
if (err) throw err;
sharp(data).metadata(function (err, info) {
if (err) throw err;
assert.strictEqual(72, info.density);
done();
});
});
});
});
it('Convert SVG to PNG at 14.4DPI', function (done) { it('Convert SVG to PNG at 14.4DPI', function (done) {
sharp(fixtures.inputSvg, { density: 14.4 }) sharp(fixtures.inputSvg, { density: 14.4 })
.toFormat('png') .toFormat('png')

View File

@ -133,12 +133,6 @@ describe('WebP', function () {
}); });
}); });
it('invalid pageHeight throws', () => {
assert.throws(() => {
sharp().webp({ pageHeight: 0 });
});
});
it('invalid loop throws', () => { it('invalid loop throws', () => {
assert.throws(() => { assert.throws(() => {
sharp().webp({ loop: -1 }); sharp().webp({ loop: -1 });
@ -151,7 +145,7 @@ describe('WebP', function () {
it('invalid delay throws', () => { it('invalid delay throws', () => {
assert.throws(() => { assert.throws(() => {
sharp().webp({ delay: [-1] }); sharp().webp({ delay: -1 });
}); });
assert.throws(() => { assert.throws(() => {
@ -159,16 +153,13 @@ describe('WebP', function () {
}); });
}); });
it('should double the number of frames with default delay', async () => { it('should repeat a single delay for all frames', async () => {
const original = await sharp(fixtures.inputWebPAnimated, { pages: -1 }).metadata();
const updated = await sharp(fixtures.inputWebPAnimated, { pages: -1 }) const updated = await sharp(fixtures.inputWebPAnimated, { pages: -1 })
.webp({ pageHeight: original.pageHeight / 2 }) .webp({ delay: 100 })
.toBuffer() .toBuffer()
.then(data => sharp(data, { pages: -1 }).metadata()); .then(data => sharp(data, { pages: -1 }).metadata());
assert.strictEqual(updated.pages, original.pages * 2); assert.deepStrictEqual(updated.delay, Array(updated.pages).fill(100));
assert.strictEqual(updated.pageHeight, original.pageHeight / 2);
assert.deepStrictEqual(updated.delay, [...original.delay, ...Array(9).fill(120)]);
}); });
it('should limit animation loop', async () => { it('should limit animation loop', async () => {
@ -216,22 +207,14 @@ describe('WebP', function () {
}); });
}); });
it('should remove animation properties when loading single page', async () => { it('should resize animated image to page height', async () => {
const data = await sharp(fixtures.inputGifAnimatedLoop3) const updated = await sharp(fixtures.inputWebPAnimated, { pages: -1 })
.resize({ height: 570 }) .resize({ height: 570 })
.webp({ effort: 0 }) .webp({ effort: 0 })
.toBuffer(); .toBuffer()
const { size, ...metadata } = await sharp(data).metadata(); .then(data => sharp(data, { pages: -1 }).metadata());
assert.deepStrictEqual(metadata, {
format: 'webp', assert.strictEqual(updated.height, 570 * 9);
width: 740, assert.strictEqual(updated.pageHeight, 570);
height: 570,
space: 'srgb',
channels: 3,
depth: 'uchar',
isProgressive: false,
hasProfile: false,
hasAlpha: false
});
}); });
}); });