Use libvips' new lanczos3 kernel as default for image reduce

Deprecate interpolateWith method, now provided as an option
This commit is contained in:
Lovell Fuller 2016-05-17 22:16:47 +01:00
parent 051d022fc2
commit cdb2894bd9
32 changed files with 292 additions and 658 deletions

View File

@ -118,7 +118,7 @@ Do not process input images where the number of pixels (width * height) exceeds
### Resizing ### Resizing
#### resize([width], [height]) #### resize([width], [height], [options])
Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified. Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified.
@ -126,6 +126,42 @@ Scale output to `width` x `height`. By default, the resized image is cropped to
`height` is the integral Number of pixels high the resultant image should be, between 1 and 16383. Use `null` or `undefined` to auto-scale the height to match the width. `height` is the integral Number of pixels high the resultant image should be, between 1 and 16383. Use `null` or `undefined` to auto-scale the height to match the width.
`options` is an optional Object. If present, it can contain one or more of:
* `options.kernel`, the kernel to use for image reduction, defaulting to `lanczos3`.
* `options.interpolator`, the interpolator to use for image enlargement, defaulting to `bicubic`.
Possible kernels are:
* `cubic`: Use a [Catmull-Rom spline](https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline).
* `lanczos2`: Use a [Lanczos kernel](https://en.wikipedia.org/wiki/Lanczos_resampling#Lanczos_kernel) with `a=2`.
* `lanczos3`: Use a Lanczos kernel with `a=3` (the default).
Possible interpolators are:
* `nearest`: Use [nearest neighbour interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation).
* `bilinear`: Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation), faster than bicubic but with less smooth results.
* `vertexSplitQuadraticBasisSpline`: Use the smoother [VSQBS interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48) to prevent "staircasing" when enlarging.
* `bicubic`: Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation) (the default).
* `locallyBoundedBicubic`: Use [LBB interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100), which prevents some "[acutance](http://en.wikipedia.org/wiki/Acutance)" but typically reduces performance by a factor of 2.
* `nohalo`: Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/), which prevents acutance but typically reduces performance by a factor of 3.
```javascript
sharp(inputBuffer)
.resize(200, 300, {
kernel: sharp.kernel.lanczos2,
interpolator: sharp.interpolator.nohalo
})
.background('white')
.embed()
.toFile('output.tiff')
.then(function() {
// output.tiff is a 200 pixels wide and 300 pixels high image
// containing a lanczos2/nohalo scaled version, embedded on a white canvas,
// of the image data in inputBuffer
});
```
#### crop([option]) #### crop([option])
Crop the resized image to the exact size specified, the default behaviour. Crop the resized image to the exact size specified, the default behaviour.
@ -232,37 +268,6 @@ if its width or height exceeds the geometry specification*".
Ignoring the aspect ratio of the input, stretch the image to the exact `width` and/or `height` provided via `resize`. Ignoring the aspect ratio of the input, stretch the image to the exact `width` and/or `height` provided via `resize`.
#### interpolateWith(interpolator)
Use the given interpolator for image resizing, where `interpolator` is an attribute of the `sharp.interpolator` Object e.g. `sharp.interpolator.bicubic`.
The default interpolator is `bicubic`, providing a general-purpose interpolator that is both fast and of good quality.
Possible interpolators, in order of performance, are:
* `nearest`: Use [nearest neighbour interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation), suitable for image enlargement only.
* `bilinear`: Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation), faster than bicubic but with less smooth results.
* `vertexSplitQuadraticBasisSpline`: Use the smoother [VSQBS interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48) to prevent "staircasing" when enlarging.
* `bicubic`: Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation) (the default).
* `locallyBoundedBicubic`: Use [LBB interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100), which prevents some "[acutance](http://en.wikipedia.org/wiki/Acutance)" but typically reduces performance by a factor of 2.
* `nohalo`: Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/), which prevents acutance but typically reduces performance by a factor of 3.
[Compare the output of these interpolators](https://github.com/lovell/sharp/tree/master/test/interpolators)
```javascript
sharp(inputBuffer)
.resize(200, 300)
.interpolateWith(sharp.interpolator.nohalo)
.background('white')
.embed()
.toFile('output.tiff')
.then(function() {
// output.tiff is a 200 pixels wide and 300 pixels high image
// containing a nohalo scaled version, embedded on a white canvas,
// of the image data in inputBuffer
});
```
### Operations ### Operations
#### extract({ left: left, top: top, width: width, height: height }) #### extract({ left: left, top: top, width: width, height: height })

View File

@ -6,6 +6,11 @@ Requires libvips v8.3.1
#### v0.15.0 - TBD #### v0.15.0 - TBD
* Use libvips' new Lanczos 3 kernel as default for image reduction.
Deprecate interpolateWith method, now provided as a resize option.
[#310](https://github.com/lovell/sharp/issues/310)
[@jcupitt](https://github.com/jcupitt)
* Take advantage of libvips v8.3 features. * Take advantage of libvips v8.3 features.
Add support for libvips' new GIF and SVG loaders. Add support for libvips' new GIF and SVG loaders.
Pre-built binaries now include giflib and librsvg, exclude *magick. Pre-built binaries now include giflib and librsvg, exclude *magick.

View File

@ -74,6 +74,7 @@ var Sharp = function(input, options) {
extendLeft: 0, extendLeft: 0,
extendRight: 0, extendRight: 0,
withoutEnlargement: false, withoutEnlargement: false,
kernel: 'lanczos3',
interpolator: 'bicubic', interpolator: 'bicubic',
// operations // operations
background: [0, 0, 0, 255], background: [0, 0, 0, 255],
@ -480,33 +481,6 @@ Sharp.prototype.threshold = function(threshold) {
return this; return this;
}; };
/*
Set the interpolator to use for the affine transformation
*/
module.exports.interpolator = {
nearest: 'nearest',
bilinear: 'bilinear',
bicubic: 'bicubic',
nohalo: 'nohalo',
locallyBoundedBicubic: 'lbb',
vertexSplitQuadraticBasisSpline: 'vsqbs'
};
Sharp.prototype.interpolateWith = function(interpolator) {
var isValid = false;
for (var key in module.exports.interpolator) {
if (module.exports.interpolator[key] === interpolator) {
isValid = true;
break;
}
}
if (isValid) {
this.options.interpolator = interpolator;
} else {
throw new Error('Invalid interpolator ' + interpolator);
}
return this;
};
/* /*
Darken image pre-resize (1/gamma) and brighten post-resize (gamma). Darken image pre-resize (1/gamma) and brighten post-resize (gamma).
Improves brightness of resized image in non-linear colour spaces. Improves brightness of resized image in non-linear colour spaces.
@ -712,27 +686,75 @@ Sharp.prototype.extend = function(extend) {
return this; return this;
}; };
Sharp.prototype.resize = function(width, height) { // Kernels for reduction
if (!width) { module.exports.kernel = {
this.options.width = -1; cubic: 'cubic',
} else { lanczos2: 'lanczos2',
if (typeof width === 'number' && !Number.isNaN(width) && width % 1 === 0 && width > 0 && width <= maximum.width) { lanczos3: 'lanczos3'
};
// Interpolators for enlargement
module.exports.interpolator = {
nearest: 'nearest',
bilinear: 'bilinear',
bicubic: 'bicubic',
nohalo: 'nohalo',
lbb: 'lbb',
locallyBoundedBicubic: 'lbb',
vsqbs: 'vsqbs',
vertexSplitQuadraticBasisSpline: 'vsqbs'
};
/*
Resize image to width x height pixels
options.kernel is the kernel to use for reductions, default 'lanczos3'
options.interpolator is the interpolator to use for enlargements, default 'bicubic'
*/
Sharp.prototype.resize = function(width, height, options) {
if (isDefined(width)) {
if (isInteger(width) && inRange(width, 1, maximum.width)) {
this.options.width = width; this.options.width = width;
} else { } else {
throw new Error('Invalid width (1 to ' + maximum.width + ') ' + width); throw new Error('Invalid width (1 to ' + maximum.width + ') ' + width);
} }
}
if (!height) {
this.options.height = -1;
} else { } else {
if (typeof height === 'number' && !Number.isNaN(height) && height % 1 === 0 && height > 0 && height <= maximum.height) { this.options.width = -1;
}
if (isDefined(height)) {
if (isInteger(height) && inRange(height, 1, maximum.height)) {
this.options.height = height; this.options.height = height;
} else { } else {
throw new Error('Invalid height (1 to ' + maximum.height + ') ' + height); throw new Error('Invalid height (1 to ' + maximum.height + ') ' + height);
} }
} else {
this.options.height = -1;
}
if (isObject(options)) {
// Kernel
if (isDefined(options.kernel)) {
if (isString(module.exports.kernel[options.kernel])) {
this.options.kernel = module.exports.kernel[options.kernel];
} else {
throw new Error('Invalid kernel ' + options.kernel);
}
}
// Interpolator
if (isDefined(options.interpolator)) {
if (isString(module.exports.interpolator[options.interpolator])) {
this.options.interpolator = module.exports.interpolator[options.interpolator];
} else {
throw new Error('Invalid interpolator ' + options.interpolator);
}
}
} }
return this; return this;
}; };
Sharp.prototype.interpolateWith = util.deprecate(function(interpolator) {
return this.resize(
this.options.width > 0 ? this.options.width : null,
this.options.height > 0 ? this.options.height : null,
{ interpolator: interpolator }
);
}, 'interpolateWith: Please use resize(w, h, { interpolator: ... }) instead');
/* /*
Limit the total number of pixels for input images Limit the total number of pixels for input images

View File

@ -250,4 +250,22 @@ namespace sharp {
return image.hist_find().hist_entropy(); return image.hist_find().hist_entropy();
} }
/*
Insert a tile cache to prevent over-computation of any previous operations in the pipeline
*/
VImage TileCache(VImage image, double const factor) {
int tile_width;
int tile_height;
int scanline_count;
vips_get_tile_size(image.get_image(), &tile_width, &tile_height, &scanline_count);
double const need_lines = 1.2 * scanline_count / factor;
return image.tilecache(VImage::option()
->set("tile_width", image.width())
->set("tile_height", 10)
->set("max_tiles", static_cast<int>(round(1.0 + need_lines / 10.0)))
->set("access", VIPS_ACCESS_SEQUENTIAL)
->set("threaded", TRUE)
);
}
} // namespace sharp } // namespace sharp

