Compare commits

..

48 Commits

Author SHA1 Message Date
Lovell Fuller
d1b47ef419 Add details of performance test task - closes #12 2014-04-16 21:55:22 +01:00
Lovell Fuller
0954ca6adf Update performance test results 2014-04-15 22:57:27 +01:00
Lovell Fuller
c9d7f43bd9 Add gm buffer-input perf tests 2014-04-15 22:57:07 +01:00
Lovell Fuller
481741315d Correct README markdown format 2014-04-14 22:09:08 +01:00
Lovell Fuller
cae1dbdb89 Add imagemagick-native comparison to and disable vips caching in perf tests. 2014-04-14 22:03:48 +01:00
Lovell Fuller
200d5a9312 Major version increase ahead of incoming API additions. Add link to Travis CI status. 2014-04-08 23:11:01 +01:00
Lovell Fuller
e9ca25cb45 Correct markdown typo 2014-04-08 22:40:34 +01:00
Lovell Fuller
33f24d41e7 Add Travis CI config
Uses a PPA with back-ported libwebp to work around https://bugs.launchpad.net/ubuntu/+source/libwebp/+bug/1108731
2014-04-08 22:35:42 +01:00
Lovell Fuller
45d5f12a63 Add details of Ubuntu 12.04 oddities. Closes #16. 2014-04-08 22:35:20 +01:00
Lovell Fuller
8785ca4331 Merge pull request #15 from pierreinglebert/feature-movefixtures
Move test fixtures to their own directory.
2014-04-02 22:54:05 +01:00
Pierre Inglebert
1ecdf97bdb Move fixtures to a proper directory
Add test for gif (libmagick)
2014-04-02 23:42:05 +02:00
Lovell Fuller
3703ee41aa Merge pull request #14 from pierreinglebert/feature-magickloader
Add support for many other input files via the *magick libraries
2014-04-02 22:26:38 +01:00
Lovell Fuller
c8f023d8ba Support Node.js v0.10+ only. Add @pierreinglebert credit. 2014-04-02 22:19:53 +01:00
Lovell Fuller
fe773733cd Merge pull request #11 from pierreinglebert/master
Add support for Node.js v0.11+ via nan. Remove epeg test dependency as it fails to compile on v0.11+.
2014-04-02 22:08:21 +01:00
Pierre Inglebert
19bec9346e add libmagick load support 2014-04-02 22:17:41 +02:00
Pierre Inglebert
9bd335079f check input files with vips foreign lib 2014-04-02 21:04:01 +02:00
Pierre Inglebert
b6dc179551 remove all references to epeg 2014-04-02 18:51:19 +02:00
Pierre Inglebert
e96fd8b9de 0.11 compat using nan 2014-04-02 00:10:42 +02:00
Lovell Fuller
6a2816e917 Add help for compiling against the gettext dependency of libvips on Mac OS #9 2014-03-13 22:16:47 +00:00
Lovell Fuller
06d88de5a2 Version bump 2014-03-12 21:11:25 +00:00
Lovell Fuller
a292e1fe8e Add path to homebrew-installed pkgconfig for Mac OS 10.8 (10.9 is symlinked to 10.8) #9 2014-03-12 20:45:35 +00:00
Lovell Fuller
08bb35e7af Add test dependency installation details 2014-03-11 22:44:05 +00:00
Lovell Fuller
0e89a5bbf2 Add summary of JPEG and PNG optimisation features 2014-03-10 22:32:14 +00:00
Lovell Fuller
2a0d79a78b Minor markdown layout fix 2014-03-09 21:54:31 +00:00
Lovell Fuller
764b57022b Minor markdown layout fix 2014-03-09 21:53:09 +00:00
Lovell Fuller
5f61331d1a Breaking change to API to become more expressive (see #8). Add support for upscaling. 2014-03-09 21:44:03 +00:00
Lovell Fuller
d0e6a4c0f3 Support identity transform when both width and height are not specified 2014-03-04 22:44:53 +00:00
Lovell Fuller
f99e42d447 Add support for auto-scaling of width and height 2014-03-03 23:24:09 +00:00
Lovell Fuller
9b4387be97 Improve installation instructions for libvips 2014-02-28 23:11:05 +00:00
Lovell Fuller
9c3631ecb7 Add comparative performance tests using random dimensions 2014-02-28 22:39:02 +00:00
Lovell Fuller
31ca68fb14 Improve documentation of cache method 2014-02-28 22:37:59 +00:00
Lovell Fuller
d5d85a8697 Expose vips internal cache settings and status 2014-02-25 23:31:33 +00:00
Lovell Fuller
ae9a8b0f57 Remove unnecessary temporary copy of input buffer. 2014-02-23 10:52:40 +00:00
Lovell Fuller
0899252a72 Add support for WebP and TIFF image formats. Closes #7. 2014-02-22 21:48:00 +00:00
Lovell Fuller
16551bc058 Take a temporary copy of buffers provided as input. Soak testing suggests this prevents the problems seen in #6. 2014-02-12 22:36:13 +00:00
Lovell Fuller
e9d196f696 Fix (unrelated) memory leak discovered whilst investigating #6 2014-02-06 22:20:48 +00:00
Lovell Fuller
2f97d04dfa Keep shrink-on-load logic DRY. Ensure canvas option (crop vs embed) is always as requested. 2014-02-03 23:20:38 +00:00
Lovell Fuller
e4ca8f44ec Version bump 2014-02-01 23:00:37 +00:00
Lovell Fuller
6b3dc1e350 Ensure crop occurs on y-axis 2014-02-01 22:58:30 +00:00
Lovell Fuller
7ffcdb79e0 Add glib-2.0 path for Mac OS X 2014-01-30 18:28:24 +00:00
Lovell Fuller
10ce7c6693 Add parallel performance test to demonstrate effect of waiting for a worker thread 2014-01-26 20:16:42 +00:00
Lovell Fuller
ccd6012152 Version bump 2014-01-21 22:49:40 +00:00
Lovell Fuller
377662fffc Ensure gm perf tests actually resize. Prevent coredump when embeding pre-shrunk image within same dimensions. 2014-01-21 22:48:33 +00:00
Lovell Fuller
d509458ba1 Major rewrite and therefore API change. Despite the name, image sharpening is now optional, plus there are other new options. Can now handle buffers in and out. Doubled performance in certain cases. Closes #3. Closes #4. 2014-01-19 21:38:37 +00:00
Lovell Fuller
be8f35d830 Use shrink-on-load for JPEG images, partially implementing #4. Switch to new vip_ methods from legacy im_ methods. Large performance gains all round. 2014-01-16 22:51:44 +00:00
Lovell Fuller
7e8af63129 Improve thread/buffer shutdown using new vips_thread_shutdown method - closes #5 2014-01-15 21:38:23 +00:00
Lovell Fuller
f7b8ce1287 Add support for writing image data output to a buffer, boosting ops/sec by ~7%. Partially implements #3. 2014-01-11 21:50:17 +00:00
Lovell Fuller
dde9e94850 From here on in, this module will be using the bleeding edge version of libvips. Why? It just keeps getting faster. 2013-11-23 22:08:51 +00:00
16 changed files with 1312 additions and 288 deletions

3
.gitignore vendored
View File

@@ -12,7 +12,6 @@ logs
results
build
node_modules
tests/output.jpg
tests/output.png
tests/fixtures/output.*
npm-debug.log

16
.travis.yml Normal file
View File

@@ -0,0 +1,16 @@
language: node_js
node_js:
- "0.10"
before_install:
- sudo add-apt-repository ppa:lyrasis/precise-backports -y
- sudo apt-get update -qq
- sudo apt-get install -qq automake gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev liborc-0.4-dev libxml2-dev swig graphicsmagick libmagick++-dev
- git clone https://github.com/jcupitt/libvips.git
- cd libvips
- git checkout 7.38
- ./bootstrap.sh
- ./configure --enable-debug=no --enable-cxx=no --without-python
- make
- sudo make install
- sudo ldconfig
- cd $TRAVIS_BUILD_DIR

259
README.md
View File

@@ -1,51 +1,67 @@
# sharp
# sharp
_adj_
* [Installation](https://github.com/lovell/sharp#installation)
* [Usage examples](https://github.com/lovell/sharp#usage-examples)
* [API](https://github.com/lovell/sharp#api)
* [Testing](https://github.com/lovell/sharp#testing)
* [Performance](https://github.com/lovell/sharp#performance)
* [Licence](https://github.com/lovell/sharp#licence)
1. clearly defined; distinct: a sharp photographic image.
2. quick, brisk, or spirited.
3. shrewd or astute: a sharp bargainer.
4. (Informal.) very stylish: a sharp dresser; a sharp jacket.
The typical use case for this high speed Node.js module is to convert large images of many formats to smaller, web-friendly JPEG, PNG and WebP images of varying dimensions.
The typical use case for this high speed Node.js module is to convert large JPEG and PNG images to smaller JPEG and PNG images of varying dimensions.
The performance of JPEG resizing is typically 8x faster than ImageMagick and GraphicsMagick, based mainly on the number of CPU cores available. Everything remains non-blocking thanks to _libuv_.
It is somewhat opinionated in that it only deals with JPEG and PNG images, always obeys the requested dimensions by either cropping or embedding and insists on a mild sharpen of the resulting image.
This module supports reading and writing images of JPEG, PNG and WebP to and from both Buffer objects and the filesystem. It also supports reading images of many other types from the filesystem via libmagick++ or libgraphicsmagick++ if present.
Under the hood you'll find the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by the University of Southampton.
When generating JPEG output all metadata is removed and Huffman tables optimised without having to use separate command line tools like [jpegoptim](https://github.com/tjko/jpegoptim) and [jpegtran](http://jpegclub.org/jpegtran/).
Performance is 4x-8x faster than ImageMagick and 2x-4x faster than GraphicsMagick, based mainly on the number of CPU cores available.
Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent.
## Prerequisites
This module is powered by the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by John Cupitt.
* Node.js v0.8+
* node-gyp
* libvips-dev 7.28+ (7.36+ for optimal JPEG Huffman coding)
```
sudo npm install -g node-gyp
sudo apt-get install libvips-dev
```
When installed as a package, please symlink `vips-7.28.pc` (or later, installed with libvips-dev) as `/usr/lib/pkgconfig/vips.pc`. To do this in Ubuntu 13.04 (64-bit), use:
sudo ln -s /usr/lib/x86_64-linux-gnu/pkgconfig/vips-7.28.pc /usr/lib/pkgconfig/vips.pc
## Install
## Installation
npm install sharp
## Usage
### Prerequisites
var sharp = require("sharp");
* Node.js v0.10+
* [libvips](https://github.com/jcupitt/libvips) v7.38.5+
### crop(inputPath, outputPath, width, height, callback)
### Install libvips on Mac OS
Scale and crop `inputPath` to `width` x `height` and write to `outputPath` calling `callback` when complete.
brew install homebrew/science/vips --with-webp --with-graphicsmagick
Example:
The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp/issues/9) to a `library not found for -lintl` error. If so, please try:
brew link gettext --force
### Install libvips on Ubuntu/Debian Linux
sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev liborc-0.4-dev libxml2-dev swig
git clone https://github.com/jcupitt/libvips.git
cd libvips
git checkout 7.38
./bootstrap.sh
./configure --enable-debug=no --enable-cxx=no --without-python
make
sudo make install
sudo ldconfig
Ubuntu 12.04 requires `libtiff4-dev` instead of `libtiff5-dev` and has [a bug](https://bugs.launchpad.net/ubuntu/+source/libwebp/+bug/1108731) in the libwebp package. Work around these problems by running these command first:
sudo add-apt-repository ppa:lyrasis/precise-backports
sudo apt-get update
sudo apt-get install libtiff4-dev
## Usage examples
```javascript
sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
var sharp = require('sharp');
```
```javascript
sharp('input.jpg').resize(300, 200).write('output.jpg', function(err) {
if (err) {
throw err;
}
@@ -54,65 +70,184 @@ sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
});
```
### embedWhite(inputPath, outputPath, width, height, callback)
Scale and embed `inputPath` to `width` x `height` using a white canvas and write to `outputPath` calling `callback` when complete.
```javascript
sharp.embedWhite("input.jpg", "output.png", 200, 300, function(err) {
sharp('input.jpg').resize(null, 200).progressive().toBuffer(function(err, buffer) {
if (err) {
throw err;
}
// output.jpg is a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.png embedded on a white canvas
// buffer contains progressive JPEG image data, 200 pixels high
});
```
### embedBlack(inputPath, outputPath, width, height, callback)
Scale and embed `inputPath` to `width` x `height` using a black canvas and write to `outputPath` calling `callback` when complete.
```javascript
sharp.embedBlack("input.png", "output.png", 200, 300, function(err) {
sharp('input.png').resize(300).sharpen().webp(function(err, buffer) {
if (err) {
throw err;
}
// output.png is a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.png embedded on a black canvas
// buffer contains sharpened WebP image data (converted from PNG), 300 pixels wide
});
```
```javascript
sharp(buffer).resize(200, 300).embedWhite().write('output.tiff', function(err) {
if (err) {
throw err;
}
// output.tiff is a 200 pixels wide and 300 pixels high image containing a scaled
// version, embedded on a white canvas, of the image data in buffer
});
```
```javascript
sharp('input.gif').resize(200, 300).embedBlack().webp(function(err, buffer) {
if (err) {
throw err;
}
// buffer contains WebP image data of a 200 pixels wide and 300 pixels high image
// containing a scaled version, embedded on a black canvas, of input.gif
});
```
## API
### sharp(input)
Constructor to which further methods are chained. `input` can be one of:
* Buffer containing JPEG, PNG or WebP image data, or
* String containing the filename of an image, with most major formats supported.
### resize(width, [height])
Scale to `width` x `height`. By default, the resized image is cropped to the exact size specified.
`width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
`height` is the Number of pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width.
### crop()
Crop the resized image to the exact size specified, the default behaviour.
### embedWhite()
Embed the resized image on a white background of the exact size specified.
### embedBlack()
Embed the resized image on a black background of the exact size specified.
### sharpen()
Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
### progressive()
Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed.
### sequentialRead()
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
### write(filename, callback)
`filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP and TIFF supported.
`callback` is called with a single argument `(err)` containing an error message, if any.
### jpeg(callback)
Write JPEG image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant JPEG image data.
### png(callback)
Write PNG image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant PNG image data.
### webp(callback)
Write WebP image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant WebP image data.
### toBuffer(callback)
Write image data to a Buffer, the format of which will match the input image. JPEG, PNG and WebP are supported.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data.
### sharp.cache([limit])
If `limit` is provided, set the (soft) limit of _libvips_ working/cache memory to this value in MB. The default value is 100.
This method always returns cache statistics, useful for determining how much working memory is required for a particular task.
Warnings such as _Application transferred too many scanlines_ are a good indicator you've set this value too low.
```javascript
var stats = sharp.cache(); // { current: 98, high: 115, limit: 100 }
sharp.cache(200); // { current: 98, high: 115, limit: 200 }
sharp.cache(50); // { current: 49, high: 115, limit: 50 }
```
## Testing
npm install --dev sharp
[![Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp)
npm test
Running the tests requires both ImageMagick and GraphicsMagick plus one of either libmagick++-dev or libgraphicsmagick++.
brew install imagemagick
brew install graphicsmagick
sudo apt-get install imagemagick graphicsmagick libmagick++-dev
## Performance
Test environment:
### Test environment
* AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3
* libvips 7.36
* libjpeg-turbo8 1.2.1
* libpng 1.6.6
* zlib1g 1.2.7
* Intel Xeon [L5520](http://ark.intel.com/products/40201/Intel-Xeon-Processor-L5520-8M-Cache-2_26-GHz-5_86-GTs-Intel-QPI) 2.27GHz 8MB cache
* Ubuntu 13.10
* libvips 7.38.5
#### JPEG
### The contenders
* imagemagick x 5.53 ops/sec ±0.55% (31 runs sampled)
* gm x 10.86 ops/sec ±0.43% (56 runs sampled)
* epeg x 28.07 ops/sec ±0.07% (70 runs sampled)
* sharp x 31.60 ops/sec ±8.80% (80 runs sampled)
* [imagemagick-native](https://github.com/mash/node-imagemagick-native) - Supports Buffers only and blocks main V8 thread whilst processing.
* [imagemagick](https://github.com/rsms/node-imagemagick) - Supports filesystem only and "has been unmaintained for a long time".
* [gm](https://github.com/aheckmann/gm) - Fully featured wrapper around GraphicsMagick.
* sharp - Caching within libvips disabled to ensure a fair comparison.
#### PNG
### The task
* imagemagick x 4.65 ops/sec ±0.37% (27 runs sampled)
* gm x 21.65 ops/sec ±0.18% (56 runs sampled)
* sharp x 39.47 ops/sec ±6.78% (68 runs sampled)
Decompress a 2725x2225 JPEG image, resize and crop to 720x480, then compress to JPEG.
### Results
| Module | Input | Output | Ops/sec | Speed-up |
| :-------------------- | :----- | :----- | ------: | -------: |
| imagemagick-native | buffer | buffer | 0.97 | 1 |
| imagemagick | file | file | 2.49 | 2.6 |
| gm | buffer | file | 3.72 | 3.8 |
| gm | buffer | buffer | 3.80 | 3.9 |
| gm | file | file | 3.67 | 3.8 |
| gm | file | buffer | 3.67 | 3.8 |
| sharp | buffer | file | 13.62 | 14.0 |
| sharp | buffer | buffer | 12.43 | 12.8 |
| sharp | file | file | 13.02 | 13.4 |
| sharp | file | buffer | 11.15 | 11.5 |
| sharp +sharpen | file | buffer | 10.26 | 10.6 |
| sharp +progressive | file | buffer | 9.44 | 9.7 |
| sharp +sequentialRead | file | buffer | 11.94 | 12.3 |
You can expect much greater performance with caching enabled (default) and using 16+ core machines.
## Licence
Copyright 2013 Lovell Fuller
Copyright 2013, 2014 Lovell Fuller and Pierre Inglebert
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -3,15 +3,17 @@
'target_name': 'sharp',
'sources': ['src/sharp.cc'],
'libraries': [
'<!@(PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" pkg-config --libs vips)',
'<!@(PKG_CONFIG_PATH="/usr/lib/pkgconfig" pkg-config --libs vips)'
'<!@(PKG_CONFIG_PATH="/usr/local/Library/ENV/pkgconfig/10.8:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig" pkg-config --libs vips)'
],
'include_dirs': [
'/usr/local/include/glib-2.0',
'/usr/local/lib/glib-2.0/include',
'/usr/include/glib-2.0',
'/usr/lib/glib-2.0/include',
'/usr/lib/x86_64-linux-gnu/glib-2.0/include'
'/usr/lib/x86_64-linux-gnu/glib-2.0/include',
'<!(node -e "require(\'nan\')")'
],
'cflags': ['-fexceptions'],
'cflags_cc': ['-fexceptions']
'cflags': ['-fexceptions', '-pedantic', '-Wall', '-O3'],
'cflags_cc': ['-fexceptions', '-pedantic', '-Wall', '-O3']
}]
}

129
index.js
View File

@@ -1,13 +1,128 @@
var sharp = require("./build/Release/sharp");
/*jslint node: true */
'use strict';
module.exports.crop = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "c", callback);
var sharp = require('./build/Release/sharp');
var Sharp = function(input) {
if (!(this instanceof Sharp)) {
return new Sharp(input);
}
this.options = {
width: -1,
height: -1,
canvas: 'c',
sharpen: false,
progressive: false,
sequentialRead: false,
output: '__jpeg'
};
if (typeof input === 'string') {
this.options.inFile = input;
} else if (typeof input ==='object' && input instanceof Buffer) {
this.options.inBuffer = input;
} else {
throw 'Unsupported input ' + typeof input;
}
return this;
};
module.exports = Sharp;
Sharp.prototype.crop = function() {
this.options.canvas = 'c';
return this;
};
module.exports.embedWhite = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "w", callback);
Sharp.prototype.embedWhite = function() {
this.options.canvas = 'w';
return this;
};
module.exports.embedBlack = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, "b", callback);
Sharp.prototype.embedBlack = function() {
this.options.canvas = 'b';
return this;
};
Sharp.prototype.sharpen = function(sharpen) {
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
return this;
};
Sharp.prototype.progressive = function(progressive) {
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
return this;
};
Sharp.prototype.sequentialRead = function(sequentialRead) {
this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true;
return this;
};
Sharp.prototype.resize = function(width, height) {
if (!width) {
this.options.width = -1;
} else {
if (!Number.isNaN(width)) {
this.options.width = width;
} else {
throw 'Invalid width ' + width;
}
}
if (!height) {
this.options.height = -1;
} else {
if (!Number.isNaN(height)) {
this.options.height = height;
} else {
throw 'Invalid height ' + height;
}
}
return this;
};
Sharp.prototype.write = function(output, callback) {
if (!output || output.length === 0) {
throw 'Invalid output';
} else {
this._sharp(output, callback);
}
return this;
};
Sharp.prototype.toBuffer = function(callback) {
return this._sharp('__input', callback);
};
Sharp.prototype.jpeg = function(callback) {
return this._sharp('__jpeg', callback);
};
Sharp.prototype.png = function(callback) {
return this._sharp('__png', callback);
};
Sharp.prototype.webp = function(callback) {
return this._sharp('__webp', callback);
};
Sharp.prototype._sharp = function(output, callback) {
sharp.resize(
this.options.inFile,
this.options.inBuffer,
output,
this.options.width,
this.options.height,
this.options.canvas,
this.options.sharpen,
this.options.progressive,
this.options.sequentialRead,
callback
);
return this;
};
module.exports.cache = function(limit) {
if (Number.isNaN(limit)) {
limit = null;
}
return sharp.cache(limit);
};

View File

@@ -1,10 +1,13 @@
{
"name": "sharp",
"version": "0.0.5",
"author": "Lovell Fuller",
"description": "High performance module to resize JPEG and PNG images using the libvips image processing library",
"version": "0.3.0",
"author": "Lovell Fuller <npm@lovell.info>",
"contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>"
],
"description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library",
"scripts": {
"test": "node tests/perf.js"
"test": "node tests/unit && node tests/perf"
},
"main": "index.js",
"repository": {
@@ -14,23 +17,31 @@
"keywords": [
"jpeg",
"png",
"webp",
"tiff",
"gif",
"resize",
"thumbnail",
"sharpen",
"crop",
"embed",
"libvips",
"fast"
"vips",
"fast",
"buffer"
],
"dependencies": {
"nan": "^0.8.0"
},
"devDependencies": {
"imagemagick": "*",
"gm": "*",
"epeg": "*",
"async": "*",
"benchmark": "*"
"imagemagick": "^0.1.3",
"imagemagick-native": "^0.2.9",
"gm": "^1.14.2",
"async": "^0.6.2",
"benchmark": "^1.0.0"
},
"license": "Apache 2.0",
"engines": {
"node": ">=0.8"
"node": ">=0.10"
}
}
}

View File

@@ -1,193 +1,382 @@
#include <node.h>
#include <node_buffer.h>
#include <math.h>
#include <string>
#include <vector>
#include <string.h>
#include <vips/vips.h>
#include "nan.h"
using namespace v8;
using namespace node;
// Free VipsImage children when object goes out of scope
// Thanks due to https://github.com/dosx/node-vips
class ImageFreer {
public:
ImageFreer() {}
~ImageFreer() {
for (uint16_t i = 0; i < v_.size(); i++) {
if (v_[i] != NULL) {
g_object_unref(v_[i]);
}
}
v_.clear();
}
void add(VipsImage* i) { v_.push_back(i); }
private:
std::vector<VipsImage*> v_;
};
struct ResizeBaton {
std::string src;
std::string dst;
int cols;
int rows;
struct resize_baton {
std::string file_in;
void* buffer_in;
size_t buffer_in_len;
std::string file_out;
void* buffer_out;
size_t buffer_out_len;
int width;
int height;
bool crop;
int embed;
VipsExtend extend;
bool sharpen;
bool progessive;
VipsAccess access_method;
std::string err;
Persistent<Function> callback;
resize_baton(): buffer_in_len(0), buffer_out_len(0) {}
};
bool EndsWith(std::string const &str, std::string const &end) {
typedef enum {
JPEG,
PNG,
WEBP,
TIFF,
MAGICK
} ImageType;
unsigned char MARKER_JPEG[] = {0xff, 0xd8};
unsigned char MARKER_PNG[] = {0x89, 0x50};
unsigned char MARKER_WEBP[] = {0x52, 0x49};
bool ends_with(std::string const &str, std::string const &end) {
return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
}
void ResizeAsync(uv_work_t *work) {
ResizeBaton* baton = static_cast<ResizeBaton*>(work->data);
VipsImage *in = vips_image_new_mode((baton->src).c_str(), "p");
if (EndsWith(baton->src, ".jpg") || EndsWith(baton->src, ".jpeg")) {
vips_jpegload((baton->src).c_str(), &in, NULL);
} else if (EndsWith(baton->src, ".png")) {
vips_pngload((baton->src).c_str(), &in, NULL);
} else {
(baton->err).append("Unsupported input file type");
return;
}
if (in == NULL) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
ImageFreer freer;
freer.add(in);
VipsImage* img = in;
VipsImage* t[4];
if (im_open_local_array(img, t, 4, "temp", "p")) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
double xfactor = static_cast<double>(img->Xsize) / std::max(baton->cols, 1);
double yfactor = static_cast<double>(img->Ysize) / std::max(baton->rows, 1);
double factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
factor = std::max(factor, 1.0);
int shrink = floor(factor);
double residual = shrink / factor;
// Use im_shrink with the integral reduction
if (im_shrink(img, t[0], shrink, shrink)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
// Use im_affinei with the remaining float part using bilinear interpolation
if (im_affinei_all(t[0], t[1], vips_interpolate_bilinear_static(), residual, 0, 0, residual, 0, 0)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[1];
if (baton->crop) {
int width = std::min(img->Xsize, baton->cols);
int height = std::min(img->Ysize, baton->rows);
int left = (img->Xsize - width + 1) / 2;
int top = (img->Ysize - height + 1) / 2;
if (im_extract_area(img, t[2], left, top, width, height)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[2];
} else {
int left = (baton->cols - img->Xsize) / 2;
int top = (baton->rows - img->Ysize) / 2;
if (im_embed(img, t[2], baton->embed, left, top, baton->cols, baton->rows)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[2];
}
// Mild sharpen
INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3,
-1, -1, -1,
-1, 32, -1,
-1, -1, -1);
sharpen->scale = 24;
if (im_conv(img, t[3], sharpen)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[3];
if (EndsWith(baton->dst, ".jpg") || EndsWith(baton->dst, ".jpeg")) {
if (vips_jpegsave(img, baton->dst.c_str(), "Q", 80, "profile", "none", "optimize_coding", TRUE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else if (EndsWith(baton->dst, ".png")) {
if (vips_pngsave(img, baton->dst.c_str(), "compression", 6, "interlace", FALSE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else {
(baton->err).append("Unsupported output file type");
}
bool is_jpeg(std::string const &str) {
return ends_with(str, ".jpg") || ends_with(str, ".jpeg") || ends_with(str, ".JPG") || ends_with(str, ".JPEG");
}
void ResizeAsyncAfter(uv_work_t *work, int status) {
HandleScope scope;
ResizeBaton *baton = static_cast<ResizeBaton*>(work->data);
Local<Value> argv[1];
if (!baton->err.empty()) {
argv[0] = String::New(baton->err.data(), baton->err.size());
} else {
argv[0] = Local<Value>::New(Null());
}
baton->callback->Call(Context::GetCurrent()->Global(), 1, argv);
baton->callback.Dispose();
delete baton;
delete work;
bool is_png(std::string const &str) {
return ends_with(str, ".png") || ends_with(str, ".PNG");
}
Handle<Value> Resize(const Arguments& args) {
HandleScope scope;
bool is_webp(std::string const &str) {
return ends_with(str, ".webp") || ends_with(str, ".WEBP");
}
bool is_tiff(std::string const &str) {
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
}
void resize_error(resize_baton *baton, VipsImage *unref) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(unref);
vips_thread_shutdown();
return;
}
class ResizeWorker : public NanAsyncWorker {
public:
ResizeWorker(NanCallback *callback, resize_baton *baton)
: NanAsyncWorker(callback), baton(baton) {}
~ResizeWorker() {}
void Execute () {
// Input
ImageType inputImageType = JPEG;
VipsImage *in = vips_image_new();
if (baton->buffer_in_len > 1) {
if (memcmp(MARKER_JPEG, baton->buffer_in, 2) == 0) {
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if(memcmp(MARKER_PNG, baton->buffer_in, 2) == 0) {
inputImageType = PNG;
if (vips_pngload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if(memcmp(MARKER_WEBP, baton->buffer_in, 2) == 0) {
inputImageType = WEBP;
if (vips_webpload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else {
resize_error(baton, in);
(baton->err).append("Unsupported input buffer");
return;
}
} else if (vips_foreign_is_a("jpegload", baton->file_in.c_str())) {
if (vips_jpegload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if (vips_foreign_is_a("pngload", baton->file_in.c_str())) {
inputImageType = PNG;
if (vips_pngload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if (vips_foreign_is_a("webpload", baton->file_in.c_str())) {
inputImageType = WEBP;
if (vips_webpload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if (vips_foreign_is_a("tiffload", baton->file_in.c_str())) {
inputImageType = TIFF;
if (vips_tiffload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if(vips_foreign_is_a("magickload", (baton->file_in).c_str())) {
inputImageType = MAGICK;
if (vips_magickload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else {
resize_error(baton, in);
(baton->err).append("Unsupported input file " + baton->file_in);
return;
}
// Scaling calculations
double factor;
if (baton->width > 0 && baton->height > 0) {
// Fixed width and height
double xfactor = (double)(in->Xsize) / (double)(baton->width);
double yfactor = (double)(in->Ysize) / (double)(baton->height);
factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
} else if (baton->width > 0) {
// Fixed width, auto height
factor = (double)(in->Xsize) / (double)(baton->width);
baton->height = floor((double)(in->Ysize) / factor);
} else if (baton->height > 0) {
// Fixed height, auto width
factor = (double)(in->Ysize) / (double)(baton->height);
baton->width = floor((double)(in->Xsize) / factor);
} else {
// Identity transform
factor = 1;
baton->width = in->Xsize;
baton->height = in->Ysize;
}
int shrink = floor(factor);
if (shrink < 1) {
shrink = 1;
}
double residual = shrink / (double)factor;
// Try to use libjpeg shrink-on-load
int shrink_on_load = 1;
if (inputImageType == JPEG) {
if (shrink >= 8) {
factor = factor / 8;
shrink_on_load = 8;
} else if (shrink >= 4) {
factor = factor / 4;
shrink_on_load = 4;
} else if (shrink >= 2) {
factor = factor / 2;
shrink_on_load = 2;
}
}
VipsImage *shrunk_on_load = vips_image_new();
if (shrink_on_load > 1) {
// Recalculate integral shrink and double residual
factor = std::max(factor, 1.0);
shrink = floor(factor);
residual = shrink / factor;
// Reload input using shrink-on-load
if (baton->buffer_in_len > 1) {
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, in);
}
} else {
if (vips_jpegload((baton->file_in).c_str(), &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, in);
}
}
} else {
vips_copy(in, &shrunk_on_load, NULL);
}
g_object_unref(in);
VipsImage *shrunk = vips_image_new();
if (shrink > 1) {
// Use vips_shrink with the integral reduction
if (vips_shrink(shrunk_on_load, &shrunk, shrink, shrink, NULL)) {
return resize_error(baton, shrunk_on_load);
}
} else {
vips_copy(shrunk_on_load, &shrunk, NULL);
}
g_object_unref(shrunk_on_load);
// Use vips_affine with the remaining float part using bilinear interpolation
VipsImage *affined = vips_image_new();
if (residual != 0) {
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) {
return resize_error(baton, shrunk);
}
} else {
vips_copy(shrunk, &affined, NULL);
}
g_object_unref(shrunk);
// Crop/embed
VipsImage *canvased = vips_image_new();
if (affined->Xsize != baton->width || affined->Ysize != baton->height) {
if (baton->crop) {
// Crop
int width = std::min(affined->Xsize, baton->width);
int height = std::min(affined->Ysize, baton->height);
int left = (affined->Xsize - width + 1) / 2;
int top = (affined->Ysize - height + 1) / 2;
if (vips_extract_area(affined, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, affined);
}
} else {
// Embed
int left = (baton->width - affined->Xsize) / 2;
int top = (baton->height - affined->Ysize) / 2;
if (vips_embed(affined, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) {
return resize_error(baton, affined);
}
}
} else {
vips_copy(affined, &canvased, NULL);
}
g_object_unref(affined);
// Mild sharpen
VipsImage *sharpened = vips_image_new();
if (baton->sharpen) {
INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3,
-1, -1, -1,
-1, 32, -1,
-1, -1, -1);
sharpen->scale = 24;
if (im_conv(canvased, sharpened, sharpen)) {
return resize_error(baton, canvased);
}
} else {
vips_copy(canvased, &sharpened, NULL);
}
g_object_unref(canvased);
// Output
if (baton->file_out == "__jpeg" || (baton->file_out == "__input" && inputImageType == JPEG)) {
// Write JPEG to buffer
if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened);
}
} else if (baton->file_out == "__png" || (baton->file_out == "__input" && inputImageType == PNG)) {
// Write PNG to buffer
if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened);
}
} else if (baton->file_out == "__webp" || (baton->file_out == "__input" && inputImageType == WEBP)) {
// Write WEBP to buffer
if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, NULL)) {
return resize_error(baton, sharpened);
}
} else if (is_jpeg(baton->file_out)) {
// Write JPEG to file
if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened);
}
} else if (is_png(baton->file_out)) {
// Write PNG to file
if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened);
}
} else if (is_webp(baton->file_out)) {
// Write WEBP to file
if (vips_webpsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, NULL)) {
return resize_error(baton, sharpened);
}
} else if (is_tiff(baton->file_out)) {
// Write TIFF to file
if (vips_tiffsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", 80, NULL)) {
return resize_error(baton, sharpened);
}
} else {
(baton->err).append("Unsupported output " + baton->file_out);
}
g_object_unref(sharpened);
vips_thread_shutdown();
}
void HandleOKCallback () {
NanScope();
Handle<Value> argv[2] = { Null(), Null() };
if (!baton->err.empty()) {
// Error
argv[0] = String::New(baton->err.data(), baton->err.size());
} else if (baton->buffer_out_len > 0) {
// Buffer
argv[1] = NanNewBufferHandle((char *)baton->buffer_out, baton->buffer_out_len);
g_free(baton->buffer_out);
}
delete baton;
callback->Call(2, argv);
}
private:
resize_baton* baton;
};
NAN_METHOD(resize) {
NanScope();
ResizeBaton *baton = new ResizeBaton;
baton->src = *String::Utf8Value(args[0]->ToString());
baton->dst = *String::Utf8Value(args[1]->ToString());
baton->cols = args[2]->Int32Value();
baton->rows = args[3]->Int32Value();
Local<String> canvas = args[4]->ToString();
resize_baton *baton = new resize_baton;
baton->file_in = *String::Utf8Value(args[0]->ToString());
if (args[1]->IsObject()) {
Local<Object> buffer = args[1]->ToObject();
baton->buffer_in_len = Buffer::Length(buffer);
baton->buffer_in = Buffer::Data(buffer);
}
baton->file_out = *String::Utf8Value(args[2]->ToString());
baton->width = args[3]->Int32Value();
baton->height = args[4]->Int32Value();
Local<String> canvas = args[5]->ToString();
if (canvas->Equals(String::NewSymbol("c"))) {
baton->crop = true;
baton->crop = true;
} else if (canvas->Equals(String::NewSymbol("w"))) {
baton->crop = false;
baton->embed = 4;
baton->extend = VIPS_EXTEND_WHITE;
} else if (canvas->Equals(String::NewSymbol("b"))) {
baton->crop = false;
baton->embed = 0;
}
baton->callback = Persistent<Function>::New(Local<Function>::Cast(args[5]));
baton->extend = VIPS_EXTEND_BLACK;
}
baton->sharpen = args[6]->BooleanValue();
baton->progessive = args[7]->BooleanValue();
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
uv_work_t *work = new uv_work_t;
work->data = baton;
uv_queue_work(uv_default_loop(), work, ResizeAsync, (uv_after_work_cb)ResizeAsyncAfter);
return Undefined();
NanCallback *callback = new NanCallback(args[9].As<v8::Function>());
NanAsyncQueueWorker(new ResizeWorker(callback, baton));
NanReturnUndefined();
}
NAN_METHOD(cache) {
NanScope();
// Set cache limit
if (args[0]->IsInt32()) {
vips_cache_set_max_mem(args[0]->Int32Value() * 1048576);
}
// Get cache statistics
Local<Object> cache = Object::New();
cache->Set(String::NewSymbol("current"), Number::New(vips_tracked_get_mem() / 1048576));
cache->Set(String::NewSymbol("high"), Number::New(vips_tracked_get_mem_highwater() / 1048576));
cache->Set(String::NewSymbol("limit"), Number::New(vips_cache_get_max_mem() / 1048576));
NanReturnValue(cache);
}
static void at_exit(void* arg) {
NanScope();
vips_shutdown();
}
extern "C" void init(Handle<Object> target) {
HandleScope scope;
NanScope();
vips_init("");
NODE_SET_METHOD(target, "resize", Resize);
};
AtExit(at_exit);
NODE_SET_METHOD(target, "resize", resize);
NODE_SET_METHOD(target, "cache", cache);
}
NODE_MODULE(sharp, init)

View File

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 813 KiB

BIN
tests/fixtures/4.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

BIN
tests/fixtures/Crash_test.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

BIN
tests/fixtures/G31D.TIF vendored Normal file

Binary file not shown.

32
tests/parallel.js Executable file
View File

@@ -0,0 +1,32 @@
var sharp = require("../index");
var fs = require("fs");
var path = require("path");
var assert = require("assert");
var async = require("async");
var inputJpg = path.join(__dirname, "fixtures/2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
var width = 720;
var height = 480;
async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
var start = new Date().getTime();
async.times(parallelism,
function(id, callback) {
sharp(inputJpg).resize(width, height).toBuffer(function(err, buffer) {
buffer = null;
callback(err, new Date().getTime() - start);
});
},
function(err, ids) {
assert(!err);
assert(ids.length === parallelism);
var mean = ids.reduce(function(a, b) {
return a + b;
}) / ids.length;
console.log(parallelism + " parallel calls: fastest=" + ids[0] + "ms slowest=" + ids[ids.length - 1] + "ms mean=" + mean + "ms");
next();
}
);
}, function() {
console.dir(sharp.cache());
});

View File

@@ -1,25 +1,41 @@
var sharp = require("../index");
var fs = require("fs");
var path = require("path");
var imagemagick = require("imagemagick");
var imagemagickNative = require("imagemagick-native");
var gm = require("gm");
var epeg = require("epeg");
var async = require("async");
var assert = require("assert");
var Benchmark = require("benchmark");
var inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = __dirname + "/output.jpg";
var fixturesPath = path.join(__dirname, "fixtures");
var inputPng = __dirname + "/50020484-00001.png"; // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
var outputPng = __dirname + "/output.png";
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = path.join(fixturesPath, "output.jpg");
var width = 640;
var inputPng = path.join(fixturesPath, "50020484-00001.png"); // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
var outputPng = path.join(fixturesPath, "output.png");
var inputWebp = path.join(fixturesPath, "4.webp"); // http://www.gstatic.com/webp/gallery/4.webp
var outputWebp = path.join(fixturesPath, "output.webp");
var inputTiff = path.join(fixturesPath, "G31D.TIF"); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
var outputTiff = path.join(fixturesPath, "output.tiff");
var inputGif = path.join(fixturesPath, "Crash_test.gif"); // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
var width = 720;
var height = 480;
// Disable libvips cache to ensure tests are as fair as they can be
sharp.cache(0);
async.series({
jpeg: function(callback) {
(new Benchmark.Suite("jpeg")).add("imagemagick", {
"defer": true,
"fn": function(deferred) {
var inputJpgBuffer = fs.readFileSync(inputJpg);
(new Benchmark.Suite("jpeg")).add("imagemagick-file-file", {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: inputJpg,
dstPath: outputJpg,
@@ -34,10 +50,22 @@ async.series({
}
});
}
}).add("gm", {
"defer": true,
"fn": function(deferred) {
gm(inputJpg).crop(width, height).quality(80).write(outputJpg, function (err) {
}).add("imagemagick-native-buffer-buffer", {
defer: true,
fn: function(deferred) {
imagemagickNative.convert({
srcData: inputJpgBuffer,
quality: 80,
width: width,
height: height,
format: 'JPEG'
});
deferred.resolve();
}
}).add("gm-buffer-file", {
defer: true,
fn: function(deferred) {
gm(inputJpgBuffer).resize(width, height).quality(80).write(outputJpg, function (err) {
if (err) {
throw err;
} else {
@@ -45,20 +73,119 @@ async.series({
}
});
}
}).add("epeg", {
"defer": true,
"fn": function(deferred) {
var image = new epeg.Image({path: inputJpg});
image.downsize(width, height, 80).saveTo(outputJpg);
deferred.resolve();
}
}).add("sharp", {
"defer": true,
"fn": function(deferred) {
sharp.crop(inputJpg, outputJpg, width, height, function(err) {
}).add("gm-buffer-buffer", {
defer: true,
fn: function(deferred) {
gm(inputJpgBuffer).resize(width, height).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(inputJpg).resize(width, height).quality(80).write(outputJpg, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("gm-file-buffer", {
defer: true,
fn: function(deferred) {
gm(inputJpg).resize(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-buffer-file", {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).write(outputJpg, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-buffer-buffer", {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).write(outputJpg, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-buffer", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-sharpen", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-progressive", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).progressive().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).sequentialRead().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
@@ -70,9 +197,10 @@ async.series({
}).run();
},
png: function(callback) {
(new Benchmark.Suite("png")).add("imagemagick", {
"defer": true,
"fn": function(deferred) {
var inputPngBuffer = fs.readFileSync(inputPng);
(new Benchmark.Suite("png")).add("imagemagick-file-file", {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: inputPng,
dstPath: outputPng,
@@ -86,10 +214,21 @@ async.series({
}
});
}
}).add("gm", {
"defer": true,
"fn": function(deferred) {
gm(inputPng).crop(width, height).write(outputPng, function (err) {
}).add("imagemagick-native-buffer-buffer", {
defer: true,
fn: function(deferred) {
imagemagickNative.convert({
srcData: inputPngBuffer,
width: width,
height: height,
format: 'PNG'
});
deferred.resolve();
}
}).add("gm-file-file", {
defer: true,
fn: function(deferred) {
gm(inputPng).resize(width, height).write(outputPng, function (err) {
if (err) {
throw err;
} else {
@@ -97,13 +236,96 @@ async.series({
}
});
}
}).add("sharp", {
"defer": true,
"fn": function(deferred) {
sharp.crop(inputPng, outputPng, width, height, function(err) {
}).add("gm-file-buffer", {
defer: true,
fn: function(deferred) {
gm(inputPng).resize(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-buffer-file", {
defer: true,
fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).write(outputPng, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-buffer-buffer", {
defer: true,
fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).write(outputPng, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-buffer", {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-sharpen", {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-progressive", {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).progressive().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputPng).sequentialRead().resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
@@ -113,9 +335,171 @@ async.series({
}).on("complete", function() {
callback(null, this.filter("fastest").pluck("name"));
}).run();
}
},
webp: function(callback) {
var inputWebpBuffer = fs.readFileSync(inputWebp);
(new Benchmark.Suite("webp")).add("sharp-buffer-file", {
defer: true,
fn: function(deferred) {
sharp(inputWebpBuffer).resize(width, height).write(outputWebp, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-buffer-buffer", {
defer: true,
fn: function(deferred) {
sharp(inputWebpBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputWebp).resize(width, height).write(outputWebp, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-buffer", {
defer: true,
fn: function(deferred) {
sharp(inputWebp).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-sharpen", {
defer: true,
fn: function(deferred) {
sharp(inputWebp).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputWebp).sequentialRead().resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log("webp " + String(event.target));
}).on("complete", function() {
callback(null, this.filter("fastest").pluck("name"));
}).run();
},
tiff: function(callback) {
(new Benchmark.Suite("tiff")).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputTiff).resize(width, height).write(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-file-sharpen", {
defer: true,
fn: function(deferred) {
sharp(inputTiff).resize(width, height).sharpen().write(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-file-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputTiff).sequentialRead().resize(width, height).write(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log("tiff " + String(event.target));
}).on("complete", function() {
callback(null, this.filter("fastest").pluck("name"));
}).run();
},
gif: function(callback) {
(new Benchmark.Suite("gif")).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputGif).resize(width, height).write(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-file-sharpen", {
defer: true,
fn: function(deferred) {
sharp(inputGif).resize(width, height).sharpen().write(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-file-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputGif).sequentialRead().resize(width, height).write(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log("gif " + String(event.target));
}).on("complete", function() {
callback(null, this.filter("fastest").pluck("name"));
}).run();
}
}, function(err, results) {
results.forEach(function(format, fastest) {
assert(fastest === "sharp", "sharp was slower than " + fastest + " for " + format);
assert(!err, err);
Object.keys(results).forEach(function(format) {
if (results[format].toString().substr(0, 5) !== "sharp") {
console.log("sharp was slower than " + results[format] + " for " + format);
}
});
console.dir(sharp.cache());
});

68
tests/random.js Executable file
View File

@@ -0,0 +1,68 @@
var sharp = require("../index");
var fs = require("fs");
var path = require("path");
var imagemagick = require("imagemagick");
var gm = require("gm");
var async = require("async");
var assert = require("assert");
var Benchmark = require("benchmark");
var fixturesPath = path.join(__dirname, "fixtures");
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = path.join(fixturesPath, "output.jpg");
var min = 320;
var max = 960;
var randomDimension = function() {
return Math.random() * (max - min) + min;
};
new Benchmark.Suite("random").add("imagemagick", {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: inputJpg,
dstPath: outputJpg,
quality: 0.8,
width: randomDimension(),
height: randomDimension()
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("gm", {
defer: true,
fn: function(deferred) {
gm(inputJpg).resize(randomDimension(), randomDimension()).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp", {
defer: true,
fn: function(deferred) {
sharp(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").pluck("name");
assert.strictEqual("sharp", String(winner), "sharp was slower than " + winner);
console.dir(sharp.cache());
}).run();

73
tests/unit.js Executable file
View File

@@ -0,0 +1,73 @@
var sharp = require("../index");
var path = require("path");
var imagemagick = require("imagemagick");
var assert = require("assert");
var async = require("async");
var fixturesPath = path.join(__dirname, "fixtures");
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = path.join(fixturesPath, "output.jpg");
async.series([
// Resize with exact crop
function(done) {
sharp(inputJpg).resize(320, 240).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Resize to fixed width
function(done) {
sharp(inputJpg).resize(320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(261, features.height);
done();
});
});
},
// Resize to fixed height
function(done) {
sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(391, features.width);
assert.strictEqual(320, features.height);
done();
});
});
},
// Identity transform
function(done) {
sharp(inputJpg).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(2725, features.width);
assert.strictEqual(2225, features.height);
done();
});
});
},
// Upscale
function(done) {
sharp(inputJpg).resize(3000).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(3000, features.width);
assert.strictEqual(2449, features.height);
done();
});
});
}
]);