diff --git a/docs/api.md b/docs/api.md index 9e20e22e..8cefabe8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -118,7 +118,7 @@ Do not process input images where the number of pixels (width * height) exceeds ### 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. @@ -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. +`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 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`. -#### 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 #### extract({ left: left, top: top, width: width, height: height }) diff --git a/docs/changelog.md b/docs/changelog.md index c67ebcd2..7fabac91 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,11 @@ Requires libvips v8.3.1 #### 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. Add support for libvips' new GIF and SVG loaders. Pre-built binaries now include giflib and librsvg, exclude *magick. diff --git a/index.js b/index.js index 36d0fabe..b0d40f47 100644 --- a/index.js +++ b/index.js @@ -74,6 +74,7 @@ var Sharp = function(input, options) { extendLeft: 0, extendRight: 0, withoutEnlargement: false, + kernel: 'lanczos3', interpolator: 'bicubic', // operations background: [0, 0, 0, 255], @@ -480,33 +481,6 @@ Sharp.prototype.threshold = function(threshold) { 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). Improves brightness of resized image in non-linear colour spaces. @@ -712,27 +686,75 @@ Sharp.prototype.extend = function(extend) { return this; }; -Sharp.prototype.resize = function(width, height) { - if (!width) { - this.options.width = -1; - } else { - if (typeof width === 'number' && !Number.isNaN(width) && width % 1 === 0 && width > 0 && width <= maximum.width) { +// Kernels for reduction +module.exports.kernel = { + cubic: 'cubic', + lanczos2: 'lanczos2', + 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; } else { throw new Error('Invalid width (1 to ' + maximum.width + ') ' + width); } - } - if (!height) { - this.options.height = -1; } 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; } else { 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; }; +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 diff --git a/src/operations.cc b/src/operations.cc index ca5d41ba..ac5b4d4d 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -250,4 +250,22 @@ namespace sharp { 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(round(1.0 + need_lines / 10.0))) + ->set("access", VIPS_ACCESS_SEQUENTIAL) + ->set("threaded", TRUE) + ); + } + } // namespace sharp diff --git a/src/operations.h b/src/operations.h index 7820cd2e..4877b431 100644 --- a/src/operations.h +++ b/src/operations.h @@ -44,6 +44,11 @@ namespace sharp { */ 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 #endif // SRC_OPERATIONS_H_ diff --git a/src/pipeline.cc b/src/pipeline.cc index 1bbe8108..2a341782 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -50,6 +50,7 @@ using sharp::Gamma; using sharp::Blur; using sharp::Sharpen; using sharp::EntropyCrop; +using sharp::TileCache; using sharp::ImageType; using sharp::ImageTypeId; @@ -215,17 +216,13 @@ class PipelineWorker : public AsyncWorker { 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 double xfactor = 1.0; double yfactor = 1.0; if (baton->width > 0 && baton->height > 0) { // Fixed width and height - xfactor = static_cast(inputWidth) / static_cast(baton->width); - yfactor = static_cast(inputHeight) / static_cast(baton->height); + xfactor = static_cast(inputWidth) / (static_cast(baton->width) + 0.1); + yfactor = static_cast(inputHeight) / (static_cast(baton->height) + 0.1); switch (baton->canvas) { case Canvas::CROP: xfactor = std::min(xfactor, yfactor); @@ -262,7 +259,7 @@ class PipelineWorker : public AsyncWorker { } } else if (baton->width > 0) { // Fixed width - xfactor = static_cast(inputWidth) / static_cast(baton->width); + xfactor = static_cast(inputWidth) / (static_cast(baton->width) + 0.1); if (baton->canvas == Canvas::IGNORE_ASPECT) { baton->height = inputHeight; } else { @@ -272,7 +269,7 @@ class PipelineWorker : public AsyncWorker { } } else if (baton->height > 0) { // Fixed height - yfactor = static_cast(inputHeight) / static_cast(baton->height); + yfactor = static_cast(inputHeight) / (static_cast(baton->height) + 0.1); if (baton->canvas == Canvas::IGNORE_ASPECT) { baton->width = inputWidth; } else { @@ -287,12 +284,12 @@ class PipelineWorker : public AsyncWorker { } // Calculate integral box shrink - int xshrink = CalculateShrink(xfactor, interpolatorWindowSize); - int yshrink = CalculateShrink(yfactor, interpolatorWindowSize); + int xshrink = std::max(1, static_cast(floor(xfactor))); + int yshrink = std::max(1, static_cast(floor(yfactor))); // Calculate residual float affine transformation - double xresidual = CalculateResidual(xshrink, xfactor); - double yresidual = CalculateResidual(yshrink, yfactor); + double xresidual = static_cast(xshrink) / xfactor; + double yresidual = static_cast(yshrink) / yfactor; // Do not enlarge the output if the input width *or* height // are already less than the required dimensions @@ -335,10 +332,10 @@ class PipelineWorker : public AsyncWorker { // Recalculate integral shrink and double residual xfactor = std::max(xfactor, 1.0); yfactor = std::max(yfactor, 1.0); - xshrink = CalculateShrink(xfactor, interpolatorWindowSize); - yshrink = CalculateShrink(yfactor, interpolatorWindowSize); - xresidual = CalculateResidual(xshrink, xfactor); - yresidual = CalculateResidual(yshrink, yfactor); + xshrink = std::max(1, static_cast(floor(xfactor))); + yshrink = std::max(1, static_cast(floor(yfactor))); + xresidual = static_cast(xshrink) / xfactor; + yresidual = static_cast(yshrink) / yfactor; // Reload input using shrink-on-load VOption *option = VImage::option()->set("shrink", shrink_on_load); if (baton->bufferInLength > 1) { @@ -418,7 +415,12 @@ class PipelineWorker : public AsyncWorker { } 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 int shrunkWidth = image.width(); int shrunkHeight = image.height(); @@ -466,31 +468,41 @@ class PipelineWorker : public AsyncWorker { 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) { - // Use average of x and y residuals to compute sigma for Gaussian blur - 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) { - image = image.linecache(VImage::option() - ->set("access", VIPS_ACCESS_SEQUENTIAL) - ->set("tile_height", 1) - ->set("threaded", TRUE) - ); - } - // Apply Gaussian blur - image = image.gaussblur(sigma); + // Insert tile cache to prevent over-computation of previous operations + if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL) { + image = TileCache(image, yresidual); + } + // Perform kernel-based reduction + if (yresidual < 1.0 || xresidual < 1.0) { + VipsKernel kernel = static_cast( + 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"); + } + if (yresidual < 1.0) { + 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 enlargement + 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) + ); + } + if (xresidual > 1.0) { + image = image.affine({xresidual, 0.0, 0.0, 1.0}, VImage::option() + ->set("interpolate", interpolator) + ); } } - // Perform affine transformation - image = image.affine({xresidual, 0.0, 0.0, yresidual}, VImage::option() - ->set("interpolate", interpolator) - ); } // Rotate @@ -943,30 +955,6 @@ class PipelineWorker : public AsyncWorker { 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(floor(factor * 3.0 / interpolatorWindowSize)); - } else { - shrink = static_cast(floor(factor)); - } - if (shrink < 1) { - shrink = 1; - } - return shrink; - } - - /* - Calculate residual given shrink and factor - */ - double CalculateResidual(int shrink, double factor) { - return static_cast(shrink) / factor; - } - /* Clear all thread-local data. */ @@ -1058,6 +1046,7 @@ NAN_METHOD(pipeline) { // Resize options baton->withoutEnlargement = attrAs(options, "withoutEnlargement"); baton->crop = attrAs(options, "crop"); + baton->kernel = attrAsStr(options, "kernel"); baton->interpolator = attrAsStr(options, "interpolator"); // Operators baton->flatten = attrAs(options, "flatten"); diff --git a/src/pipeline.h b/src/pipeline.h index 49ca6d39..50674b15 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -46,6 +46,7 @@ struct PipelineBaton { int channels; Canvas canvas; int crop; + std::string kernel; std::string interpolator; double background[4]; bool flatten; diff --git a/test/bench/package.json b/test/bench/package.json index 9427a368..ffd5d00c 100644 --- a/test/bench/package.json +++ b/test/bench/package.json @@ -10,10 +10,10 @@ "devDependencies": { "async": "^1.5.2", "benchmark": "^2.1.0", - "gm": "^1.21.0", + "gm": "^1.22.0", "imagemagick": "^0.1.3", "imagemagick-native": "^1.9.2", - "jimp": "^0.2.20", + "jimp": "^0.2.24", "lwip": "^0.0.8", "semver": "^5.1.0" }, diff --git a/test/bench/perf.js b/test/bench/perf.js index e2397510..a94d72da 100644 --- a/test/bench/perf.js +++ b/test/bench/perf.js @@ -30,18 +30,15 @@ var fixtures = require('../fixtures'); var width = 720; var height = 480; -var magickFilterBilinear = 'Triangle'; -var magickFilterBicubic = 'Lanczos'; - // Disable libvips cache to ensure tests are as fair as they can be sharp.cache(false); // Enable use of SIMD sharp.simd(true); async.series({ - 'jpeg-linear': function(callback) { + 'jpeg': function(callback) { var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg); - var jpegSuite = new Benchmark.Suite('jpeg-linear'); + var jpegSuite = new Benchmark.Suite('jpeg'); // jimp jpegSuite.add('jimp-buffer-buffer', { defer: true, @@ -93,7 +90,7 @@ async.series({ if (err) { throw err; } - image.resize(width, height, 'linear', function (err, image) { + image.resize(width, height, 'lanczos', function (err, image) { if (err) { throw err; } @@ -113,7 +110,7 @@ async.series({ if (err) { throw err; } - image.resize(width, height, 'linear', function (err, image) { + image.resize(width, height, 'lanczos', function (err, image) { if (err) { throw err; } @@ -140,7 +137,7 @@ async.series({ width: width, height: height, format: 'jpg', - filter: magickFilterBilinear + filter: 'Lanczos' }, function(err) { if (err) { throw err; @@ -161,7 +158,7 @@ async.series({ width: width, height: height, format: 'JPEG', - filter: magickFilterBilinear + filter: 'Lanczos' }, function (err, buffer) { if (err) { throw err; @@ -179,7 +176,7 @@ async.series({ fn: function(deferred) { gm(inputJpgBuffer) .resize(width, height) - .filter(magickFilterBilinear) + .filter('Lanczos') .quality(80) .write(fixtures.outputJpg, function (err) { if (err) { @@ -194,7 +191,7 @@ async.series({ fn: function(deferred) { gm(inputJpgBuffer) .resize(width, height) - .filter(magickFilterBilinear) + .filter('Lanczos') .quality(80) .toBuffer(function (err, buffer) { if (err) { @@ -210,7 +207,7 @@ async.series({ fn: function(deferred) { gm(fixtures.inputJpg) .resize(width, height) - .filter(magickFilterBilinear) + .filter('Lanczos') .quality(80) .write(fixtures.outputJpg, function (err) { if (err) { @@ -225,7 +222,7 @@ async.series({ fn: function(deferred) { gm(fixtures.inputJpg) .resize(width, height) - .filter(magickFilterBilinear) + .filter('Lanczos') .quality(80) .toBuffer(function (err, buffer) { if (err) { @@ -243,7 +240,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toFile(fixtures.outputJpg, function(err) { if (err) { throw err; @@ -257,7 +253,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -272,7 +267,6 @@ async.series({ fn: function(deferred) { sharp(fixtures.inputJpg) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toFile(fixtures.outputJpg, function(err) { if (err) { throw err; @@ -290,8 +284,7 @@ async.series({ deferred.resolve(); }); var pipeline = sharp() - .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear); + .resize(width, height); readable.pipe(pipeline).pipe(writable); } }).add('sharp-file-buffer', { @@ -299,7 +292,6 @@ async.series({ fn: function(deferred) { sharp(fixtures.inputJpg) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -314,19 +306,27 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toBuffer() .then(function(buffer) { assert.notStrictEqual(null, buffer); 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, fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .sharpen() .toBuffer(function(err, buffer) { if (err) { @@ -342,7 +342,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .sharpen(3, 1, 3) .toBuffer(function(err, buffer) { if (err) { @@ -358,7 +357,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .blur() .toBuffer(function(err, buffer) { if (err) { @@ -374,7 +372,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .blur(3) .toBuffer(function(err, buffer) { if (err) { @@ -390,7 +387,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .gamma() .toBuffer(function(err, buffer) { if (err) { @@ -406,7 +402,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .normalise() .toBuffer(function(err, buffer) { if (err) { @@ -422,7 +417,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .greyscale() .toBuffer(function(err, buffer) { if (err) { @@ -438,7 +432,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .gamma() .greyscale() .toBuffer(function(err, buffer) { @@ -455,7 +448,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .progressive() .toBuffer(function(err, buffer) { if (err) { @@ -471,7 +463,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .withoutChromaSubsampling() .toBuffer(function(err, buffer) { if (err) { @@ -487,7 +478,6 @@ async.series({ fn: function(deferred) { sharp(inputJpgBuffer) .rotate(90) - .interpolateWith(sharp.interpolator.bilinear) .resize(width, height) .toBuffer(function(err, buffer) { if (err) { @@ -503,8 +493,6 @@ async.series({ fn: function(deferred) { sharp.simd(false); sharp(inputJpgBuffer) - .rotate(90) - .interpolateWith(sharp.interpolator.bilinear) .resize(width, height) .toBuffer(function(err, buffer) { sharp.simd(true); @@ -520,9 +508,8 @@ async.series({ defer: true, fn: function(deferred) { sharp(inputJpgBuffer) - .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .sequentialRead() + .resize(width, height) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -533,189 +520,19 @@ async.series({ }); } }).on('cycle', function(event) { - console.log('jpeg-linear ' + String(event.target)); + console.log('operations ' + String(event.target)); }).on('complete', function() { callback(null, this.filter('fastest').map('name')); }).run(); }, - - 'jpeg-cubic': function(callback) { + // Comparitive speed of kernels + kernels: function(callback) { var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg); - var jpegSuite = new Benchmark.Suite('jpeg-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', { + (new Benchmark.Suite('kernels')).add('sharp-cubic', { defer: true, fn: function(deferred) { sharp(inputJpgBuffer) - .resize(width, height) - .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) + .resize(width, height, { kernel: 'cubic' }) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -725,39 +542,11 @@ async.series({ } }); } - }).add('sharp-file-file', { + }).add('sharp-lanczos2', { defer: true, fn: function(deferred) { - sharp(fixtures.inputJpg) - .resize(width, height) - .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) + sharp(inputJpgBuffer) + .resize(width, height, { kernel: 'lanczos2' }) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -767,109 +556,11 @@ async.series({ } }); } - }).add('sharp-promise', { + }).add('sharp-lanczos3', { defer: true, fn: function(deferred) { sharp(inputJpgBuffer) - .resize(width, height) - .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) + .resize(width, height, { kernel: 'lanczos3' }) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -880,12 +571,12 @@ async.series({ }); } }).on('cycle', function(event) { - console.log('interpolators ' + String(event.target)); + console.log('kernels ' + String(event.target)); }).on('complete', function() { callback(null, this.filter('fastest').map('name')); }).run(); }, - + // PNG png: function(callback) { var inputPngBuffer = fs.readFileSync(fixtures.inputPng); var pngSuite = new Benchmark.Suite('png'); @@ -938,7 +629,7 @@ async.series({ if (err) { throw err; } - image.resize(width, height, 'linear', function (err, image) { + image.resize(width, height, 'lanczos', function (err, image) { if (err) { throw err; } @@ -963,7 +654,7 @@ async.series({ dstPath: fixtures.outputPng, width: width, height: height, - filter: magickFilterBilinear + filter: 'Lanczos' }, function(err) { if (err) { throw err; @@ -983,7 +674,7 @@ async.series({ width: width, height: height, format: 'PNG', - filter: magickFilterBilinear + filter: 'Lanczos' }); deferred.resolve(); } @@ -995,7 +686,7 @@ async.series({ fn: function(deferred) { gm(fixtures.inputPng) .resize(width, height) - .filter(magickFilterBilinear) + .filter('Lanczos') .write(fixtures.outputPng, function (err) { if (err) { throw err; @@ -1009,7 +700,7 @@ async.series({ fn: function(deferred) { gm(fixtures.inputPng) .resize(width, height) - .filter(magickFilterBilinear) + .filter('Lanczos') .toBuffer(function (err, buffer) { if (err) { throw err; @@ -1026,7 +717,6 @@ async.series({ fn: function(deferred) { sharp(inputPngBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toFile(fixtures.outputPng, function(err) { if (err) { throw err; @@ -1040,7 +730,6 @@ async.series({ fn: function(deferred) { sharp(inputPngBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -1055,7 +744,6 @@ async.series({ fn: function(deferred) { sharp(fixtures.inputPng) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toFile(fixtures.outputPng, function(err) { if (err) { throw err; @@ -1069,7 +757,6 @@ async.series({ fn: function(deferred) { sharp(fixtures.inputPng) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -1084,7 +771,6 @@ async.series({ fn: function(deferred) { sharp(inputPngBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .progressive() .toBuffer(function(err, buffer) { if (err) { @@ -1100,7 +786,6 @@ async.series({ fn: function(deferred) { sharp(inputPngBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .withoutAdaptiveFiltering() .toBuffer(function(err, buffer) { if (err) { @@ -1118,7 +803,7 @@ async.series({ callback(null, this.filter('fastest').map('name')); }).run(); }, - + // WebP webp: function(callback) { var inputWebPBuffer = fs.readFileSync(fixtures.inputWebP); (new Benchmark.Suite('webp')).add('sharp-buffer-file', { @@ -1126,7 +811,6 @@ async.series({ fn: function(deferred) { sharp(inputWebPBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toFile(fixtures.outputWebP, function(err) { if (err) { throw err; @@ -1140,7 +824,6 @@ async.series({ fn: function(deferred) { sharp(inputWebPBuffer) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toBuffer(function(err, buffer) { if (err) { throw err; @@ -1155,7 +838,6 @@ async.series({ fn: function(deferred) { sharp(fixtures.inputWebP) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toFile(fixtures.outputWebP, function(err) { if (err) { throw err; @@ -1169,7 +851,6 @@ async.series({ fn: function(deferred) { sharp(fixtures.inputWebp) .resize(width, height) - .interpolateWith(sharp.interpolator.bilinear) .toBuffer(function(err, buffer) { if (err) { throw err; diff --git a/test/bench/random.js b/test/bench/random.js index 9fa9d093..a781e76a 100644 --- a/test/bench/random.js +++ b/test/bench/random.js @@ -8,14 +8,12 @@ var Benchmark = require('benchmark'); var sharp = require('../../index'); var fixtures = require('../fixtures'); +sharp.cache(false); sharp.simd(true); var min = 320; var max = 960; -// Nearest equivalent to bilinear -var magickFilter = 'Triangle'; - var randomDimension = function() { return Math.ceil(Math.random() * (max - min) + min); }; @@ -30,7 +28,7 @@ new Benchmark.Suite('random').add('imagemagick', { width: randomDimension(), height: randomDimension(), format: 'jpg', - filter: magickFilter + filter: 'Lanczos' }, function(err) { if (err) { throw err; @@ -44,7 +42,7 @@ new Benchmark.Suite('random').add('imagemagick', { fn: function(deferred) { gm(fixtures.inputJpg) .resize(randomDimension(), randomDimension()) - .filter(magickFilter) + .filter('Lanczos') .quality(80) .toBuffer(function (err, buffer) { if (err) { @@ -58,19 +56,20 @@ new Benchmark.Suite('random').add('imagemagick', { }).add('sharp', { defer: true, fn: function(deferred) { - sharp(fixtures.inputJpg).resize(randomDimension(), randomDimension()).toBuffer(function(err, buffer) { - if (err) { - throw err; - } else { - assert.notStrictEqual(null, buffer); - deferred.resolve(); - } - }); + sharp(fixtures.inputJpg) + .resize(randomDimension(), randomDimension()) + .toBuffer(function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); } }).on('cycle', function(event) { console.log(String(event.target)); }).on('complete', function() { var winner = this.filter('fastest').map('name'); assert.strictEqual('sharp', String(winner), 'sharp was slower than ' + winner); - console.dir(sharp.cache()); }).run(); diff --git a/test/fixtures/expected/crop-entropy.jpg b/test/fixtures/expected/crop-entropy.jpg index c1c08061..40107d2d 100644 Binary files a/test/fixtures/expected/crop-entropy.jpg and b/test/fixtures/expected/crop-entropy.jpg differ diff --git a/test/fixtures/expected/embed-16bit-rgba.png b/test/fixtures/expected/embed-16bit-rgba.png index e50edc89..06a53d2c 100644 Binary files a/test/fixtures/expected/embed-16bit-rgba.png and b/test/fixtures/expected/embed-16bit-rgba.png differ diff --git a/test/fixtures/expected/embed-16bit.png b/test/fixtures/expected/embed-16bit.png index f5981791..e5ce8c4e 100644 Binary files a/test/fixtures/expected/embed-16bit.png and b/test/fixtures/expected/embed-16bit.png differ diff --git a/test/fixtures/expected/flatten-rgb16-orange.jpg b/test/fixtures/expected/flatten-rgb16-orange.jpg index 91890f5a..f4567011 100644 Binary files a/test/fixtures/expected/flatten-rgb16-orange.jpg and b/test/fixtures/expected/flatten-rgb16-orange.jpg differ diff --git a/test/fixtures/expected/gamma-0.0.jpg b/test/fixtures/expected/gamma-0.0.jpg index 30c123c6..975ae125 100644 Binary files a/test/fixtures/expected/gamma-0.0.jpg and b/test/fixtures/expected/gamma-0.0.jpg differ diff --git a/test/fixtures/expected/svg1200.png b/test/fixtures/expected/svg1200.png index 8861d7d5..78df696d 100644 Binary files a/test/fixtures/expected/svg1200.png and b/test/fixtures/expected/svg1200.png differ diff --git a/test/fixtures/expected/threshold-128-transparency.webp b/test/fixtures/expected/threshold-128-transparency.webp index cd1f2f5a..e126e12f 100644 Binary files a/test/fixtures/expected/threshold-128-transparency.webp and b/test/fixtures/expected/threshold-128-transparency.webp differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 73048b30..c8c4e3ed 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -18,7 +18,6 @@ var fingerprint = function(image, callback) { .normalise() .resize(9, 8) .ignoreAspectRatio() - .interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline) .raw() .toBuffer(function(err, data) { if (err) { diff --git a/test/interpolators/README.md b/test/interpolators/README.md deleted file mode 100644 index add6d84a..00000000 --- a/test/interpolators/README.md +++ /dev/null @@ -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 -``` diff --git a/test/interpolators/bicubic.jpg b/test/interpolators/bicubic.jpg deleted file mode 100644 index 95c211c4..00000000 Binary files a/test/interpolators/bicubic.jpg and /dev/null differ diff --git a/test/interpolators/bilinear.jpg b/test/interpolators/bilinear.jpg deleted file mode 100644 index 880bdbe5..00000000 Binary files a/test/interpolators/bilinear.jpg and /dev/null differ diff --git a/test/interpolators/generate.js b/test/interpolators/generate.js deleted file mode 100644 index 2ff0e917..00000000 --- a/test/interpolators/generate.js +++ /dev/null @@ -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; - }); -}); diff --git a/test/interpolators/gm.jpg b/test/interpolators/gm.jpg deleted file mode 100644 index 9bc33c44..00000000 Binary files a/test/interpolators/gm.jpg and /dev/null differ diff --git a/test/interpolators/lbb.jpg b/test/interpolators/lbb.jpg deleted file mode 100644 index a179a767..00000000 Binary files a/test/interpolators/lbb.jpg and /dev/null differ diff --git a/test/interpolators/nearest.jpg b/test/interpolators/nearest.jpg deleted file mode 100644 index 844f6c7b..00000000 Binary files a/test/interpolators/nearest.jpg and /dev/null differ diff --git a/test/interpolators/nohalo.jpg b/test/interpolators/nohalo.jpg deleted file mode 100644 index 11247428..00000000 Binary files a/test/interpolators/nohalo.jpg and /dev/null differ diff --git a/test/interpolators/vsqbs.jpg b/test/interpolators/vsqbs.jpg deleted file mode 100644 index 12908e04..00000000 Binary files a/test/interpolators/vsqbs.jpg and /dev/null differ diff --git a/test/unit/alpha.js b/test/unit/alpha.js index 026cc8f3..1f27544d 100644 --- a/test/unit/alpha.js +++ b/test/unit/alpha.js @@ -45,15 +45,17 @@ describe('Alpha transparency', function() { }); it('Flatten 16-bit PNG with transparency to orange', function(done) { + var output = fixtures.path('output.flatten-rgb16-orange.jpg'); sharp(fixtures.inputPngWithTransparency16bit) .flatten() .background({r: 255, g: 102, b: 0}) - .toBuffer(function(err, data, info) { + .toFile(output, function(err, info) { if (err) throw err; assert.strictEqual(true, info.size > 0); assert.strictEqual(32, info.width); 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(); }); }); diff --git a/test/unit/clone.js b/test/unit/clone.js index e145423e..c59d5b89 100644 --- a/test/unit/clone.js +++ b/test/unit/clone.js @@ -57,7 +57,7 @@ describe('Clone', function() { var rotator = sharp().rotate(90); // Cloned instances with differing dimensions rotator.clone().resize(320, 240).pipe(writable1); - rotator.clone().resize(100).pipe(writable2); + rotator.clone().resize(100, 122).pipe(writable2); // Go fs.createReadStream(fixtures.inputJpg).pipe(rotator); }); diff --git a/test/unit/interpolation.js b/test/unit/interpolation.js index 9945cfc4..d979b294 100644 --- a/test/unit/interpolation.js +++ b/test/unit/interpolation.js @@ -5,100 +5,67 @@ var assert = require('assert'); var sharp = require('../../index'); var fixtures = require('../fixtures'); -describe('Interpolation', function() { +describe('Interpolators and kernels', function() { - it('nearest neighbour', function(done) { - sharp(fixtures.inputJpg) - .resize(320, 240) - .interpolateWith(sharp.interpolator.nearest) - .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(); + describe('Reducers', function() { + [ + sharp.kernel.cubic, + sharp.kernel.lanczos2, + sharp.kernel.lanczos3 + ].forEach(function(kernel) { + it(kernel, function(done) { + sharp(fixtures.inputJpg) + .resize(320, null, { kernel: kernel }) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + fixtures.assertSimilar(fixtures.inputJpg, data, done); + }); }); + }); }); - it('bilinear', function(done) { - sharp(fixtures.inputJpg) - .resize(320, 240) - .interpolateWith(sharp.interpolator.bilinear) - .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(); + 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) + .resize(320, null, { interpolator: interpolator }) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + fixtures.assertSimilar(fixtures.inputJpg, data, done); + }); }); + }); }); - it('bicubic', function(done) { - sharp(fixtures.inputJpg) - .resize(320, 240) - .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('unknown kernel throws', function() { + assert.throws(function() { + sharp().resize(null, null, { kernel: 'unknown' }); + }); }); - it('nohalo', function(done) { - sharp(fixtures.inputJpg) - .resize(320, 240) - .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('unknown interpolator throws', function() { + assert.throws(function() { + sharp().resize(null, null, { interpolator: 'unknown' }); + }); }); - it('locally bounded bicubic (LBB)', function(done) { - sharp(fixtures.inputJpg) - .resize(320, 240) - .interpolateWith(sharp.interpolator.locallyBoundedBicubic) - .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(); - }); + describe('deprecated interpolateWith method still works', function() { + it('resize then interpolateWith', function() { + sharp().resize(1, 1).interpolateWith('bicubic'); + }); + it('interpolateWith then resize', function() { + sharp().interpolateWith('bicubic').resize(1, 1); + }); }); - - 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(); - }); - }); diff --git a/test/unit/io.js b/test/unit/io.js index 77d44ff7..472dc0f9 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -1028,14 +1028,14 @@ describe('Input/output', function() { it('Info event data', function(done) { var readable = fs.createReadStream(fixtures.inputJPGBig); var inPipeline = sharp() - .resize(840) + .resize(840, 472) .raw() .on('info', function(info) { assert.strictEqual(840, info.width); assert.strictEqual(472, info.height); 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') .toBuffer(function(err, data, info) { assert.strictEqual(err.message.indexOf('memory area too small') > 0, true); @@ -1047,7 +1047,7 @@ describe('Input/output', function() { done(); }); inPipeline = sharp() - .resize(840) + .resize(840, 472) .raw(); readable.pipe(inPipeline).pipe(goodPipeline); }); diff --git a/test/unit/threshold.js b/test/unit/threshold.js index dd0cecfc..7609e582 100644 --- a/test/unit/threshold.js +++ b/test/unit/threshold.js @@ -96,7 +96,7 @@ describe('Threshold', function() { .threshold() .toBuffer(function(err, data, info) { 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); }); }); }