View File

@ -44,6 +44,11 @@ namespace sharp {
*/ */
double Entropy(VImage image); double Entropy(VImage image);
/*
Insert a tile cache to prevent over-computation of any previous operations in the pipeline
*/
VImage TileCache(VImage image, double const factor);
} // namespace sharp } // namespace sharp
#endif // SRC_OPERATIONS_H_ #endif // SRC_OPERATIONS_H_

View File

@ -50,6 +50,7 @@ using sharp::Gamma;
using sharp::Blur; using sharp::Blur;
using sharp::Sharpen; using sharp::Sharpen;
using sharp::EntropyCrop; using sharp::EntropyCrop;
using sharp::TileCache;
using sharp::ImageType; using sharp::ImageType;
using sharp::ImageTypeId; using sharp::ImageTypeId;
@ -215,17 +216,13 @@ class PipelineWorker : public AsyncWorker {
std::swap(inputWidth, inputHeight); std::swap(inputWidth, inputHeight);
} }
// Get window size of interpolator, used for determining shrink vs affine
VInterpolate interpolator = VInterpolate::new_from_name(baton->interpolator.data());
int interpolatorWindowSize = vips_interpolate_get_window_size(interpolator.get_interpolate());
// Scaling calculations // Scaling calculations
double xfactor = 1.0; double xfactor = 1.0;
double yfactor = 1.0; double yfactor = 1.0;
if (baton->width > 0 && baton->height > 0) { if (baton->width > 0 && baton->height > 0) {
// Fixed width and height // Fixed width and height
xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width); xfactor = static_cast<double>(inputWidth) / (static_cast<double>(baton->width) + 0.1);
yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height); yfactor = static_cast<double>(inputHeight) / (static_cast<double>(baton->height) + 0.1);
switch (baton->canvas) { switch (baton->canvas) {
case Canvas::CROP: case Canvas::CROP:
xfactor = std::min(xfactor, yfactor); xfactor = std::min(xfactor, yfactor);
@ -262,7 +259,7 @@ class PipelineWorker : public AsyncWorker {
} }
} else if (baton->width > 0) { } else if (baton->width > 0) {
// Fixed width // Fixed width
xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width); xfactor = static_cast<double>(inputWidth) / (static_cast<double>(baton->width) + 0.1);
if (baton->canvas == Canvas::IGNORE_ASPECT) { if (baton->canvas == Canvas::IGNORE_ASPECT) {
baton->height = inputHeight; baton->height = inputHeight;
} else { } else {
@ -272,7 +269,7 @@ class PipelineWorker : public AsyncWorker {
} }
} else if (baton->height > 0) { } else if (baton->height > 0) {
// Fixed height // Fixed height
yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height); yfactor = static_cast<double>(inputHeight) / (static_cast<double>(baton->height) + 0.1);
if (baton->canvas == Canvas::IGNORE_ASPECT) { if (baton->canvas == Canvas::IGNORE_ASPECT) {
baton->width = inputWidth; baton->width = inputWidth;
} else { } else {
@ -287,12 +284,12 @@ class PipelineWorker : public AsyncWorker {
} }
// Calculate integral box shrink // Calculate integral box shrink
int xshrink = CalculateShrink(xfactor, interpolatorWindowSize); int xshrink = std::max(1, static_cast<int>(floor(xfactor)));
int yshrink = CalculateShrink(yfactor, interpolatorWindowSize); int yshrink = std::max(1, static_cast<int>(floor(yfactor)));
// Calculate residual float affine transformation // Calculate residual float affine transformation
double xresidual = CalculateResidual(xshrink, xfactor); double xresidual = static_cast<double>(xshrink) / xfactor;
double yresidual = CalculateResidual(yshrink, yfactor); double yresidual = static_cast<double>(yshrink) / yfactor;
// Do not enlarge the output if the input width *or* height // Do not enlarge the output if the input width *or* height
// are already less than the required dimensions // are already less than the required dimensions
@ -335,10 +332,10 @@ class PipelineWorker : public AsyncWorker {
// Recalculate integral shrink and double residual // Recalculate integral shrink and double residual
xfactor = std::max(xfactor, 1.0); xfactor = std::max(xfactor, 1.0);
yfactor = std::max(yfactor, 1.0); yfactor = std::max(yfactor, 1.0);
xshrink = CalculateShrink(xfactor, interpolatorWindowSize); xshrink = std::max(1, static_cast<int>(floor(xfactor)));
yshrink = CalculateShrink(yfactor, interpolatorWindowSize); yshrink = std::max(1, static_cast<int>(floor(yfactor)));
xresidual = CalculateResidual(xshrink, xfactor); xresidual = static_cast<double>(xshrink) / xfactor;
yresidual = CalculateResidual(yshrink, yfactor); yresidual = static_cast<double>(yshrink) / yfactor;
// Reload input using shrink-on-load // Reload input using shrink-on-load
VOption *option = VImage::option()->set("shrink", shrink_on_load); VOption *option = VImage::option()->set("shrink", shrink_on_load);
if (baton->bufferInLength > 1) { if (baton->bufferInLength > 1) {
@ -418,7 +415,12 @@ class PipelineWorker : public AsyncWorker {
} }
if (xshrink > 1 || yshrink > 1) { if (xshrink > 1 || yshrink > 1) {
image = image.shrink(xshrink, yshrink); if (yshrink > 1) {
image = image.shrinkv(yshrink);
}
if (xshrink > 1) {
image = image.shrinkh(xshrink);
}
// Recalculate residual float based on dimensions of required vs shrunk images // Recalculate residual float based on dimensions of required vs shrunk images
int shrunkWidth = image.width(); int shrunkWidth = image.width();
int shrunkHeight = image.height(); int shrunkHeight = image.height();
@ -466,32 +468,42 @@ class PipelineWorker : public AsyncWorker {
image = image.premultiply(VImage::option()->set("max_alpha", maxAlpha)); image = image.premultiply(VImage::option()->set("max_alpha", maxAlpha));
} }
// Use affine transformation with the remaining float part // Use affine increase or kernel reduce with the remaining float part
if (shouldAffineTransform) { if (shouldAffineTransform) {
// Use average of x and y residuals to compute sigma for Gaussian blur // Insert tile cache to prevent over-computation of previous operations
double residual = (xresidual + yresidual) / 2.0;
// Apply Gaussian blur before large affine reductions
if (residual < 1.0) {
// Calculate standard deviation
double sigma = ((1.0 / residual) - 0.4) / 3.0;
if (sigma >= 0.3) {
// Sequential input requires a small linecache before use of convolution
if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL) { if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL) {
image = image.linecache(VImage::option() image = TileCache(image, yresidual);
->set("access", VIPS_ACCESS_SEQUENTIAL) }
->set("tile_height", 1) // Perform kernel-based reduction
->set("threaded", TRUE) if (yresidual < 1.0 || xresidual < 1.0) {
VipsKernel kernel = static_cast<VipsKernel>(
vips_enum_from_nick(nullptr, VIPS_TYPE_KERNEL, baton->kernel.data())
); );
if (kernel != VIPS_KERNEL_CUBIC && kernel != VIPS_KERNEL_LANCZOS2 && kernel != VIPS_KERNEL_LANCZOS3) {
throw VError("Unknown kernel");
} }
// Apply Gaussian blur if (yresidual < 1.0) {
image = image.gaussblur(sigma); image = image.reducev(1.0 / yresidual, VImage::option()->set("kernel", kernel));
}
if (xresidual < 1.0) {
image = image.reduceh(1.0 / xresidual, VImage::option()->set("kernel", kernel));
} }
} }
// Perform affine transformation // Perform affine enlargement
image = image.affine({xresidual, 0.0, 0.0, yresidual}, VImage::option() if (yresidual > 1.0 || xresidual > 1.0) {
VInterpolate interpolator = VInterpolate::new_from_name(baton->interpolator.data());
if (yresidual > 1.0) {
image = image.affine({1.0, 0.0, 0.0, yresidual}, VImage::option()
->set("interpolate", interpolator) ->set("interpolate", interpolator)
); );
} }
if (xresidual > 1.0) {
image = image.affine({xresidual, 0.0, 0.0, 1.0}, VImage::option()
->set("interpolate", interpolator)
);
}
}
}
// Rotate // Rotate
if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) { if (!baton->rotateBeforePreExtract && rotation != VIPS_ANGLE_D0) {
@ -943,30 +955,6 @@ class PipelineWorker : public AsyncWorker {
return std::make_tuple(rotate, flip, flop); return std::make_tuple(rotate, flip, flop);
} }
/*
Calculate integral shrink given factor and interpolator window size
*/
int CalculateShrink(double factor, int interpolatorWindowSize) {
int shrink = 1;
if (factor >= 2.0 && trunc(factor) != factor && interpolatorWindowSize > 3) {
// Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic
shrink = static_cast<int>(floor(factor * 3.0 / interpolatorWindowSize));
} else {
shrink = static_cast<int>(floor(factor));
}
if (shrink < 1) {
shrink = 1;
}
return shrink;
}
/*
Calculate residual given shrink and factor
*/
double CalculateResidual(int shrink, double factor) {
return static_cast<double>(shrink) / factor;
}
/* /*
Clear all thread-local data. Clear all thread-local data.
*/ */
@ -1058,6 +1046,7 @@ NAN_METHOD(pipeline) {
// Resize options // Resize options
baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement"); baton->withoutEnlargement = attrAs<bool>(options, "withoutEnlargement");
baton->crop = attrAs<int32_t>(options, "crop"); baton->crop = attrAs<int32_t>(options, "crop");
baton->kernel = attrAsStr(options, "kernel");
baton->interpolator = attrAsStr(options, "interpolator"); baton->interpolator = attrAsStr(options, "interpolator");
// Operators // Operators
baton->flatten = attrAs<bool>(options, "flatten"); baton->flatten = attrAs<bool>(options, "flatten");

View File

@ -46,6 +46,7 @@ struct PipelineBaton {
int channels; int channels;
Canvas canvas; Canvas canvas;
int crop; int crop;
std::string kernel;
std::string interpolator; std::string interpolator;
double background[4]; double background[4];
bool flatten; bool flatten;

View File

@ -10,10 +10,10 @@
"devDependencies": { "devDependencies": {
"async": "^1.5.2", "async": "^1.5.2",
"benchmark": "^2.1.0", "benchmark": "^2.1.0",
"gm": "^1.21.0", "gm": "^1.22.0",
"imagemagick": "^0.1.3", "imagemagick": "^0.1.3",
"imagemagick-native": "^1.9.2", "imagemagick-native": "^1.9.2",
"jimp": "^0.2.20", "jimp": "^0.2.24",
"lwip": "^0.0.8", "lwip": "^0.0.8",
"semver": "^5.1.0" "semver": "^5.1.0"
}, },

View File

@ -30,18 +30,15 @@ var fixtures = require('../fixtures');
var width = 720; var width = 720;
var height = 480; var height = 480;
var magickFilterBilinear = 'Triangle';
var magickFilterBicubic = 'Lanczos';
// Disable libvips cache to ensure tests are as fair as they can be // Disable libvips cache to ensure tests are as fair as they can be
sharp.cache(false); sharp.cache(false);
// Enable use of SIMD // Enable use of SIMD
sharp.simd(true); sharp.simd(true);
async.series({ async.series({
'jpeg-linear': function(callback) { 'jpeg': function(callback) {
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg); var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
var jpegSuite = new Benchmark.Suite('jpeg-linear'); var jpegSuite = new Benchmark.Suite('jpeg');
// jimp // jimp
jpegSuite.add('jimp-buffer-buffer', { jpegSuite.add('jimp-buffer-buffer', {
defer: true, defer: true,
@ -93,7 +90,7 @@ async.series({
if (err) { if (err) {
throw err; throw err;
} }
image.resize(width, height, 'linear', function (err, image) { image.resize(width, height, 'lanczos', function (err, image) {
if (err) { if (err) {
throw err; throw err;
} }
@ -113,7 +110,7 @@ async.series({
if (err) { if (err) {
throw err; throw err;
} }
image.resize(width, height, 'linear', function (err, image) { image.resize(width, height, 'lanczos', function (err, image) {
if (err) { if (err) {
throw err; throw err;
} }
@ -140,7 +137,7 @@ async.series({
width: width, width: width,
height: height, height: height,
format: 'jpg', format: 'jpg',
filter: magickFilterBilinear filter: 'Lanczos'
}, function(err) { }, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -161,7 +158,7 @@ async.series({
width: width, width: width,
height: height, height: height,
format: 'JPEG', format: 'JPEG',
filter: magickFilterBilinear filter: 'Lanczos'
}, function (err, buffer) { }, function (err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -179,7 +176,7 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
gm(inputJpgBuffer) gm(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.filter(magickFilterBilinear) .filter('Lanczos')
.quality(80) .quality(80)
.write(fixtures.outputJpg, function (err) { .write(fixtures.outputJpg, function (err) {
if (err) { if (err) {
@ -194,7 +191,7 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
gm(inputJpgBuffer) gm(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.filter(magickFilterBilinear) .filter('Lanczos')
.quality(80) .quality(80)
.toBuffer(function (err, buffer) { .toBuffer(function (err, buffer) {
if (err) { if (err) {
@ -210,7 +207,7 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputJpg) gm(fixtures.inputJpg)
.resize(width, height) .resize(width, height)
.filter(magickFilterBilinear) .filter('Lanczos')
.quality(80) .quality(80)
.write(fixtures.outputJpg, function (err) { .write(fixtures.outputJpg, function (err) {
if (err) { if (err) {
@ -225,7 +222,7 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputJpg) gm(fixtures.inputJpg)
.resize(width, height) .resize(width, height)
.filter(magickFilterBilinear) .filter('Lanczos')
.quality(80) .quality(80)
.toBuffer(function (err, buffer) { .toBuffer(function (err, buffer) {
if (err) { if (err) {
@ -243,7 +240,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toFile(fixtures.outputJpg, function(err) { .toFile(fixtures.outputJpg, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -257,7 +253,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -272,7 +267,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toFile(fixtures.outputJpg, function(err) { .toFile(fixtures.outputJpg, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -290,8 +284,7 @@ async.series({
deferred.resolve(); deferred.resolve();
}); });
var pipeline = sharp() var pipeline = sharp()
.resize(width, height) .resize(width, height);
.interpolateWith(sharp.interpolator.bilinear);
readable.pipe(pipeline).pipe(writable); readable.pipe(pipeline).pipe(writable);
} }
}).add('sharp-file-buffer', { }).add('sharp-file-buffer', {
@ -299,7 +292,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -314,19 +306,27 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer() .toBuffer()
.then(function(buffer) { .then(function(buffer) {
assert.notStrictEqual(null, buffer); assert.notStrictEqual(null, buffer);
deferred.resolve(); deferred.resolve();
}); });
} }
}).add('sharp-sharpen-mild', { }).on('cycle', function(event) {
console.log('jpeg ' + String(event.target));
}).on('complete', function() {
callback(null, this.filter('fastest').map('name'));
}).run();
},
// Effect of applying operations
operations: function(callback) {
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
var operationsSuite = new Benchmark.Suite('operations');
operationsSuite.add('sharp-sharpen-mild', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.sharpen() .sharpen()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -342,7 +342,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.sharpen(3, 1, 3) .sharpen(3, 1, 3)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -358,7 +357,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.blur() .blur()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -374,7 +372,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.blur(3) .blur(3)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -390,7 +387,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.gamma() .gamma()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -406,7 +402,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.normalise() .normalise()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -422,7 +417,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.greyscale() .greyscale()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -438,7 +432,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.gamma() .gamma()
.greyscale() .greyscale()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
@ -455,7 +448,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.progressive() .progressive()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -471,7 +463,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.withoutChromaSubsampling() .withoutChromaSubsampling()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -487,7 +478,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.rotate(90) .rotate(90)
.interpolateWith(sharp.interpolator.bilinear)
.resize(width, height) .resize(width, height)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -503,8 +493,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp.simd(false); sharp.simd(false);
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.rotate(90)
.interpolateWith(sharp.interpolator.bilinear)
.resize(width, height) .resize(width, height)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
sharp.simd(true); sharp.simd(true);
@ -520,9 +508,8 @@ async.series({
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.sequentialRead() .sequentialRead()
.resize(width, height)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -533,189 +520,19 @@ async.series({
}); });
} }
}).on('cycle', function(event) { }).on('cycle', function(event) {
console.log('jpeg-linear ' + String(event.target)); console.log('operations ' + String(event.target));
}).on('complete', function() { }).on('complete', function() {
callback(null, this.filter('fastest').map('name')); callback(null, this.filter('fastest').map('name'));
}).run(); }).run();
}, },
// Comparitive speed of kernels
'jpeg-cubic': function(callback) { kernels: function(callback) {
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg); var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
var jpegSuite = new Benchmark.Suite('jpeg-cubic'); (new Benchmark.Suite('kernels')).add('sharp-cubic', {
// lwip
if (typeof lwip !== 'undefined') {
jpegSuite.add('lwip-file-file', {
defer: true,
fn: function(deferred) {
lwip.open(fixtures.inputJpg, function (err, image) {
if (err) {
throw err;
}
image.resize(width, height, 'lanczos', function (err, image) {
if (err) {
throw err;
}
image.writeFile(fixtures.outputJpg, {quality: 80}, function (err) {
if (err) {
throw err;
}
deferred.resolve();
});
});
});
}
}).add('lwip-buffer-buffer', {
defer: true,
fn: function(deferred) {
lwip.open(inputJpgBuffer, 'jpg', function (err, image) {
if (err) {
throw err;
}
image.resize(width, height, 'lanczos', function (err, image) {
if (err) {
throw err;
}
image.toBuffer('jpg', {quality: 80}, function (err, buffer) {
if (err) {
throw err;
}
assert.notStrictEqual(null, buffer);
deferred.resolve();
});
});
});
}
});
}
// imagemagick
jpegSuite.add('imagemagick-file-file', {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: fixtures.inputJpg,
dstPath: fixtures.outputJpg,
quality: 0.8,
width: width,
height: height,
format: 'jpg',
filter: magickFilterBicubic
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
});
// imagemagick-native
if (typeof imagemagickNative !== 'undefined') {
jpegSuite.add('imagemagick-native-buffer-buffer', {
defer: true,
fn: function(deferred) {
imagemagickNative.convert({
srcData: inputJpgBuffer,
quality: 80,
width: width,
height: height,
format: 'JPEG',
filter: magickFilterBicubic
}, function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
});
}
// gm
jpegSuite.add('gm-buffer-file', {
defer: true,
fn: function(deferred) {
gm(inputJpgBuffer)
.resize(width, height)
.filter(magickFilterBicubic)
.quality(80)
.write(fixtures.outputJpg, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('gm-buffer-buffer', {
defer: true,
fn: function(deferred) {
gm(inputJpgBuffer)
.resize(width, height)
.filter(magickFilterBicubic)
.quality(80)
.toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('gm-file-file', {
defer: true,
fn: function(deferred) {
gm(fixtures.inputJpg)
.resize(width, height)
.filter(magickFilterBicubic)
.quality(80)
.write(fixtures.outputJpg, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('gm-file-buffer', {
defer: true,
fn: function(deferred) {
gm(fixtures.inputJpg)
.resize(width, height)
.filter(magickFilterBicubic)
.quality(80)
.toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
});
// sharp
jpegSuite.add('sharp-buffer-file', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height, { kernel: 'cubic' })
.interpolateWith(sharp.interpolator.bicubic)
.toFile(fixtures.outputJpg, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-buffer-buffer', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer)
.resize(width, height)
.interpolateWith(sharp.interpolator.bicubic)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -725,39 +542,11 @@ async.series({
} }
}); });
} }
}).add('sharp-file-file', { }).add('sharp-lanczos2', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(fixtures.inputJpg) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height, { kernel: 'lanczos2' })
.interpolateWith(sharp.interpolator.bicubic)
.toFile(fixtures.outputJpg, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-stream-stream', {
defer: true,
fn: function(deferred) {
var readable = fs.createReadStream(fixtures.inputJpg);
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
deferred.resolve();
});
var pipeline = sharp()
.resize(width, height)
.interpolateWith(sharp.interpolator.bicubic);
readable.pipe(pipeline).pipe(writable);
}
}).add('sharp-file-buffer', {
defer: true,
fn: function(deferred) {
sharp(fixtures.inputJpg)
.resize(width, height)
.interpolateWith(sharp.interpolator.bicubic)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -767,109 +556,11 @@ async.series({
} }
}); });
} }
}).add('sharp-promise', { }).add('sharp-lanczos3', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer) sharp(inputJpgBuffer)
.resize(width, height) .resize(width, height, { kernel: 'lanczos3' })
.interpolateWith(sharp.interpolator.bicubic)
.toBuffer()
.then(function(buffer) {
assert.notStrictEqual(null, buffer);
deferred.resolve();
});
}
}).on('cycle', function(event) {
console.log('jpeg-cubic ' + String(event.target));
}).on('complete', function() {
callback(null, this.filter('fastest').map('name'));
}).run();
},
// Comparitive speed of pixel interpolators
interpolators: function(callback) {
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
(new Benchmark.Suite('interpolators')).add('sharp-nearest-neighbour', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer)
.resize(width, height)
.interpolateWith(sharp.interpolator.nearest)
.toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-bilinear', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer)
.resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-vertexSplitQuadraticBasisSpline', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer)
.resize(width, height)
.interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline)
.toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-bicubic', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer)
.resize(width, height)
.interpolateWith(sharp.interpolator.bicubic)
.toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-locallyBoundedBicubic', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer)
.resize(width, height)
.interpolateWith(sharp.interpolator.locallyBoundedBicubic)
.toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-nohalo', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer)
.resize(width, height)
.interpolateWith(sharp.interpolator.nohalo)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -880,12 +571,12 @@ async.series({
}); });
} }
}).on('cycle', function(event) { }).on('cycle', function(event) {
console.log('interpolators ' + String(event.target)); console.log('kernels ' + String(event.target));
}).on('complete', function() { }).on('complete', function() {
callback(null, this.filter('fastest').map('name')); callback(null, this.filter('fastest').map('name'));
}).run(); }).run();
}, },
// PNG
png: function(callback) { png: function(callback) {
var inputPngBuffer = fs.readFileSync(fixtures.inputPng); var inputPngBuffer = fs.readFileSync(fixtures.inputPng);
var pngSuite = new Benchmark.Suite('png'); var pngSuite = new Benchmark.Suite('png');
@ -938,7 +629,7 @@ async.series({
if (err) { if (err) {
throw err; throw err;
} }
image.resize(width, height, 'linear', function (err, image) { image.resize(width, height, 'lanczos', function (err, image) {
if (err) { if (err) {
throw err; throw err;
} }
@ -963,7 +654,7 @@ async.series({
dstPath: fixtures.outputPng, dstPath: fixtures.outputPng,
width: width, width: width,
height: height, height: height,
filter: magickFilterBilinear filter: 'Lanczos'
}, function(err) { }, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -983,7 +674,7 @@ async.series({
width: width, width: width,
height: height, height: height,
format: 'PNG', format: 'PNG',
filter: magickFilterBilinear filter: 'Lanczos'
}); });
deferred.resolve(); deferred.resolve();
} }
@ -995,7 +686,7 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputPng) gm(fixtures.inputPng)
.resize(width, height) .resize(width, height)
.filter(magickFilterBilinear) .filter('Lanczos')
.write(fixtures.outputPng, function (err) { .write(fixtures.outputPng, function (err) {
if (err) { if (err) {
throw err; throw err;
@ -1009,7 +700,7 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputPng) gm(fixtures.inputPng)
.resize(width, height) .resize(width, height)
.filter(magickFilterBilinear) .filter('Lanczos')
.toBuffer(function (err, buffer) { .toBuffer(function (err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -1026,7 +717,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputPngBuffer) sharp(inputPngBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toFile(fixtures.outputPng, function(err) { .toFile(fixtures.outputPng, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -1040,7 +730,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputPngBuffer) sharp(inputPngBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -1055,7 +744,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toFile(fixtures.outputPng, function(err) { .toFile(fixtures.outputPng, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -1069,7 +757,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(fixtures.inputPng) sharp(fixtures.inputPng)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -1084,7 +771,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputPngBuffer) sharp(inputPngBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.progressive() .progressive()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -1100,7 +786,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputPngBuffer) sharp(inputPngBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.withoutAdaptiveFiltering() .withoutAdaptiveFiltering()
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
@ -1118,7 +803,7 @@ async.series({
callback(null, this.filter('fastest').map('name')); callback(null, this.filter('fastest').map('name'));
}).run(); }).run();
}, },
// WebP
webp: function(callback) { webp: function(callback) {
var inputWebPBuffer = fs.readFileSync(fixtures.inputWebP); var inputWebPBuffer = fs.readFileSync(fixtures.inputWebP);
(new Benchmark.Suite('webp')).add('sharp-buffer-file', { (new Benchmark.Suite('webp')).add('sharp-buffer-file', {
@ -1126,7 +811,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputWebPBuffer) sharp(inputWebPBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toFile(fixtures.outputWebP, function(err) { .toFile(fixtures.outputWebP, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -1140,7 +824,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(inputWebPBuffer) sharp(inputWebPBuffer)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
@ -1155,7 +838,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(fixtures.inputWebP) sharp(fixtures.inputWebP)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toFile(fixtures.outputWebP, function(err) { .toFile(fixtures.outputWebP, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -1169,7 +851,6 @@ async.series({
fn: function(deferred) { fn: function(deferred) {
sharp(fixtures.inputWebp) sharp(fixtures.inputWebp)
.resize(width, height) .resize(width, height)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, buffer) { .toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;

View File

@ -8,14 +8,12 @@ var Benchmark = require('benchmark');
var sharp = require('../../index'); var sharp = require('../../index');
var fixtures = require('../fixtures'); var fixtures = require('../fixtures');
sharp.cache(false);
sharp.simd(true); sharp.simd(true);
var min = 320; var min = 320;
var max = 960; var max = 960;
// Nearest equivalent to bilinear
var magickFilter = 'Triangle';
var randomDimension = function() { var randomDimension = function() {
return Math.ceil(Math.random() * (max - min) + min); return Math.ceil(Math.random() * (max - min) + min);
}; };
@ -30,7 +28,7 @@ new Benchmark.Suite('random').add('imagemagick', {
width: randomDimension(), width: randomDimension(),
height: randomDimension(), height: randomDimension(),
format: 'jpg', format: 'jpg',
filter: magickFilter filter: 'Lanczos'
}, function(err) { }, function(err) {
if (err) { if (err) {
throw err; throw err;
@ -44,7 +42,7 @@ new Benchmark.Suite('random').add('imagemagick', {
fn: function(deferred) { fn: function(deferred) {
gm(fixtures.inputJpg) gm(fixtures.inputJpg)
.resize(randomDimension(), randomDimension()) .resize(randomDimension(), randomDimension())
.filter(magickFilter) .filter('Lanczos')
.quality(80) .quality(80)
.toBuffer(function (err, buffer) { .toBuffer(function (err, buffer) {
if (err) { if (err) {
@ -58,7 +56,9 @@ new Benchmark.Suite('random').add('imagemagick', {
}).add('sharp', { }).add('sharp', {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(fixtures.inputJpg).resize(randomDimension(), randomDimension()).toBuffer(function(err, buffer) { sharp(fixtures.inputJpg)
.resize(randomDimension(), randomDimension())
.toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@ -72,5 +72,4 @@ new Benchmark.Suite('random').add('imagemagick', {
}).on('complete', function() { }).on('complete', function() {
var winner = this.filter('fastest').map('name'); var winner = this.filter('fastest').map('name');
assert.strictEqual('sharp', String(winner), 'sharp was slower than ' + winner); assert.strictEqual('sharp', String(winner), 'sharp was slower than ' + winner);
console.dir(sharp.cache());
}).run(); }).run();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 782 B

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 B

After

Width:  |  Height:  |  Size: 999 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 B

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 B

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -18,7 +18,6 @@ var fingerprint = function(image, callback) {
.normalise() .normalise()
.resize(9, 8) .resize(9, 8)
.ignoreAspectRatio() .ignoreAspectRatio()
.interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline)
.raw() .raw()
.toBuffer(function(err, data) { .toBuffer(function(err, data) {
if (err) { if (err) {

View File

@ -1,48 +0,0 @@
# Interpolators
[Photo](https://www.flickr.com/photos/aotaro/21978966091) by
[aotaro](https://www.flickr.com/photos/aotaro/) is licensed under
[CC BY 2.0](https://creativecommons.org/licenses/by/2.0/).
The following examples take the 4608x3072px original image
and resize to 480x320px using various interpolators.
To fetch the original 4608x3072px image and
generate the interpolator sample images:
```sh
curl -O https://farm6.staticflickr.com/5682/21978966091_b421afe866_o.jpg
node generate.js
```
## Nearest neighbour
![Nearest neighbour interpolation](nearest.jpg)
## Bilinear
![Bilinear interpolation](bilinear.jpg)
## Bicubic
![Bicubic interpolation](bicubic.jpg)
## Locally bounded bicubic
![Locally bounded bicubic interpolation](lbb.jpg)
## Vertex-split quadratic b-splines (VSQBS)
![Vertex-split quadratic b-splines interpolation](vsqbs.jpg)
## Nohalo
![Nohalo interpolation](nohalo.jpg)
## GraphicsMagick
![GraphicsMagick](gm.jpg)
```sh
gm convert 21978966091_b421afe866_o.jpg -resize 480x320^ -gravity center -extent 480x320 -quality 95 -strip -define jpeg:optimize-coding=true gm.jpg
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@ -1,11 +0,0 @@
'use strict';
['nearest', 'bilinear', 'bicubic', 'vsqbs', 'lbb', 'nohalo'].forEach(function(interpolator) {
require('../../')('21978966091_b421afe866_o.jpg')
.resize(480, 320)
.interpolateWith(interpolator)
.quality(95)
.toFile(interpolator + '.jpg', function(err) {
if (err) throw err;
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -45,15 +45,17 @@ describe('Alpha transparency', function() {
}); });
it('Flatten 16-bit PNG with transparency to orange', function(done) { it('Flatten 16-bit PNG with transparency to orange', function(done) {
var output = fixtures.path('output.flatten-rgb16-orange.jpg');
sharp(fixtures.inputPngWithTransparency16bit) sharp(fixtures.inputPngWithTransparency16bit)
.flatten() .flatten()
.background({r: 255, g: 102, b: 0}) .background({r: 255, g: 102, b: 0})
.toBuffer(function(err, data, info) { .toFile(output, function(err, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, info.size > 0); assert.strictEqual(true, info.size > 0);
assert.strictEqual(32, info.width); assert.strictEqual(32, info.width);
assert.strictEqual(32, info.height); assert.strictEqual(32, info.height);
fixtures.assertSimilar(fixtures.expected('flatten-rgb16-orange.jpg'), data, { threshold: 6 }, done); fixtures.assertMaxColourDistance(output, fixtures.expected('flatten-rgb16-orange.jpg'), 25);
done();
}); });
}); });

View File

@ -57,7 +57,7 @@ describe('Clone', function() {
var rotator = sharp().rotate(90); var rotator = sharp().rotate(90);
// Cloned instances with differing dimensions // Cloned instances with differing dimensions
rotator.clone().resize(320, 240).pipe(writable1); rotator.clone().resize(320, 240).pipe(writable1);
rotator.clone().resize(100).pipe(writable2); rotator.clone().resize(100, 122).pipe(writable2);
// Go // Go
fs.createReadStream(fixtures.inputJpg).pipe(rotator); fs.createReadStream(fixtures.inputJpg).pipe(rotator);
}); });

View File

@ -5,100 +5,67 @@ var assert = require('assert');
var sharp = require('../../index'); var sharp = require('../../index');
var fixtures = require('../fixtures'); var fixtures = require('../fixtures');
describe('Interpolation', function() { describe('Interpolators and kernels', function() {
it('nearest neighbour', function(done) { describe('Reducers', function() {
[
sharp.kernel.cubic,
sharp.kernel.lanczos2,
sharp.kernel.lanczos3
].forEach(function(kernel) {
it(kernel, function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, null, { kernel: kernel })
.interpolateWith(sharp.interpolator.nearest)
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); fixtures.assertSimilar(fixtures.inputJpg, data, done);
done(); });
});
}); });
}); });
it('bilinear', function(done) { describe('Enlargers', function() {
[
sharp.interpolator.nearest,
sharp.interpolator.bilinear,
sharp.interpolator.bicubic,
sharp.interpolator.nohalo,
sharp.interpolator.locallyBoundedBicubic,
sharp.interpolator.vertexSplitQuadraticBasisSpline
].forEach(function(interpolator) {
it(interpolator, function(done) {
sharp(fixtures.inputJpg) sharp(fixtures.inputJpg)
.resize(320, 240) .resize(320, null, { interpolator: interpolator })
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format); assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height); fixtures.assertSimilar(fixtures.inputJpg, data, done);
done(); });
});
}); });
}); });
it('bicubic', function(done) { it('unknown kernel throws', function() {
sharp(fixtures.inputJpg) assert.throws(function() {
.resize(320, 240) sharp().resize(null, null, { kernel: 'unknown' });
.interpolateWith(sharp.interpolator.bicubic)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
}); });
}); });
it('nohalo', function(done) { it('unknown interpolator throws', function() {
sharp(fixtures.inputJpg) assert.throws(function() {
.resize(320, 240) sharp().resize(null, null, { interpolator: 'unknown' });
.interpolateWith(sharp.interpolator.nohalo)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
}); });
}); });
it('locally bounded bicubic (LBB)', function(done) { describe('deprecated interpolateWith method still works', function() {
sharp(fixtures.inputJpg) it('resize then interpolateWith', function() {
.resize(320, 240) sharp().resize(1, 1).interpolateWith('bicubic');
.interpolateWith(sharp.interpolator.locallyBoundedBicubic) });
.toBuffer(function(err, data, info) { it('interpolateWith then resize', function() {
if (err) throw err; sharp().interpolateWith('bicubic').resize(1, 1);
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
}); });
}); });
it('vertex split quadratic basis spline (VSQBS)', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('unknown interpolator throws', function(done) {
var isValid = false;
try {
sharp().interpolateWith('nonexistant');
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
}); });

View File

@ -1028,14 +1028,14 @@ describe('Input/output', function() {
it('Info event data', function(done) { it('Info event data', function(done) {
var readable = fs.createReadStream(fixtures.inputJPGBig); var readable = fs.createReadStream(fixtures.inputJPGBig);
var inPipeline = sharp() var inPipeline = sharp()
.resize(840) .resize(840, 472)
.raw() .raw()
.on('info', function(info) { .on('info', function(info) {
assert.strictEqual(840, info.width); assert.strictEqual(840, info.width);
assert.strictEqual(472, info.height); assert.strictEqual(472, info.height);
assert.strictEqual(3, info.channels); assert.strictEqual(3, info.channels);
}); });
var badPipeline = sharp(null, {raw: {width: 840, height: 473, channels: 3}}) var badPipeline = sharp(null, {raw: {width: 840, height: 500, channels: 3}})
.toFormat('jpeg') .toFormat('jpeg')
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
assert.strictEqual(err.message.indexOf('memory area too small') > 0, true); assert.strictEqual(err.message.indexOf('memory area too small') > 0, true);
@ -1047,7 +1047,7 @@ describe('Input/output', function() {
done(); done();
}); });
inPipeline = sharp() inPipeline = sharp()
.resize(840) .resize(840, 472)
.raw(); .raw();
readable.pipe(inPipeline).pipe(goodPipeline); readable.pipe(inPipeline).pipe(goodPipeline);
}); });

View File

@ -96,7 +96,7 @@ describe('Threshold', function() {
.threshold() .threshold()
.toBuffer(function(err, data, info) { .toBuffer(function(err, data, info) {
assert.strictEqual('webp', info.format); assert.strictEqual('webp', info.format);
fixtures.assertSimilar(fixtures.expected('threshold-128-transparency.webp'), data, { threshold: 14 }, done); fixtures.assertSimilar(fixtures.expected('threshold-128-transparency.webp'), data, done);
}); });
}); });
} }