Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
515b4656e6 | ||
|
|
34c96ff925 | ||
|
|
b8a04cc4ef | ||
|
|
ddc3f6e9c6 | ||
|
|
3699e61c20 | ||
|
|
2820218609 | ||
|
|
eb3e739f7b | ||
|
|
87f6e83988 | ||
|
|
5728efd32b | ||
|
|
bac367b005 | ||
|
|
0d89131f66 | ||
|
|
8380be4be3 | ||
|
|
d0f51363bf | ||
|
|
15160d3b61 | ||
|
|
c5efb77bad | ||
|
|
b877751b2d | ||
|
|
40db482fd8 | ||
|
|
98554e919c | ||
|
|
017bf1e905 | ||
|
|
6498fc3a9e | ||
|
|
8ba71c94f4 | ||
|
|
f2f3eb76e1 | ||
|
|
5fe945fca8 | ||
|
|
8ef0851a49 | ||
|
|
e45956db6c | ||
|
|
7cc9f7e2e0 | ||
|
|
df3903532d | ||
|
|
46456c9a2a | ||
|
|
e98f2fc013 | ||
|
|
7df7a505ee | ||
|
|
d40bdcc6ac | ||
|
|
1cce56b024 | ||
|
|
2126f9afc1 | ||
|
|
41420eedcf | ||
|
|
1b6ab19b6d | ||
|
|
fbe5c18762 | ||
|
|
8acb0ed5d0 | ||
|
|
430e04d894 | ||
|
|
012edb4379 | ||
|
|
11ead360a9 | ||
|
|
84a059d7e3 | ||
|
|
b1b070ae5c | ||
|
|
4eb910fec9 | ||
|
|
f0a9d82bf7 | ||
|
|
6d20a1ca81 | ||
|
|
eca2787213 | ||
|
|
8d146accf3 | ||
|
|
b635d015cd | ||
|
|
261a90c8a2 | ||
|
|
4ae22b3425 | ||
|
|
c9aa9c7723 | ||
|
|
5e0b5969da | ||
|
|
ae6d5e69b1 | ||
|
|
5ccc2bca97 | ||
|
|
46b701c85c | ||
|
|
7319533969 | ||
|
|
9a05684302 | ||
|
|
906311d403 | ||
|
|
4de9a2435f | ||
|
|
a94dd2b354 | ||
|
|
bc3311cbad | ||
|
|
7b03eb89d7 | ||
|
|
15a519ebd9 | ||
|
|
3f8e9f6487 | ||
|
|
39688371a8 | ||
|
|
e32faac17a | ||
|
|
6c96bd0d37 | ||
|
|
6b5f2028b7 | ||
|
|
276ba5228b | ||
|
|
ad7735a0a6 | ||
|
|
88edad3fae | ||
|
|
308d1971d8 | ||
|
|
6622045172 | ||
|
|
f68ba8ea57 | ||
|
|
2e427bb28a | ||
|
|
efc7504961 | ||
|
|
8118613fa0 | ||
|
|
eb6a221cee | ||
|
|
acdfe02502 | ||
|
|
2e106f8e2e | ||
|
|
10496881f1 | ||
|
|
e275f6f5dd | ||
|
|
d635c297a2 | ||
|
|
817c0a2a5a | ||
|
|
92fd34c627 | ||
|
|
43086cf134 | ||
|
|
e607bac31c | ||
|
|
f8338e7c4f | ||
|
|
8322b442e0 | ||
|
|
afc51df4d8 | ||
|
|
e3a70c1075 | ||
|
|
e3ee2b2976 | ||
|
|
cb285a6fb3 | ||
|
|
aed3ca63b3 | ||
|
|
cbcf5e0dcc | ||
|
|
59f5c2d31b | ||
|
|
d1b47ef419 | ||
|
|
0954ca6adf | ||
|
|
c9d7f43bd9 | ||
|
|
481741315d | ||
|
|
cae1dbdb89 | ||
|
|
200d5a9312 | ||
|
|
e9ca25cb45 | ||
|
|
33f24d41e7 | ||
|
|
45d5f12a63 | ||
|
|
8785ca4331 | ||
|
|
1ecdf97bdb | ||
|
|
3703ee41aa | ||
|
|
c8f023d8ba | ||
|
|
fe773733cd | ||
|
|
19bec9346e | ||
|
|
9bd335079f | ||
|
|
b6dc179551 | ||
|
|
e96fd8b9de | ||
|
|
6a2816e917 | ||
|
|
06d88de5a2 | ||
|
|
a292e1fe8e | ||
|
|
08bb35e7af | ||
|
|
0e89a5bbf2 | ||
|
|
2a0d79a78b | ||
|
|
764b57022b | ||
|
|
5f61331d1a | ||
|
|
d0e6a4c0f3 | ||
|
|
f99e42d447 | ||
|
|
9b4387be97 | ||
|
|
9c3631ecb7 | ||
|
|
31ca68fb14 | ||
|
|
d5d85a8697 | ||
|
|
ae9a8b0f57 | ||
|
|
0899252a72 | ||
|
|
16551bc058 | ||
|
|
e9d196f696 | ||
|
|
2f97d04dfa | ||
|
|
e4ca8f44ec | ||
|
|
6b3dc1e350 | ||
|
|
7ffcdb79e0 | ||
|
|
10ce7c6693 | ||
|
|
ccd6012152 | ||
|
|
377662fffc | ||
|
|
d509458ba1 | ||
|
|
be8f35d830 | ||
|
|
7e8af63129 | ||
|
|
f7b8ce1287 |
6
.gitignore
vendored
@@ -12,7 +12,5 @@ logs
|
||||
results
|
||||
build
|
||||
node_modules
|
||||
tests/output.jpg
|
||||
tests/output.png
|
||||
|
||||
npm-debug.log
|
||||
tests/fixtures/output.*
|
||||
tests/libvips.supp
|
||||
|
||||
18
.npmignore
Normal file
@@ -0,0 +1,18 @@
|
||||
lib-cov
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.gz
|
||||
|
||||
pids
|
||||
logs
|
||||
results
|
||||
build
|
||||
node_modules
|
||||
|
||||
.gitignore
|
||||
tests
|
||||
.travis.yml
|
||||
17
.travis.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.11"
|
||||
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 libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libexif-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=yes --without-orc --without-python --without-fftw
|
||||
- make
|
||||
- sudo make install
|
||||
- sudo ldconfig
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
501
README.md
@@ -1,46 +1,111 @@
|
||||
# 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)
|
||||
* [Thanks](https://github.com/lovell/sharp#thanks)
|
||||
* [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.
|
||||
|
||||
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.
|
||||
Memory usage is kept to a minimum, no child processes are spawned, everything remains non-blocking thanks to _libuv_ and Promises/A+ are supported.
|
||||
|
||||
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.
|
||||
This module supports reading and writing JPEG, PNG and WebP images to and from Streams, Buffer objects and the filesystem. It also supports reading images of many other types from the filesystem via libmagick++ or libgraphicsmagick++ if present.
|
||||
|
||||
Performance is 4x-8x faster than ImageMagick and 2x-4x faster than GraphicsMagick, based mainly on the number of CPU cores available.
|
||||
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/).
|
||||
|
||||
## Prerequisites
|
||||
Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent.
|
||||
|
||||
* Node.js v0.8+
|
||||
* node-gyp
|
||||
* [libvips](https://github.com/jcupitt/libvips) v7.37+
|
||||
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](https://github.com/jcupitt).
|
||||
|
||||
For the sharpest results, please compile libvips from source.
|
||||
|
||||
If you prefer to run a stable, package-managed environment such as Ubuntu 12.04 LTS, [v0.0.3](https://github.com/lovell/sharp/tree/v0.0.3) will work with the libvips-dev package.
|
||||
|
||||
## 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)
|
||||
_libvips_ can take advantage of [liborc](http://code.entropywave.com/orc/) if present. Warning: versions of _liborc_ prior to 0.4.19 suffer [memory leaks](https://github.com/lovell/sharp/issues/21#issuecomment-42367306) and version 0.4.19 suffers [buffer overflows](https://github.com/lovell/sharp/issues/21#issuecomment-44813498).
|
||||
|
||||
Scale and crop `inputPath` to `width` x `height` and write to `outputPath` calling `callback` when complete.
|
||||
### Install libvips on Mac OS
|
||||
|
||||
Example:
|
||||
brew install homebrew/science/vips --with-webp --with-graphicsmagick
|
||||
|
||||
A missing or incorrectly configured _Xcode Command Line Tools_ installation [can lead](https://github.com/lovell/sharp/issues/80) to a `library not found for -ljpeg` error. If so, please try:
|
||||
|
||||
xcode-select --install
|
||||
|
||||
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 Linux
|
||||
|
||||
#### Ubuntu 14.x
|
||||
|
||||
sudo apt-get install libvips-dev
|
||||
|
||||
#### Ubuntu 13.x
|
||||
|
||||
Compiling from source is recommended:
|
||||
|
||||
sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev libexif-dev libxml2-dev swig libmagickwand-dev
|
||||
git clone https://github.com/jcupitt/libvips.git
|
||||
cd libvips
|
||||
git checkout 7.38
|
||||
./bootstrap.sh
|
||||
./configure --enable-debug=no --enable-cxx=yes --without-python --without-orc --without-fftw
|
||||
make
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
|
||||
#### Ubuntu 12.x
|
||||
|
||||
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 commands first:
|
||||
|
||||
sudo add-apt-repository ppa:lyrasis/precise-backports
|
||||
sudo apt-get update
|
||||
sudo apt-get install libtiff4-dev
|
||||
|
||||
Then follow Ubuntu 13.x instructions.
|
||||
|
||||
### Install libvips on Redhat/Centos Linux
|
||||
|
||||
#### Centos 6
|
||||
|
||||
sudo yum groupinstall -y "Development Tools"
|
||||
sudo yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel ImageMagick-devel
|
||||
sudo yum install -y http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm
|
||||
sudo yum install -y --enablerepo=nux-dextop gobject-introspection-devel
|
||||
sudo yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
|
||||
sudo yum install -y --enablerepo=remi libwebp-devel
|
||||
git clone https://github.com/jcupitt/libvips.git
|
||||
cd libvips
|
||||
git checkout 7.40
|
||||
./bootstrap.sh
|
||||
./configure --prefix=/usr --enable-docs=no --enable-debug=no --enable-cxx=yes --without-orc --without-python --without-fftw
|
||||
make
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
|
||||
### Install libvips on Heroku
|
||||
|
||||
[Alessandro Tagliapietra](https://github.com/alex88) maintains an [Heroku buildpack for libvips](https://github.com/alex88/heroku-buildpack-vips) and its dependencies.
|
||||
|
||||
## Usage examples
|
||||
|
||||
```javascript
|
||||
sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
|
||||
var sharp = require('sharp');
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp('input.jpg').resize(300, 200).toFile('output.jpg', function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
@@ -49,64 +114,386 @@ 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
|
||||
var transformer = sharp().resize(300, 200).crop(sharp.gravity.north);
|
||||
readableStream.pipe(transformer).pipe(writableStream);
|
||||
// Read image data from readableStream, resize and write image data to writableStream
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp.embedWhite("input.jpg", "output.png", 200, 300, function(err) {
|
||||
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
|
||||
var image = sharp(inputJpg);
|
||||
image.metadata(function(err, metadata) {
|
||||
image.resize(metadata.width / 2).webp().toBuffer(function(err, outputBuffer, info) {
|
||||
// outputBuffer contains a WebP image half the width and height of the original JPEG
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 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
|
||||
var pipeline = sharp()
|
||||
.rotate()
|
||||
.resize(null, 200)
|
||||
.progressive()
|
||||
.toBuffer(function(err, outputBuffer, info) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// outputBuffer contains 200px high progressive JPEG image data,
|
||||
// auto-rotated using EXIF Orientation tag
|
||||
// info.width and info.height contain the dimensions of the resized image
|
||||
});
|
||||
readableStream.pipe(pipeline);
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp.embedBlack("input.png", "output.png", 200, 300, function(err) {
|
||||
sharp('input.png')
|
||||
.rotate(180)
|
||||
.resize(300)
|
||||
.sharpen()
|
||||
.withMetadata()
|
||||
.quality(90)
|
||||
.webp()
|
||||
.toBuffer()
|
||||
.then(function(outputBuffer) {
|
||||
// outputBuffer contains 300px wide, upside down, sharpened,
|
||||
// with metadata, 90% quality WebP image data
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
http.createServer(function(request, response) {
|
||||
response.writeHead(200, {'Content-Type': 'image/webp'});
|
||||
sharp('input.jpg').rotate().resize(200).webp().pipe(response);
|
||||
}).listen(8000);
|
||||
// Create HTTP server that always returns auto-rotated 'input.jpg',
|
||||
// resized to 200 pixels wide, in WebP format
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp(inputBuffer)
|
||||
.resize(200, 300)
|
||||
.interpolateWith(sharp.interpolator.nohalo)
|
||||
.embedWhite()
|
||||
.toFile('output.tiff')
|
||||
.then(function() {
|
||||
// output.tiff is a 200 pixels wide and 300 pixels high image
|
||||
// containing a bicubic scaled version, embedded on a white canvas,
|
||||
// of the image data in inputBuffer
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp('input.gif').resize(200, 300).embedBlack().webp().toBuffer(function(err, outputBuffer) {
|
||||
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
|
||||
// outputBuffer contains WebP image data of a 200 pixels wide and 300 pixels high
|
||||
// containing a scaled version, embedded on a black canvas, of input.gif
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp(inputBuffer).resize(200, 200).max().jpeg().toBuffer().then(function(outputBuffer) {
|
||||
// outputBuffer contains JPEG image data no wider than 200 pixels and no higher
|
||||
// than 200 pixels regardless of the inputBuffer image dimensions
|
||||
});
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Input methods
|
||||
|
||||
#### sharp([input])
|
||||
|
||||
Constructor to which further methods are chained. `input`, if present, 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.
|
||||
|
||||
The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
|
||||
|
||||
JPEG, PNG or WebP format image data can be streamed into the object when `input` is not provided.
|
||||
|
||||
JPEG, PNG or WebP format image data can be streamed out from this object.
|
||||
|
||||
#### metadata([callback])
|
||||
|
||||
Fast access to image metadata without decoding any compressed image data.
|
||||
|
||||
`callback`, if present, gets the arguments `(err, metadata)` where `metadata` has the attributes:
|
||||
|
||||
* `format`: Name of decoder to be used to decompress image data e.g. `jpeg`, `png`, `webp` (for file-based input additionally `tiff` and `magick`)
|
||||
* `width`: Number of pixels wide
|
||||
* `height`: Number of pixels high
|
||||
* `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L502)
|
||||
* `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
|
||||
* `orientation`: Number value of the EXIF Orientation header, if present
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
#### 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.
|
||||
|
||||
### Image transformation options
|
||||
|
||||
#### resize(width, [height])
|
||||
|
||||
Scale output 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([gravity])
|
||||
|
||||
Crop the resized image to the exact size specified, the default behaviour.
|
||||
|
||||
`gravity`, if present, is an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north`.
|
||||
|
||||
Possible values are `north`, `east`, `south`, `west`, `center` and `centre`. The default gravity is `center`/`centre`.
|
||||
|
||||
#### max()
|
||||
|
||||
Preserving aspect ratio, resize the image to the maximum width or height specified.
|
||||
|
||||
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
|
||||
|
||||
#### 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.
|
||||
|
||||
#### rotate([angle])
|
||||
|
||||
Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag.
|
||||
|
||||
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
|
||||
|
||||
Use this method without `angle` to determine the angle from EXIF data. Mirroring is currently unsupported.
|
||||
|
||||
#### withoutEnlargement()
|
||||
|
||||
Do not enlarge the output image if the input image width *or* height are already less than the required dimensions.
|
||||
|
||||
This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification".
|
||||
|
||||
#### sharpen()
|
||||
|
||||
Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
|
||||
|
||||
#### interpolateWith(interpolator)
|
||||
|
||||
Use the given interpolator for image resizing, where `interpolator` is an attribute of the `sharp.interpolator` Object e.g. `sharp.interpolator.bicubic`.
|
||||
|
||||
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), the default and fastest image reduction interpolation.
|
||||
* `bicubic`: Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation), which typically reduces performance by 5%.
|
||||
* `vertexSplitQuadraticBasisSpline`: Use [VSQBS interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48), which prevents "staircasing" and typically reduces performance by 5%.
|
||||
* `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)" and typically reduces performance by a factor of 2.
|
||||
* `nohalo`: Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/), which prevents acutance and typically reduces performance by a factor of 3.
|
||||
|
||||
#### gamma([gamma])
|
||||
|
||||
Apply a gamma correction by reducing the encoding (darken) pre-resize at a factor of `1/gamma` then increasing the encoding (brighten) post-resize at a factor of `gamma`.
|
||||
|
||||
`gamma`, if present, is a Number betweem 1 and 3. The default value is `2.2`, a suitable approximation for sRGB images.
|
||||
|
||||
This can improve the perceived brightness of a resized image in non-linear colour spaces.
|
||||
|
||||
JPEG input images will not take advantage of the shrink-on-load performance optimisation when applying a gamma correction.
|
||||
|
||||
### Output options
|
||||
|
||||
#### jpeg()
|
||||
|
||||
Use JPEG format for the output image.
|
||||
|
||||
#### png()
|
||||
|
||||
Use PNG format for the output image.
|
||||
|
||||
#### webp()
|
||||
|
||||
Use WebP format for the output image.
|
||||
|
||||
#### quality(quality)
|
||||
|
||||
The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`.
|
||||
|
||||
`quality` is a Number between 1 and 100.
|
||||
|
||||
#### 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.
|
||||
|
||||
#### withMetadata([boolean])
|
||||
|
||||
Include all metadata (ICC, EXIF, XMP) from the input image in the output image. The default behaviour is to strip all metadata.
|
||||
|
||||
#### compressionLevel(compressionLevel)
|
||||
|
||||
An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`.
|
||||
|
||||
`compressionLevel` is a Number between -1 and 9.
|
||||
|
||||
### Output methods
|
||||
|
||||
#### toFile(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`, if present, is called with two arguments `(err, info)` where:
|
||||
|
||||
* `err` contains an error message, if any.
|
||||
* `info` contains the output image `format`, `width` and `height`.
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
#### toBuffer([callback])
|
||||
|
||||
Write image data to a Buffer, the format of which will match the input image by default. JPEG, PNG and WebP are supported.
|
||||
|
||||
`callback`, if present, gets three arguments `(err, buffer, info)` where:
|
||||
|
||||
* `err` is an error message, if any.
|
||||
* `buffer` is the output image data.
|
||||
* `info` contains the output image `format`, `width` and `height`.
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
### Utility methods
|
||||
|
||||
#### sharp.cache([memory], [items])
|
||||
|
||||
If `memory` or `items` are provided, set the limits of _libvips'_ operation cache.
|
||||
|
||||
* `memory` is the maximum memory in MB to use for this cache, with a default value of 100
|
||||
* `items` is the maximum number of operations to cache, with a default value of 500
|
||||
|
||||
This method always returns cache statistics, useful for determining how much working memory is required for a particular task.
|
||||
|
||||
```javascript
|
||||
var stats = sharp.cache(); // { current: 75, high: 99, memory: 100, items: 500 }
|
||||
sharp.cache(200); // { current: 75, high: 99, memory: 200, items: 500 }
|
||||
sharp.cache(50, 200); // { current: 49, high: 99, memory: 50, items: 200}
|
||||
```
|
||||
|
||||
#### sharp.concurrency([threads])
|
||||
|
||||
`threads`, if provided, is the Number of threads _libvips'_ should create for image processing. The default value is the number of CPU cores. A value of `0` will reset to this default.
|
||||
|
||||
This method always returns the current concurrency.
|
||||
|
||||
```javascript
|
||||
var threads = sharp.concurrency(); // 4
|
||||
sharp.concurrency(2); // 2
|
||||
sharp.concurrency(0); // 4
|
||||
```
|
||||
|
||||
#### sharp.counters()
|
||||
|
||||
Provides access to internal task counters.
|
||||
|
||||
* `queue` is the number of tasks this module has queued waiting for _libuv_ to provide a worker thread from its pool.
|
||||
* `process` is the number of resize tasks currently being processed.
|
||||
|
||||
```javascript
|
||||
var counters = sharp.counters(); // { queue: 2, process: 4 }
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
npm test
|
||||
### Ubuntu 12.04
|
||||
|
||||
[](https://travis-ci.org/lovell/sharp)
|
||||
|
||||
### Centos 6.5
|
||||
|
||||
[](https://snap-ci.com/lovell/sharp/branch/master)
|
||||
|
||||
### It worked on my machine
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
Running the comparative performance tests requires _ImageMagick_ and _GraphicsMagick_.
|
||||
|
||||
```
|
||||
brew install imagemagick
|
||||
brew install graphicsmagick
|
||||
```
|
||||
|
||||
```
|
||||
sudo apt-get install -qq imagemagick graphicsmagick
|
||||
```
|
||||
|
||||
```
|
||||
sudo yum install ImageMagick
|
||||
sudo yum install -y http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
|
||||
sudo yum install -y --enablerepo=epel GraphicsMagick
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
## Thanks
|
||||
|
||||
This module would never have been possible without the help and code contributions of the following people:
|
||||
|
||||
* [John Cupitt](https://github.com/jcupitt)
|
||||
* [Pierre Inglebert](https://github.com/pierreinglebert)
|
||||
* [Jonathan Ong](https://github.com/jonathanong)
|
||||
* [Chanon Sajjamanochai](https://github.com/chanon)
|
||||
* [Juliano Julio](https://github.com/julianojulio)
|
||||
|
||||
Thank you!
|
||||
|
||||
## Licence
|
||||
|
||||
Copyright 2013 Lovell Fuller
|
||||
Copyright 2013, 2014 Lovell Fuller and contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
20
binding.gyp
@@ -2,16 +2,22 @@
|
||||
'targets': [{
|
||||
'target_name': 'sharp',
|
||||
'sources': ['src/sharp.cc'],
|
||||
'variables': {
|
||||
'PKG_CONFIG_PATH': '<!(which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true):$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig'
|
||||
},
|
||||
'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="<(PKG_CONFIG_PATH)" pkg-config --libs vips)'
|
||||
],
|
||||
'include_dirs': [
|
||||
'/usr/include/glib-2.0',
|
||||
'/usr/lib/glib-2.0/include',
|
||||
'/usr/lib/x86_64-linux-gnu/glib-2.0/include'
|
||||
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips glib-2.0)',
|
||||
'<!(node -e "require(\'nan\')")'
|
||||
],
|
||||
'cflags': ['-fexceptions'],
|
||||
'cflags_cc': ['-fexceptions']
|
||||
'cflags': ['-fexceptions', '-Wall', '-O3'],
|
||||
'cflags_cc': ['-std=c++0x', '-fexceptions', '-Wall', '-O3'],
|
||||
'xcode_settings': {
|
||||
'OTHER_CFLAGS': ['-std=c++11', '-stdlib=libc++', '-fexceptions', '-Wall', '-O3'],
|
||||
'OTHER_CPLUSPLUSFLAGS': ['-std=c++11', '-stdlib=libc++', '-fexceptions', '-Wall', '-O3'],
|
||||
'MACOSX_DEPLOYMENT_TARGET': '10.7'
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
467
index.js
@@ -1,13 +1,454 @@
|
||||
var sharp = require("./build/Release/sharp");
|
||||
|
||||
module.exports.crop = function(input, output, width, height, callback) {
|
||||
sharp.resize(input, output, width, height, "c", callback);
|
||||
};
|
||||
|
||||
module.exports.embedWhite = function(input, output, width, height, callback) {
|
||||
sharp.resize(input, output, width, height, "w", callback);
|
||||
};
|
||||
|
||||
module.exports.embedBlack = function(input, output, width, height, callback) {
|
||||
sharp.resize(input, output, width, height, "b", callback);
|
||||
};
|
||||
/*jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var util = require('util');
|
||||
var stream = require('stream');
|
||||
var Promise = require('bluebird');
|
||||
var sharp = require('./build/Release/sharp');
|
||||
|
||||
var Sharp = function(input) {
|
||||
if (!(this instanceof Sharp)) {
|
||||
return new Sharp(input);
|
||||
}
|
||||
stream.Duplex.call(this);
|
||||
this.options = {
|
||||
width: -1,
|
||||
height: -1,
|
||||
canvas: 'c',
|
||||
gravity: 0,
|
||||
angle: 0,
|
||||
withoutEnlargement: false,
|
||||
sharpen: false,
|
||||
interpolator: 'bilinear',
|
||||
gamma: 0,
|
||||
progressive: false,
|
||||
sequentialRead: false,
|
||||
quality: 80,
|
||||
compressionLevel: 6,
|
||||
streamIn: false,
|
||||
streamOut: false,
|
||||
withMetadata: false,
|
||||
output: '__input'
|
||||
};
|
||||
if (typeof input === 'string') {
|
||||
// input=file
|
||||
this.options.fileIn = input;
|
||||
} else if (typeof input === 'object' && input instanceof Buffer) {
|
||||
// input=buffer
|
||||
if (input.length > 0) {
|
||||
this.options.bufferIn = input;
|
||||
} else {
|
||||
throw new Error('Buffer is empty');
|
||||
}
|
||||
} else {
|
||||
// input=stream
|
||||
this.options.streamIn = true;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
module.exports = Sharp;
|
||||
util.inherits(Sharp, stream.Duplex);
|
||||
|
||||
/*
|
||||
Handle incoming chunk on Writable Stream
|
||||
*/
|
||||
Sharp.prototype._write = function(chunk, encoding, callback) {
|
||||
if (this.options.streamIn) {
|
||||
if (typeof chunk === 'object' || chunk instanceof Buffer) {
|
||||
if (typeof this.options.bufferIn === 'undefined') {
|
||||
// Create new Buffer
|
||||
this.options.bufferIn = new Buffer(chunk.length);
|
||||
chunk.copy(this.options.bufferIn);
|
||||
} else {
|
||||
// Append to existing Buffer
|
||||
this.options.bufferIn = Buffer.concat(
|
||||
[this.options.bufferIn, chunk],
|
||||
this.options.bufferIn.length + chunk.length
|
||||
);
|
||||
}
|
||||
callback();
|
||||
} else {
|
||||
callback(new Error('Non-Buffer data on Writable Stream'));
|
||||
}
|
||||
} else {
|
||||
callback(new Error('Unexpected data on Writable Stream'));
|
||||
}
|
||||
};
|
||||
|
||||
// Crop this part of the resized image (Center/Centre, North, East, South, West)
|
||||
module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'south': 3, 'west': 4};
|
||||
|
||||
Sharp.prototype.crop = function(gravity) {
|
||||
this.options.canvas = 'c';
|
||||
if (typeof gravity !== 'undefined') {
|
||||
// Is this a supported gravity?
|
||||
if (!Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
|
||||
this.options.gravity = gravity;
|
||||
} else {
|
||||
throw new Error('Unsupported crop gravity ' + gravity);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.embedWhite = function() {
|
||||
this.options.canvas = 'w';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.embedBlack = function() {
|
||||
this.options.canvas = 'b';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.max = function() {
|
||||
this.options.canvas = 'm';
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Rotate output image by 0, 90, 180 or 270 degrees
|
||||
Auto-rotation based on the EXIF Orientation tag is represented by an angle of -1
|
||||
*/
|
||||
Sharp.prototype.rotate = function(angle) {
|
||||
if (typeof angle === 'undefined') {
|
||||
this.options.angle = -1;
|
||||
} else if (!Number.isNaN(angle) && [0, 90, 180, 270].indexOf(angle) !== -1) {
|
||||
this.options.angle = angle;
|
||||
} else {
|
||||
throw new Error('Unsupported angle (0, 90, 180, 270) ' + angle);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Do not enlarge the output if the input width *or* height are already less than the required dimensions
|
||||
This is equivalent to GraphicsMagick's ">" geometry option:
|
||||
"change the dimensions of the image only if its width or height exceeds the geometry specification"
|
||||
*/
|
||||
Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {
|
||||
this.options.withoutEnlargement = (typeof withoutEnlargement === 'boolean') ? withoutEnlargement : true;
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.sharpen = function(sharpen) {
|
||||
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
|
||||
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) {
|
||||
this.options.interpolator = interpolator;
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Deprecated interpolation methods, to be removed in v0.7.0
|
||||
*/
|
||||
Sharp.prototype.bilinearInterpolation = util.deprecate(function() {
|
||||
return this.interpolateWith(module.exports.interpolator.bilinear);
|
||||
}, 'bilinearInterpolation() is deprecated, use interpolateWith(sharp.interpolator.bilinear) instead');
|
||||
Sharp.prototype.bicubicInterpolation = util.deprecate(function() {
|
||||
return this.interpolateWith(module.exports.interpolator.bicubic);
|
||||
}, 'bicubicInterpolation() is deprecated, use interpolateWith(sharp.interpolator.bicubic) instead');
|
||||
Sharp.prototype.nohaloInterpolation = util.deprecate(function() {
|
||||
return this.interpolateWith(module.exports.interpolator.nohalo);
|
||||
}, 'nohaloInterpolation() is deprecated, use interpolateWith(sharp.interpolator.nohalo) instead');
|
||||
|
||||
/*
|
||||
Darken image pre-resize (1/gamma) and brighten post-resize (gamma).
|
||||
Improves brightness of resized image in non-linear colour spaces.
|
||||
*/
|
||||
Sharp.prototype.gamma = function(gamma) {
|
||||
if (typeof gamma === 'undefined') {
|
||||
// Default gamma correction of 2.2 (sRGB)
|
||||
this.options.gamma = 2.2;
|
||||
} else if (!Number.isNaN(gamma) && gamma >= 1 && gamma <= 3) {
|
||||
this.options.gamma = gamma;
|
||||
} else {
|
||||
throw new Error('Invalid gamma correction (1.0 to 3.0) ' + gamma);
|
||||
}
|
||||
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.quality = function(quality) {
|
||||
if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) {
|
||||
this.options.quality = quality;
|
||||
} else {
|
||||
throw new Error('Invalid quality (1 to 100) ' + quality);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.compressionLevel = function(compressionLevel) {
|
||||
if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) {
|
||||
this.options.compressionLevel = compressionLevel;
|
||||
} else {
|
||||
throw new Error('Invalid compressionLevel (-1 to 9) ' + compressionLevel);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.withMetadata = function(withMetadata) {
|
||||
this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : 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 new Error('Invalid width ' + width);
|
||||
}
|
||||
}
|
||||
if (!height) {
|
||||
this.options.height = -1;
|
||||
} else {
|
||||
if (!Number.isNaN(height)) {
|
||||
this.options.height = height;
|
||||
} else {
|
||||
throw new Error('Invalid height ' + height);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Write output image data to a file
|
||||
*/
|
||||
Sharp.prototype.toFile = function(output, callback) {
|
||||
if (!output || output.length === 0) {
|
||||
var errOutputInvalid = new Error('Invalid output');
|
||||
if (typeof callback === 'function') {
|
||||
callback(errOutputInvalid);
|
||||
} else {
|
||||
return Promise.reject(errOutputInvalid);
|
||||
}
|
||||
} else {
|
||||
if (this.options.fileIn === output) {
|
||||
var errOutputIsInput = new Error('Cannot use same file for input and output');
|
||||
if (typeof callback === 'function') {
|
||||
callback(errOutputIsInput);
|
||||
} else {
|
||||
return Promise.reject(errOutputIsInput);
|
||||
}
|
||||
} else {
|
||||
this.options.output = output;
|
||||
return this._sharp(callback);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.toBuffer = function(callback) {
|
||||
return this._sharp(callback);
|
||||
};
|
||||
|
||||
Sharp.prototype.jpeg = function() {
|
||||
this.options.output = '__jpeg';
|
||||
if (arguments.length > 0) {
|
||||
console.error('Use of the jpeg() method with a callback is deprecated in 0.6.x and will be removed in 0.7.x');
|
||||
console.error('Please add toFile(), toBuffer() or Stream methods e.g. pipe() for JPEG output');
|
||||
this._sharp(arguments);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.png = function() {
|
||||
this.options.output = '__png';
|
||||
if (arguments.length > 0) {
|
||||
console.error('Use of the png() method with a callback is deprecated in 0.6.x and will be removed in 0.7.x');
|
||||
console.error('Please add toFile(), toBuffer() or Stream methods e.g. pipe() for PNG output');
|
||||
this._sharp(arguments);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.webp = function() {
|
||||
this.options.output = '__webp';
|
||||
if (arguments.length > 0) {
|
||||
console.error('Use of the webp() method with a callback is deprecated in 0.6.x and will be removed in 0.7.x');
|
||||
console.error('Please add toFile(), toBuffer() or Stream methods e.g. pipe() for WebP output');
|
||||
this._sharp(arguments);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Used by a Writable Stream to notify that it is ready for data
|
||||
*/
|
||||
Sharp.prototype._read = function() {
|
||||
if (!this.options.streamOut) {
|
||||
this.options.streamOut = true;
|
||||
this._sharp();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Invoke the C++ image processing pipeline
|
||||
Supports callback, stream and promise variants
|
||||
*/
|
||||
Sharp.prototype._sharp = function(callback) {
|
||||
var that = this;
|
||||
if (typeof callback === 'function') {
|
||||
// output=file/buffer
|
||||
if (this.options.streamIn) {
|
||||
// output=file/buffer, input=stream
|
||||
this.on('finish', function() {
|
||||
sharp.resize(that.options, callback);
|
||||
});
|
||||
} else {
|
||||
// output=file/buffer, input=file/buffer
|
||||
sharp.resize(this.options, callback);
|
||||
}
|
||||
return this;
|
||||
} else if (this.options.streamOut) {
|
||||
// output=stream
|
||||
if (this.options.streamIn) {
|
||||
// output=stream, input=stream
|
||||
this.on('finish', function() {
|
||||
sharp.resize(that.options, function(err, data) {
|
||||
if (err) {
|
||||
that.emit('error', new Error(err));
|
||||
} else {
|
||||
that.push(data);
|
||||
}
|
||||
that.push(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// output=stream, input=file/buffer
|
||||
sharp.resize(this.options, function(err, data) {
|
||||
if (err) {
|
||||
that.emit('error', new Error(err));
|
||||
} else {
|
||||
that.push(data);
|
||||
}
|
||||
that.push(null);
|
||||
});
|
||||
}
|
||||
return this;
|
||||
} else {
|
||||
// output=promise
|
||||
if (this.options.streamIn) {
|
||||
// output=promise, input=stream
|
||||
return new Promise(function(resolve, reject) {
|
||||
that.on('finish', function() {
|
||||
sharp.resize(that.options, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// output=promise, input=file/buffer
|
||||
return new Promise(function(resolve, reject) {
|
||||
sharp.resize(that.options, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Reads the image header and returns metadata
|
||||
Supports callback, stream and promise variants
|
||||
*/
|
||||
Sharp.prototype.metadata = function(callback) {
|
||||
var that = this;
|
||||
if (typeof callback === 'function') {
|
||||
if (this.options.streamIn) {
|
||||
this.on('finish', function() {
|
||||
sharp.metadata(that.options, callback);
|
||||
});
|
||||
} else {
|
||||
sharp.metadata(this.options, callback);
|
||||
}
|
||||
return this;
|
||||
} else {
|
||||
if (this.options.streamIn) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
that.on('finish', function() {
|
||||
sharp.metadata(that.options, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return new Promise(function(resolve, reject) {
|
||||
sharp.metadata(that.options, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Get and set cache memory and item limits
|
||||
*/
|
||||
module.exports.cache = function(memory, items) {
|
||||
if (Number.isNaN(memory)) {
|
||||
memory = null;
|
||||
}
|
||||
if (Number.isNaN(items)) {
|
||||
items = null;
|
||||
}
|
||||
return sharp.cache(memory, items);
|
||||
};
|
||||
|
||||
/*
|
||||
Get and set size of thread pool
|
||||
*/
|
||||
module.exports.concurrency = function(concurrency) {
|
||||
if (Number.isNaN(concurrency)) {
|
||||
concurrency = null;
|
||||
}
|
||||
return sharp.concurrency(concurrency);
|
||||
};
|
||||
|
||||
/*
|
||||
Get internal counters
|
||||
*/
|
||||
module.exports.counters = function() {
|
||||
return sharp.counters();
|
||||
};
|
||||
|
||||
40
package.json
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"name": "sharp",
|
||||
"version": "0.0.6",
|
||||
"author": "Lovell Fuller",
|
||||
"description": "High performance module to resize JPEG and PNG images using the libvips image processing library",
|
||||
"version": "0.6.2",
|
||||
"author": "Lovell Fuller <npm@lovell.info>",
|
||||
"contributors": [
|
||||
"Pierre Inglebert <pierre.inglebert@gmail.com>",
|
||||
"Jonathan Ong <jonathanrichardong@gmail.com>",
|
||||
"Chanon Sajjamanochai <chanon.s@gmail.com>",
|
||||
"Juliano Julio <julianojulio@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 +20,33 @@
|
||||
"keywords": [
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp",
|
||||
"tiff",
|
||||
"gif",
|
||||
"resize",
|
||||
"thumbnail",
|
||||
"sharpen",
|
||||
"crop",
|
||||
"embed",
|
||||
"libvips",
|
||||
"fast"
|
||||
"vips",
|
||||
"fast",
|
||||
"buffer",
|
||||
"stream"
|
||||
],
|
||||
"dependencies": {
|
||||
"nan": "^1.3.0",
|
||||
"bluebird": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"imagemagick": "*",
|
||||
"gm": "*",
|
||||
"epeg": "*",
|
||||
"async": "*",
|
||||
"benchmark": "*"
|
||||
"imagemagick": "^0.1.3",
|
||||
"imagemagick-native": "^1.2.2",
|
||||
"gm": "^1.16.0",
|
||||
"async": "^0.9.0",
|
||||
"benchmark": "^1.0.0"
|
||||
},
|
||||
"license": "Apache 2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
"node": ">=0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
998
src/sharp.cc
|
Before Width: | Height: | Size: 813 KiB After Width: | Height: | Size: 813 KiB |
BIN
tests/fixtures/4.webp
vendored
Normal file
|
After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
BIN
tests/fixtures/Crash_test.gif
vendored
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
tests/fixtures/G31D.TIF
vendored
Normal file
BIN
tests/fixtures/Landscape_8.jpg
vendored
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
tests/fixtures/gamma_dalai_lama_gray.jpg
vendored
Normal file
|
After Width: | Height: | Size: 83 KiB |
7
tests/leak.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
if ! type valgrind >/dev/null; then
|
||||
echo "Please install valgrind before running memory leak tests"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -O https://raw.githubusercontent.com/jcupitt/libvips/master/libvips.supp
|
||||
G_SLICE=always-malloc G_DEBUG=gc-friendly valgrind --suppressions=libvips.supp --suppressions=sharp.supp --leak-check=full --show-leak-kinds=definite,indirect,possible node unit.js
|
||||
39
tests/parallel.js
Executable file
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
|
||||
sharp.concurrency(1);
|
||||
|
||||
var timer = setInterval(function() {
|
||||
console.dir(sharp.counters());
|
||||
}, 100);
|
||||
|
||||
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() {
|
||||
clearInterval(timer);
|
||||
console.dir(sharp.counters());
|
||||
});
|
||||
525
tests/perf.js
@@ -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,222 @@ 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).toFile(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).toFile(outputJpg, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-stream-stream", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
var readable = fs.createReadStream(inputJpg);
|
||||
var writable = fs.createWriteStream(outputJpg);
|
||||
writable.on('finish', function() {
|
||||
deferred.resolve();
|
||||
});
|
||||
var pipeline = sharp().resize(width, height);
|
||||
readable.pipe(pipeline).pipe(writable);
|
||||
}
|
||||
}).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-promise", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).toBuffer().then(function(buffer) {
|
||||
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-nearest-neighbour", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.nearest).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-bicubic", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.bicubic).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-nohalo", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.nohalo).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-locallyBoundedBicubic", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.locallyBoundedBicubic).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-vertexSplitQuadraticBasisSpline", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-gamma", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).gamma().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-rotate", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).rotate(90).resize(width, height).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 +300,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 +317,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 +339,84 @@ 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).toFile(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).toFile(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();
|
||||
}
|
||||
});
|
||||
@@ -113,10 +426,148 @@ 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).toFile(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).toFile(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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).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).toFile(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().toFile(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).toFile(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().toFile(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).toFile(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) {
|
||||
assert(!err, err);
|
||||
Object.keys(results).forEach(function(format) {
|
||||
assert(results[format] == "sharp", "sharp was slower than " + results[format] + " for " + 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
@@ -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();
|
||||
111
tests/sharp.supp
Normal file
@@ -0,0 +1,111 @@
|
||||
# libjpeg warnings
|
||||
{
|
||||
cond_jpeg_read_scanlines
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:jpeg_read_scanlines
|
||||
}
|
||||
{
|
||||
value_jpeg_read_scanlines
|
||||
Memcheck:Value8
|
||||
...
|
||||
fun:jpeg_read_scanlines
|
||||
}
|
||||
{
|
||||
cond_jpeg_write_scanlines
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:jpeg_write_scanlines
|
||||
}
|
||||
{
|
||||
cond_jpeg_finish_compress
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:jpeg_finish_compress
|
||||
}
|
||||
{
|
||||
value_jpeg_finish_compress
|
||||
Memcheck:Value8
|
||||
...
|
||||
fun:jpeg_finish_compress
|
||||
}
|
||||
|
||||
# libvips interpolator warnings
|
||||
{
|
||||
cond_libvips_interpolate_lbb
|
||||
Memcheck:Cond
|
||||
...
|
||||
fun:_ZL32vips_interpolate_lbb_interpolateP16_VipsInterpolatePvP11_VipsRegiondd
|
||||
fun:vips_affine_gen
|
||||
}
|
||||
|
||||
# libuv warnings
|
||||
{
|
||||
free_libuv
|
||||
Memcheck:Free
|
||||
...
|
||||
fun:uv__work_done
|
||||
}
|
||||
|
||||
# nodejs warnings
|
||||
{
|
||||
param_nodejs_write_buffer
|
||||
Memcheck:Param
|
||||
write(buf)
|
||||
...
|
||||
obj:/usr/bin/nodejs
|
||||
}
|
||||
{
|
||||
leak_nodejs_ImmutableAsciiSource_CreateFromLiteral
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
...
|
||||
fun:_ZN4node20ImmutableAsciiSource17CreateFromLiteralEPKcm
|
||||
}
|
||||
{
|
||||
leak_nodejs_Buffer_New
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
...
|
||||
fun:_ZN4node6Buffer3NewERKN2v89ArgumentsE
|
||||
}
|
||||
{
|
||||
leak_nodejs_Buffer_Replace
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: indirect,possible
|
||||
...
|
||||
fun:_ZN4node6Buffer7ReplaceEPcmPFvS1_PvES2_
|
||||
}
|
||||
{
|
||||
leak_nodejs_SignalWrap_New
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:_ZN4node10SignalWrap3NewERKN2v89ArgumentsE
|
||||
}
|
||||
{
|
||||
leak_nodejs_TTYWrap_New
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:_ZN4node7TTYWrap3NewERKN2v89ArgumentsE
|
||||
}
|
||||
{
|
||||
leak_nodejs_ares_init_options
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
fun:strdup
|
||||
...
|
||||
fun:ares_init_options
|
||||
}
|
||||
|
||||
# vips__init warnings
|
||||
{
|
||||
leak_libvips_init
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: reachable
|
||||
fun:malloc
|
||||
...
|
||||
fun:vips__init
|
||||
}
|
||||
708
tests/unit.js
Executable file
@@ -0,0 +1,708 @@
|
||||
/*jslint node: true */
|
||||
/*jslint es5: true */
|
||||
'use strict';
|
||||
|
||||
var sharp = require("../index");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
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");
|
||||
|
||||
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 inputJpgWithExif = path.join(fixturesPath, "Landscape_8.jpg"); // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
|
||||
|
||||
var inputJpgWithGammaHoliness = path.join(fixturesPath, "gamma_dalai_lama_gray.jpg"); // http://www.4p8.com/eric.brasseur/gamma.html
|
||||
|
||||
var inputPng = path.join(fixturesPath, "50020484-00001.png"); // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
||||
var inputWebP = path.join(fixturesPath, "4.webp"); // http://www.gstatic.com/webp/gallery/4.webp
|
||||
var inputGif = path.join(fixturesPath, "Crash_test.gif"); // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
|
||||
|
||||
// Ensure cache limits can be set
|
||||
sharp.cache(0); // Disable
|
||||
sharp.cache(50, 500); // 50MB, 500 items
|
||||
|
||||
// Ensure concurrency can be set
|
||||
var defaultConcurrency = sharp.concurrency();
|
||||
sharp.concurrency(16);
|
||||
assert.strictEqual(16, sharp.concurrency());
|
||||
sharp.concurrency(0);
|
||||
assert.strictEqual(defaultConcurrency, sharp.concurrency());
|
||||
|
||||
async.series([
|
||||
// Resize with exact crop
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).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();
|
||||
});
|
||||
},
|
||||
// Resize to fixed width
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320).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(261, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Resize to fixed height
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(null, 320).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(391, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Identity transform
|
||||
function(done) {
|
||||
sharp(inputJpg).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(2725, info.width);
|
||||
assert.strictEqual(2225, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Upscale
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(3000).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(3000, info.width);
|
||||
assert.strictEqual(2449, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Quality
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).quality(70).toBuffer(function(err, buffer70) {
|
||||
if (err) throw err;
|
||||
sharp(inputJpg).resize(320, 240).toBuffer(function(err, buffer80) {
|
||||
if (err) throw err;
|
||||
sharp(inputJpg).resize(320, 240).quality(90).toBuffer(function(err, buffer90) {
|
||||
assert(buffer70.length < buffer80.length);
|
||||
assert(buffer80.length < buffer90.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
// TIFF with dimensions known to cause rounding errors
|
||||
function(done) {
|
||||
sharp(inputTiff).resize(240, 320).embedBlack().jpeg().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(240, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
sharp(inputTiff).resize(240, 320).jpeg().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(240, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Resize to max width or height considering ratio (landscape)
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 320).max().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(261, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Resize to max width or height considering ratio (portrait)
|
||||
function(done) {
|
||||
sharp(inputTiff).resize(320, 320).max().jpeg().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(243, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Attempt to resize to max but only provide one dimension, so should default to crop
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320).max().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(261, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Attempt to output to input, should fail
|
||||
function(done) {
|
||||
sharp(inputJpg).toFile(inputJpg, function(err) {
|
||||
assert(!!err);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Rotate by 90 degrees, respecting output input size
|
||||
function(done) {
|
||||
sharp(inputJpg).rotate(90).resize(320, 240).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();
|
||||
});
|
||||
},
|
||||
// Input image has Orientation EXIF tag but do not rotate output
|
||||
function(done) {
|
||||
sharp(inputJpgWithExif).resize(320).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(426, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate
|
||||
function(done) {
|
||||
sharp(inputJpgWithExif).rotate().resize(320).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();
|
||||
});
|
||||
},
|
||||
// Attempt to auto-rotate using image that has no EXIF
|
||||
function(done) {
|
||||
sharp(inputJpg).rotate().resize(320).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(261, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Rotate to an invalid angle, should fail
|
||||
function(done) {
|
||||
var fail = false;
|
||||
try {
|
||||
sharp(inputJpg).rotate(1);
|
||||
fail = true;
|
||||
} catch (e) {}
|
||||
assert(!fail);
|
||||
done();
|
||||
},
|
||||
// Do not enlarge the output if the input width is already less than the output width
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(2800).withoutEnlargement().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(2725, info.width);
|
||||
assert.strictEqual(2225, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Do not enlarge the output if the input height is already less than the output height
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(null, 2300).withoutEnlargement().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(2725, info.width);
|
||||
assert.strictEqual(2225, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Promises/A+
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).toBuffer().then(function(data) {
|
||||
sharp(data).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();
|
||||
});
|
||||
}).catch(function(err) {
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
// Empty Buffer, should fail
|
||||
function(done) {
|
||||
var fail = false;
|
||||
try {
|
||||
sharp(new Buffer(0));
|
||||
fail = true;
|
||||
} catch (e) {}
|
||||
assert(!fail);
|
||||
done();
|
||||
},
|
||||
// Check colour space conversion occurs from TIFF to WebP (this used to segfault)
|
||||
function(done) {
|
||||
sharp(inputTiff).webp().toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('webp', info.format);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Interpolation: nearest neighbour
|
||||
function(done) {
|
||||
sharp(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();
|
||||
});
|
||||
},
|
||||
// Interpolation: bilinear
|
||||
function(done) {
|
||||
sharp(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();
|
||||
});
|
||||
},
|
||||
// Interpolation: bicubic
|
||||
function(done) {
|
||||
sharp(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();
|
||||
});
|
||||
},
|
||||
// Interpolation: nohalo
|
||||
function(done) {
|
||||
sharp(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();
|
||||
});
|
||||
},
|
||||
// Interpolation: locally bounded bicubic (LBB)
|
||||
function(done) {
|
||||
sharp(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();
|
||||
});
|
||||
},
|
||||
// Interpolation: vertex split quadratic basis spline (VSQBS)
|
||||
function(done) {
|
||||
sharp(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();
|
||||
});
|
||||
},
|
||||
// File-Stream
|
||||
function(done) {
|
||||
var writable = fs.createWriteStream(outputJpg);
|
||||
writable.on('finish', function() {
|
||||
sharp(outputJpg).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);
|
||||
fs.unlinkSync(outputJpg);
|
||||
done();
|
||||
});
|
||||
});
|
||||
sharp(inputJpg).resize(320, 240).pipe(writable);
|
||||
},
|
||||
// Buffer-Stream
|
||||
function(done) {
|
||||
var inputJpgBuffer = fs.readFileSync(inputJpg);
|
||||
var writable = fs.createWriteStream(outputJpg);
|
||||
writable.on('finish', function() {
|
||||
sharp(outputJpg).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);
|
||||
fs.unlinkSync(outputJpg);
|
||||
done();
|
||||
});
|
||||
});
|
||||
sharp(inputJpgBuffer).resize(320, 240).pipe(writable);
|
||||
},
|
||||
// Stream-File
|
||||
function(done) {
|
||||
var readable = fs.createReadStream(inputJpg);
|
||||
var pipeline = sharp().resize(320, 240).toFile(outputJpg, function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
fs.unlinkSync(outputJpg);
|
||||
done();
|
||||
});
|
||||
readable.pipe(pipeline);
|
||||
},
|
||||
// Stream-Buffer
|
||||
function(done) {
|
||||
var readable = fs.createReadStream(inputJpg);
|
||||
var pipeline = sharp().resize(320, 240).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(240, info.height);
|
||||
done();
|
||||
});
|
||||
readable.pipe(pipeline);
|
||||
},
|
||||
// Stream-Stream
|
||||
function(done) {
|
||||
var readable = fs.createReadStream(inputJpg);
|
||||
var writable = fs.createWriteStream(outputJpg);
|
||||
writable.on('finish', function() {
|
||||
sharp(outputJpg).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);
|
||||
fs.unlinkSync(outputJpg);
|
||||
done();
|
||||
});
|
||||
});
|
||||
var pipeline = sharp().resize(320, 240);
|
||||
readable.pipe(pipeline).pipe(writable);
|
||||
},
|
||||
// Stream-Stream error handling
|
||||
function(done) {
|
||||
var pipeline = sharp().resize(320, 240);
|
||||
var anErrorWasEmitted = false;
|
||||
pipeline.on('error', function(err) {
|
||||
anErrorWasEmitted = !!err;
|
||||
}).on('end', function() {
|
||||
assert(anErrorWasEmitted);
|
||||
done();
|
||||
});
|
||||
var readableButNotAnImage = fs.createReadStream(__filename);
|
||||
var writable = fs.createWriteStream(outputJpg);
|
||||
readableButNotAnImage.pipe(pipeline).pipe(writable);
|
||||
},
|
||||
// File-Stream error handling
|
||||
function(done) {
|
||||
var readableButNotAnImage = sharp(__filename).resize(320, 240);
|
||||
var anErrorWasEmitted = false;
|
||||
readableButNotAnImage.on('error', function(err) {
|
||||
anErrorWasEmitted = !!err;
|
||||
}).on('end', function() {
|
||||
assert(anErrorWasEmitted);
|
||||
done();
|
||||
});
|
||||
var writable = fs.createWriteStream(outputJpg);
|
||||
readableButNotAnImage.pipe(writable);
|
||||
},
|
||||
// Crop, gravity=north
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 80).crop(sharp.gravity.north).toFile(path.join(fixturesPath, 'output.gravity-north.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Crop, gravity=east
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.east).toFile(path.join(fixturesPath, 'output.gravity-east.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Crop, gravity=south
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 80).crop(sharp.gravity.south).toFile(path.join(fixturesPath, 'output.gravity-south.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Crop, gravity=west
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.west).toFile(path.join(fixturesPath, 'output.gravity-west.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Crop, gravity=center
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 80).crop(sharp.gravity.center).toFile(path.join(fixturesPath, 'output.gravity-center.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Crop, gravity=centre
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.centre).toFile(path.join(fixturesPath, 'output.gravity-centre.jpg'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(80, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Keeps Metadata after a resize
|
||||
function(done) {
|
||||
sharp(inputJpgWithExif).resize(320, 240).withMetadata().toBuffer(function(err, buffer) {
|
||||
if (err) throw err;
|
||||
sharp(buffer).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(8, metadata.orientation);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Keeps Metadata after a resize
|
||||
function(done) {
|
||||
sharp(inputJpgWithExif).resize(320, 240).withMetadata(false).toBuffer(function(err, buffer) {
|
||||
if (err) throw err;
|
||||
sharp(buffer).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('undefined', typeof metadata.orientation);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Output filename without extension should mirror input format
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
sharp(inputPng).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('png', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
sharp(inputWebP).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('webp', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
sharp(inputTiff).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('tiff', info.format);
|
||||
assert.strictEqual(320, info.width);
|
||||
assert.strictEqual(80, info.height);
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
sharp(inputGif).resize(320, 80).toFile(path.join(fixturesPath, 'output.zoinks'), function(err, info) {
|
||||
assert(!!err);
|
||||
});
|
||||
},
|
||||
// Metadata - JPEG
|
||||
function(done) {
|
||||
sharp(inputJpg).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual('undefined', typeof metadata.orientation);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Metadata - JPEG with EXIF
|
||||
function(done) {
|
||||
sharp(inputJpgWithExif).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(450, metadata.width);
|
||||
assert.strictEqual(600, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
assert.strictEqual(8, metadata.orientation);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Metadata - TIFF
|
||||
function(done) {
|
||||
sharp(inputTiff).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('tiff', metadata.format);
|
||||
assert.strictEqual(2464, metadata.width);
|
||||
assert.strictEqual(3248, metadata.height);
|
||||
assert.strictEqual('b-w', metadata.space);
|
||||
assert.strictEqual(1, metadata.channels);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Metadata - PNG
|
||||
function(done) {
|
||||
sharp(inputPng).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('png', metadata.format);
|
||||
assert.strictEqual(2809, metadata.width);
|
||||
assert.strictEqual(2074, metadata.height);
|
||||
assert.strictEqual('b-w', metadata.space);
|
||||
assert.strictEqual(1, metadata.channels);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Metadata - WebP
|
||||
function(done) {
|
||||
sharp(inputWebP).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('webp', metadata.format);
|
||||
assert.strictEqual(1024, metadata.width);
|
||||
assert.strictEqual(772, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Metadata - GIF (via libmagick)
|
||||
function(done) {
|
||||
sharp(inputGif).metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('magick', metadata.format);
|
||||
assert.strictEqual(800, metadata.width);
|
||||
assert.strictEqual(533, metadata.height);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Metadata - Promise
|
||||
function(done) {
|
||||
sharp(inputJpg).metadata().then(function(metadata) {
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Metadata - Stream
|
||||
function(done) {
|
||||
var readable = fs.createReadStream(inputJpg);
|
||||
var pipeline = sharp().metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
done();
|
||||
});
|
||||
readable.pipe(pipeline);
|
||||
},
|
||||
// Get metadata then resize to half width
|
||||
function(done) {
|
||||
var image = sharp(inputJpg);
|
||||
image.metadata(function(err, metadata) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual('jpeg', metadata.format);
|
||||
assert.strictEqual(2725, metadata.width);
|
||||
assert.strictEqual(2225, metadata.height);
|
||||
assert.strictEqual('srgb', metadata.space);
|
||||
assert.strictEqual(3, metadata.channels);
|
||||
image.resize(metadata.width / 2).toBuffer(function(err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual(1362, info.width);
|
||||
assert.strictEqual(1112, info.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Gamma correction
|
||||
function(done) {
|
||||
sharp(inputJpgWithGammaHoliness).resize(129, 111).toFile(path.join(fixturesPath, 'output.gamma-0.0.jpg'), function(err) {
|
||||
if (err) throw err;
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
sharp(inputJpgWithGammaHoliness).resize(129, 111).gamma().toFile(path.join(fixturesPath, 'output.gamma-2.2.jpg'), function(err) {
|
||||
if (err) throw err;
|
||||
done();
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
sharp(inputJpgWithGammaHoliness).resize(129, 111).gamma(3).toFile(path.join(fixturesPath, 'output.gamma-3.0.jpg'), function(err) {
|
||||
if (err) throw err;
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Verify internal counters
|
||||
function(done) {
|
||||
var counters = sharp.counters();
|
||||
assert.strictEqual(0, counters.queue);
|
||||
assert.strictEqual(0, counters.process);
|
||||
done();
|
||||
}
|
||||
]);
|
||||