Compare commits

..

39 Commits

Author SHA1 Message Date
Lovell Fuller
f4cb577cb4 Code lint of new features 2014-10-17 12:26:53 +01:00
Lovell Fuller
91be57cbce Code lint of secondary tests 2014-10-17 12:26:38 +01:00
Lovell Fuller
78596545b0 Dependency version bumps ahead of 0.7.0 2014-10-17 12:15:31 +01:00
Lovell Fuller
9f6cc33858 Memory leak mop-up of new feature spillage 2014-10-13 22:53:43 +01:00
Lovell Fuller
d82de45b7e Thank you Brandon 2014-10-13 17:51:48 +01:00
Lovell Fuller
b7bbf58624 Document new extract method 2014-10-13 15:41:10 +01:00
Lovell Fuller
945d941c7b Convert image to sRGB sooner for embed 2014-10-13 15:39:52 +01:00
Lovell Fuller
2605bf966f Merge pull request #102 from brandonaaron/extract
Add support for stand-alone, pre-resize, and post-resize extraction
2014-10-07 15:19:41 +01:00
Brandon Aaron
83b72a1ede Add support for stand-alone, pre-resize, and post-resize extraction with a topOffset, leftOffset, width, and height. 2014-10-07 09:56:55 -04:00
Lovell Fuller
6190ca4307 Ensure conversion to sRGB occurs before background #99 2014-10-07 10:17:20 +01:00
Lovell Fuller
c2fcf7fc4a Add libvips install info for Debian Jessie #100 2014-10-06 11:27:29 +01:00
Lovell Fuller
37cb4339e2 Import embedded colour profile, if present #99 2014-10-06 10:45:58 +01:00
Lovell Fuller
46f229e308 Use own Angle enum to wrap jcupitt/libvips@154796a 2014-10-06 10:22:45 +01:00
Lovell Fuller
7f8f38f666 PNG compression level range is 0-9 #95 2014-10-03 14:50:15 +01:00
Lovell Fuller
fb0769a327 Improve image reference handling 2014-10-03 11:45:06 +01:00
Lovell Fuller
b84cc3d49e Unit test hardening 2014-10-03 11:42:10 +01:00
Lovell Fuller
0cba506bc4 Allow embed with rgba background #86 #89
Small memory leak mop-up related to #94
2014-10-02 20:02:14 +01:00
Lovell Fuller
5cdfbba55c Merge pull request #94 from gasi/background-flatten
Add `background` and `flatten` APIs
2014-10-01 20:51:52 +01:00
Daniel Gasienica
6145231936 Add background and flatten APIs 2014-09-30 17:58:57 -07:00
Lovell Fuller
513b07ddcf Add hasAlpha attribute to metadata API
Provides internal sharp_image_has_alpha method

Hopefully this helps with #91
2014-09-30 13:35:42 +01:00
Lovell Fuller
150971fa92 Add details of gulp.js plugin 2014-09-30 13:35:10 +01:00
Lovell Fuller
ac85d88c9c Update code style, jshint checks and EOLs 2014-09-19 11:55:52 +01:00
Lovell Fuller
1c79d6fb5d Add support for greyscale conversion #43 2014-09-16 11:22:26 +01:00
Lovell Fuller
d41321254a Update benchmarks using c3.xlarge AWS instance 2014-09-13 20:37:57 +01:00
Lovell Fuller
515b4656e6 Improve Stream error handling #88 2014-09-12 19:30:57 +01:00
Lovell Fuller
34c96ff925 Correct ...magick... test dependencies 2014-09-11 13:45:34 +01:00
Lovell Fuller
b8a04cc4ef Add link to Centos 6.5 CI via snap-ci 2014-09-08 23:58:13 +01:00
Lovell Fuller
ddc3f6e9c6 Add RHEL libvips installation - close #78 2014-09-08 23:53:01 +01:00
Lovell Fuller
3699e61c20 Improve support for extension-less output filenames 2014-09-07 08:59:11 +01:00
Lovell Fuller
2820218609 Add format property to output info
Output format may be derived from input format
2014-09-04 11:38:05 +01:00
Lovell Fuller
eb3e739f7b Expose libvips thread pool size 2014-09-03 19:52:03 +01:00
Lovell Fuller
87f6e83988 Add memory leak check #21
Runs unit tests via valgrind
2014-09-02 13:59:35 +01:00
Lovell Fuller
5728efd32b Add support for gamma correction #43 2014-08-30 14:12:42 +01:00
Lovell Fuller
bac367b005 Notify V8 GC of libvips cache size #21 2014-08-26 23:28:58 +01:00
Lovell Fuller
0d89131f66 Prevent small memory leak with non-empty argv0 #21 2014-08-26 15:25:36 +01:00
Lovell Fuller
8380be4be3 Add nearest neighbour interpolation
Suitable for image enlargement
2014-08-26 09:38:27 +01:00
Lovell Fuller
d0f51363bf Re-order output option methods
Add explicit list of and links to contributors
2014-08-26 09:09:01 +01:00
Lovell Fuller
15160d3b61 Merge pull request #83 from julianojulio/master
Add support for maintaining metadata in output image, thanks @julianojulio #83
2014-08-26 08:31:58 +01:00
Juliano Julio Costa
c5efb77bad New withMetadata([boolean]) to keep metadata in the generated images. 2014-08-25 15:59:21 -04:00
14 changed files with 2019 additions and 949 deletions

4
.gitignore vendored
View File

@@ -13,3 +13,7 @@ results
build
node_modules
tests/fixtures/output.*
tests/libvips.supp
# Mac OS X
.DS_Store

311
README.md
View File

@@ -5,6 +5,7 @@
* [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)
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.
@@ -44,40 +45,55 @@ The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp
brew link gettext --force
### Install libvips on Ubuntu Linux
### Install libvips on Linux
#### Ubuntu 14.x
#### Ubuntu 14.04 LTS
sudo apt-get install libvips-dev
#### Ubuntu 13.x
#### Ubuntu 12.04 LTS
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
sudo add-apt-repository -y ppa:lyrasis/precise-backports
sudo apt-get update
sudo apt-get install -y automake build-essential git gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libexif-dev libxml2-dev swig libmagickwand-dev
git clone https://github.com/jcupitt/libvips.git
cd libvips
git checkout 7.38
git checkout 7.40
./bootstrap.sh
./configure --enable-debug=no --enable-cxx=yes --without-python --without-orc --without-fftw
make
sudo make install
sudo ldconfig
#### Ubuntu 12.x
#### Debian Jessie
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:
apt-get install libvips-dev
sudo add-apt-repository ppa:lyrasis/precise-backports
sudo apt-get update
sudo apt-get install libtiff4-dev
#### Centos 6
Then follow Ubuntu 13.x instructions.
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.
### Using with gulp.js
[Mohammad Prabowo](https://github.com/rizalp) maintains a [gulp.js plugin](https://github.com/rizalp/gulp-sharp).
## Usage examples
```javascript
@@ -129,13 +145,17 @@ readableStream.pipe(pipeline);
sharp('input.png')
.rotate(180)
.resize(300)
.flatten()
.background('#ff6600')
.sharpen()
.withMetadata()
.quality(90)
.webp()
.toBuffer()
.then(function(outputBuffer) {
// outputBuffer contains 300px wide, upside down, sharpened,
// 90% quality WebP image data
// outputBuffer contains upside down, 300px wide, alpha channel flattened
// onto orange background, sharpened, with metadata, 90% quality WebP image
// data
});
```
@@ -148,11 +168,28 @@ http.createServer(function(request, response) {
// resized to 200 pixels wide, in WebP format
```
```javascript
sharp(input)
.extract(top, left, width, height)
.toFile(output);
// Extract a region of the input image, saving in the same format.
```
```javascript
sharp(input)
.extract(topOffsetPre, leftOffsetPre, widthPre, heightPre)
.resize(width, height)
.extract(topOffsetPost, leftOffsetPost, widthPost, heightPost)
.toFile(output);
// Extract a region, resize, then extract from the resized image
```
```javascript
sharp(inputBuffer)
.resize(200, 300)
.interpolateWith(sharp.interpolator.nohalo)
.embedWhite()
.background('white')
.embed()
.toFile('output.tiff')
.then(function() {
// output.tiff is a 200 pixels wide and 300 pixels high image
@@ -162,20 +199,29 @@ sharp(inputBuffer)
```
```javascript
sharp('input.gif').resize(200, 300).embedBlack().webp().toBuffer(function(err, outputBuffer) {
if (err) {
throw err;
}
// 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
});
sharp('input.gif')
.resize(200, 300)
.background({r: 0, g: 0, b: 0, a: 0})
.embed()
.webp()
.toBuffer(function(err, outputBuffer) {
if (err) {
throw err;
}
// outputBuffer contains WebP image data of a 200 pixels wide and 300 pixels high
// containing a scaled version, embedded on a transparent 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
});
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
@@ -206,6 +252,7 @@ Fast access to image metadata without decoding any compressed image data.
* `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
* `hasAlpha`: Boolean indicating the presence of an alpha transparency channel
* `orientation`: Number value of the EXIF Orientation header, if present
A Promises/A+ promise is returned when `callback` is not provided.
@@ -224,6 +271,16 @@ Scale output to `width` x `height`. By default, the resized image is cropped to
`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.
#### extract(top, left, width, height)
Extract a region of the image. Can be used with or without a `resize` operation.
`top` and `left` are the offset, in pixels, from the top-left corner.
`width` and `height` are the dimensions of the extracted image.
Use `extract` before `resize` for pre-resize extraction. Use `extract` after `resize` for post-resize extraction. Use `extract` before and after for both.
#### crop([gravity])
Crop the resized image to the exact size specified, the default behaviour.
@@ -234,17 +291,29 @@ Possible values are `north`, `east`, `south`, `west`, `center` and `centre`. The
#### max()
Preserving aspect ratio, resize the image to the maximum width or height specified.
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()
#### background(rgba)
Embed the resized image on a white background of the exact size specified.
Set the background for the `embed` and `flatten` operations.
#### embedBlack()
`rgba` is parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha.
Embed the resized image on a black background of the exact size specified.
The alpha value is a float between `0` (transparent) and `1` (opaque).
The default background is `{r: 0, g: 0, b: 0, a: 1}`, black without transparency.
#### embed()
Preserving aspect ratio, resize the image to the maximum `width` or `height` specified then embed on a background of the exact `width` and `height` specified.
If the background contains an alpha value then WebP and PNG format output images will contain an alpha channel, even when the input image does not.
#### flatten()
Merge alpha transparency channel, if any, with `background`.
#### rotate([angle])
@@ -262,7 +331,7 @@ This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensio
#### sharpen()
Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
Perform a mild sharpen of the output image. This typically reduces performance by 10%.
#### interpolateWith(interpolator)
@@ -270,30 +339,33 @@ Use the given interpolator for image resizing, where `interpolator` is an attrib
Possible interpolators, in order of performance, are:
* `bilinear`: Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation), the default (and fastest) interpolation.
* `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" when enlarging and 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.
#### grayscale() / greyscale()
Convert to 8-bit greyscale; 256 shades of grey.
This is a linear operation. If the input image is in a non-linear colour space such as sRGB, use `gamma()` with `greyscale()` for the best results.
The output image will still be web-friendly sRGB and contain three (identical) channels.
### Output options
#### 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.
#### 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.
#### 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.
#### jpeg()
Use JPEG format for the output image.
@@ -306,6 +378,26 @@ Use PNG format for the output image.
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()
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 0 and 9.
### Output methods
#### toFile(filename, [callback])
@@ -314,8 +406,8 @@ Use WebP format for the output image.
`callback`, if present, is called with two arguments `(err, info)` where:
* `err` contains an error message, if any
* `info` contains the final resized image dimensions in its `width` and `height` properties
* `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.
@@ -325,9 +417,9 @@ Write image data to a Buffer, the format of which will match the input image by
`callback`, if present, gets three arguments `(err, buffer, info)` where:
* `err` is an error message, if any
* `buffer` is the resultant image data
* `info` contains the final resized image dimensions in its `width` and `height` properties
* `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.
@@ -348,6 +440,18 @@ 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.
@@ -361,31 +465,52 @@ var counters = sharp.counters(); // { queue: 2, process: 4 }
## Testing
[![Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp)
### Ubuntu 12.04
npm test
[![Ubuntu 12.04 Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp)
Running the tests requires both ImageMagick and GraphicsMagick plus one of either libmagick++-dev or libgraphicsmagick++.
### Centos 6.5
brew install imagemagick
brew install graphicsmagick
[![Centos 6.5 Build Status](https://snap-ci.com/lovell/sharp/branch/master/build_image)](https://snap-ci.com/lovell/sharp/branch/master)
sudo apt-get install imagemagick graphicsmagick libmagick++-dev
### 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 libmagick++-dev
```
```
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
* 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
* AWS EC2 [c3.xlarge](http://aws.amazon.com/ec2/instance-types/#Compute_Optimized)
* Ubuntu 14.04
* libvips 7.40.8
* liborc 0.4.22
### The contenders
* [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.
* [imagemagick-native](https://github.com/mash/node-imagemagick-native) v1.2.2 - Supports Buffers only and blocks main V8 thread whilst processing.
* [imagemagick](https://github.com/yourdeveloper/node-imagemagick) v0.1.3 - Supports filesystem only and "has been unmaintained for a long time".
* [gm](https://github.com/aheckmann/gm) v1.16.0 - Fully featured wrapper around GraphicsMagick.
* sharp v0.6.2 - Caching within libvips disabled to ensure a fair comparison.
### The task
@@ -395,25 +520,43 @@ Decompress a 2725x2225 JPEG image, resize and crop to 720x480, then compress to
| 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 |
| imagemagick-native | buffer | buffer | 1.58 | 1 |
| imagemagick | file | file | 6.23 | 3.9 |
| gm | buffer | file | 5.32 | 3.4 |
| gm | buffer | buffer | 5.32 | 3.4 |
| gm | file | file | 5.36 | 3.4 |
| gm | file | buffer | 5.36 | 3.4 |
| sharp | buffer | file | 22.05 | 14.0 |
| sharp | buffer | buffer | 22.14 | 14.0 |
| sharp | file | file | 21.79 | 13.8 |
| sharp | file | buffer | 21.90 | 13.9 |
| sharp | stream | stream | 20.87 | 13.2 |
| sharp +promise | file | buffer | 21.89 | 13.9 |
| sharp +sharpen | file | buffer | 19.69 | 12.5 |
| sharp +progressive | file | buffer | 16.93 | 10.7 |
| sharp +sequentialRead | file | buffer | 21.60 | 13.7 |
You can expect much greater performance with caching enabled (default) and using 16+ core machines.
You can expect greater performance with caching enabled (default) and using 8+ 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)
* [Daniel Gasienica](https://github.com/gasi)
* [Julian Walker](https://github.com/julianwa)
* [Amit Pitaru](https://github.com/apitaru)
* [Brandon Aaron](https://github.com/brandonaaron)
Thank you!
## Licence
Copyright 2013, 2014 Lovell Fuller, Pierre Inglebert, Jonathan Ong and Chanon Sajjamanochai
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.

150
index.js
View File

@@ -3,7 +3,10 @@
var util = require('util');
var stream = require('stream');
var color = require('color');
var Promise = require('bluebird');
var sharp = require('./build/Release/sharp');
var Sharp = function(input) {
@@ -12,21 +15,38 @@ var Sharp = function(input) {
}
stream.Duplex.call(this);
this.options = {
// input options
streamIn: false,
sequentialRead: false,
// resize options
topOffsetPre: -1,
leftOffsetPre: -1,
widthPre: -1,
heightPre: -1,
topOffsetPost: -1,
leftOffsetPost: -1,
widthPost: -1,
heightPost: -1,
width: -1,
height: -1,
canvas: 'c',
gravity: 0,
angle: 0,
withoutEnlargement: false,
sharpen: false,
interpolator: 'bilinear',
// operations
background: [0, 0, 0, 255],
flatten: false,
sharpen: false,
gamma: 0,
greyscale: false,
// output options
output: '__input',
progressive: false,
sequentialRead: false,
quality: 80,
compressionLevel: 6,
streamIn: false,
streamOut: false,
output: '__input'
withMetadata: false
};
if (typeof input === 'string') {
// input=file
@@ -51,6 +71,7 @@ util.inherits(Sharp, stream.Duplex);
Handle incoming chunk on Writable Stream
*/
Sharp.prototype._write = function(chunk, encoding, callback) {
/*jslint unused: false */
if (this.options.streamIn) {
if (typeof chunk === 'object' || chunk instanceof Buffer) {
if (typeof this.options.bufferIn === 'undefined') {
@@ -89,13 +110,40 @@ Sharp.prototype.crop = function(gravity) {
return this;
};
Sharp.prototype.embedWhite = function() {
this.options.canvas = 'w';
Sharp.prototype.extract = function(topOffset, leftOffset, width, height) {
/*jslint unused: false */
var suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post';
var values = arguments;
['topOffset', 'leftOffset', 'width', 'height'].forEach(function(name, index) {
this.options[name + suffix] = values[index];
}.bind(this));
return this;
};
Sharp.prototype.embedBlack = function() {
this.options.canvas = 'b';
/*
Deprecated embed* methods, to be removed in v0.8.0
*/
Sharp.prototype.embedWhite = util.deprecate(function() {
return this.background('white').embed();
}, "embedWhite() is deprecated, use background('white').embed() instead");
Sharp.prototype.embedBlack = util.deprecate(function() {
return this.background('black').embed();
}, "embedBlack() is deprecated, use background('black').embed() instead");
/*
Set the background colour for embed and flatten operations.
Delegates to the 'Color' module, which can throw an Error
but is liberal in what it accepts, clamping values to sensible min/max.
*/
Sharp.prototype.background = function(rgba) {
var colour = color(rgba);
this.options.background = colour.rgbArray();
this.options.background.push(colour.alpha() * 255);
return this;
};
Sharp.prototype.embed = function() {
this.options.canvas = 'e';
return this;
};
@@ -104,6 +152,11 @@ Sharp.prototype.max = function() {
return this;
};
Sharp.prototype.flatten = function(flatten) {
this.options.flatten = (typeof flatten === 'boolean') ? flatten : true;
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
@@ -138,6 +191,7 @@ Sharp.prototype.sharpen = function(sharpen) {
Set the interpolator to use for the affine transformation
*/
module.exports.interpolator = {
nearest: 'nearest',
bilinear: 'bilinear',
bicubic: 'bicubic',
nohalo: 'nohalo',
@@ -150,17 +204,29 @@ Sharp.prototype.interpolateWith = function(interpolator) {
};
/*
Deprecated interpolation methods, to be removed in v0.7.0
Darken image pre-resize (1/gamma) and brighten post-resize (gamma).
Improves brightness of resized image in non-linear colour spaces.
*/
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');
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;
};
/*
Convert to greyscale
*/
Sharp.prototype.greyscale = function(greyscale) {
this.options.greyscale = (typeof greyscale === 'boolean') ? greyscale : true;
return this;
};
Sharp.prototype.grayscale = Sharp.prototype.greyscale;
Sharp.prototype.progressive = function(progressive) {
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
@@ -182,14 +248,19 @@ Sharp.prototype.quality = function(quality) {
};
Sharp.prototype.compressionLevel = function(compressionLevel) {
if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) {
if (!Number.isNaN(compressionLevel) && compressionLevel >= 0 && compressionLevel <= 9) {
this.options.compressionLevel = compressionLevel;
} else {
throw new Error('Invalid compressionLevel (-1 to 9) ' + compressionLevel);
throw new Error('Invalid compressionLevel (0 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;
@@ -245,31 +316,16 @@ Sharp.prototype.toBuffer = function(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;
};
@@ -307,16 +363,22 @@ Sharp.prototype._sharp = function(callback) {
// output=stream, input=stream
this.on('finish', function() {
sharp.resize(that.options, function(err, data) {
if (err) throw err;
that.push(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) throw err;
that.push(data);
if (err) {
that.emit('error', new Error(err));
} else {
that.push(data);
}
that.push(null);
});
}
@@ -406,6 +468,16 @@ module.exports.cache = function(memory, items) {
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
*/

View File

@@ -1,11 +1,16 @@
{
"name": "sharp",
"version": "0.6.1",
"version": "0.7.0",
"author": "Lovell Fuller <npm@lovell.info>",
"contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>",
"Jonathan Ong <jonathanrichardong@gmail.com>",
"Chanon Sajjamanochai <chanon.s@gmail.com>"
"Chanon Sajjamanochai <chanon.s@gmail.com>",
"Juliano Julio <julianojulio@gmail.com>",
"Daniel Gasienica <daniel@gasienica.ch>",
"Julian Walker <julian@fiftythree.com>",
"Amit Pitaru <pitaru.amit@gmail.com>",
"Brandon Aaron <hello.brandon@aaron.sh>"
],
"description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library",
"scripts": {
@@ -26,6 +31,7 @@
"thumbnail",
"sharpen",
"crop",
"extract",
"embed",
"libvips",
"vips",
@@ -34,12 +40,13 @@
"stream"
],
"dependencies": {
"nan": "^1.3.0",
"bluebird": "^2.3.1"
"bluebird": "^2.3.5",
"color": "^0.7.1",
"nan": "^1.3.0"
},
"devDependencies": {
"imagemagick": "^0.1.3",
"imagemagick-native": "^1.2.2",
"imagemagick-native": "^1.4.0",
"gm": "^1.16.0",
"async": "^0.9.0",
"benchmark": "^1.0.0"

View File

@@ -11,38 +11,63 @@
using namespace v8;
using namespace node;
typedef enum {
CROP,
MAX,
EMBED
} Canvas;
struct resize_baton {
std::string file_in;
void* buffer_in;
size_t buffer_in_len;
std::string output;
std::string output_format;
void* buffer_out;
size_t buffer_out_len;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
int heightPre;
int topOffsetPost;
int leftOffsetPost;
int widthPost;
int heightPost;
int width;
int height;
bool crop;
Canvas canvas;
int gravity;
bool max;
VipsExtend extend;
bool sharpen;
std::string interpolator;
double background[4];
bool flatten;
bool sharpen;
double gamma;
bool greyscale;
bool progressive;
bool without_enlargement;
VipsAccess access_method;
int quality;
int compressionLevel;
int compression_level;
int angle;
std::string err;
bool with_metadata;
resize_baton():
buffer_in_len(0),
output_format(""),
buffer_out_len(0),
crop(false),
topOffsetPre(-1),
topOffsetPost(-1),
canvas(CROP),
gravity(0),
max(false),
background{0.0, 0.0, 0.0, 255.0},
flatten(false),
sharpen(false),
gamma(0.0),
greyscale(false),
progressive(false),
without_enlargement(false) {}
without_enlargement(false),
with_metadata(false) {}
};
typedef enum {
@@ -83,14 +108,22 @@ static bool is_tiff(std::string const &str) {
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
}
static void resize_error(resize_baton *baton, VipsImage *unref) {
static void resize_error(resize_baton *baton, VipsObject *hook) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(unref);
g_object_unref(hook);
vips_thread_shutdown();
return;
}
typedef enum {
ANGLE_0,
ANGLE_90,
ANGLE_180,
ANGLE_270,
ANGLE_LAST
} Angle;
/*
Calculate the angle of rotation for the output image.
In order of priority:
@@ -98,27 +131,27 @@ static void resize_error(resize_baton *baton, VipsImage *unref) {
2. Use input image EXIF Orientation header (does not support mirroring)
3. Otherwise default to zero, i.e. no rotation
*/
static VipsAngle
static Angle
sharp_calc_rotation(int const angle, VipsImage const *input) {
VipsAngle rotate = VIPS_ANGLE_0;
Angle rotate = ANGLE_0;
if (angle == -1) {
const char *exif;
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
if (exif[0] == 0x36) { // "6"
rotate = VIPS_ANGLE_90;
rotate = ANGLE_90;
} else if (exif[0] == 0x33) { // "3"
rotate = VIPS_ANGLE_180;
rotate = ANGLE_180;
} else if (exif[0] == 0x38) { // "8"
rotate = VIPS_ANGLE_270;
rotate = ANGLE_270;
}
}
} else {
if (angle == 90) {
rotate = VIPS_ANGLE_90;
rotate = ANGLE_90;
} else if (angle == 180) {
rotate = VIPS_ANGLE_180;
rotate = ANGLE_180;
} else if (angle == 270) {
rotate = VIPS_ANGLE_270;
rotate = ANGLE_270;
}
}
return rotate;
@@ -154,6 +187,19 @@ sharp_calc_crop(int const inWidth, int const inHeight, int const outWidth, int c
return std::make_tuple(left, top);
}
/*
Does this image have an alpha channel?
Uses colour space interpretation with number of channels to guess this.
*/
static bool
sharp_image_has_alpha(VipsImage *image) {
return (
(image->Bands == 2 && image->Type == VIPS_INTERPRETATION_B_W) ||
(image->Bands == 4 && image->Type != VIPS_INTERPRETATION_CMYK) ||
(image->Bands == 5 && image->Type == VIPS_INTERPRETATION_CMYK)
);
}
/*
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
Returns the ImageType detected, if any.
@@ -221,6 +267,7 @@ struct metadata_baton {
int height;
std::string space;
int channels;
bool has_alpha;
int orientation;
std::string err;
@@ -240,7 +287,7 @@ class MetadataWorker : public NanAsyncWorker {
g_atomic_int_dec_and_test(&counter_queue);
ImageType imageType = UNKNOWN;
VipsImage *image = vips_image_new();
VipsImage *image;
if (baton->buffer_in_len > 1) {
// From buffer
imageType = sharp_init_image_from_buffer(&image, baton->buffer_in, baton->buffer_in_len, VIPS_ACCESS_RANDOM);
@@ -269,6 +316,7 @@ class MetadataWorker : public NanAsyncWorker {
baton->height = image->Ysize;
baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type);
baton->channels = image->Bands;
baton->has_alpha = sharp_image_has_alpha(image);
// EXIF Orientation
const char *exif;
if (!vips_image_get_string(image, "exif-ifd0-Orientation", &exif)) {
@@ -296,6 +344,7 @@ class MetadataWorker : public NanAsyncWorker {
info->Set(NanNew<String>("height"), NanNew<Number>(baton->height));
info->Set(NanNew<String>("space"), NanNew<String>(baton->space));
info->Set(NanNew<String>("channels"), NanNew<Number>(baton->channels));
info->Set(NanNew<String>("hasAlpha"), NanNew<Boolean>(baton->has_alpha));
if (baton->orientation > 0) {
info->Set(NanNew<String>("orientation"), NanNew<Number>(baton->orientation));
}
@@ -353,33 +402,49 @@ class ResizeWorker : public NanAsyncWorker {
// Increment processing task counter
g_atomic_int_inc(&counter_process);
// Hang image references from this hook object
VipsObject *hook = reinterpret_cast<VipsObject*>(vips_image_new());
// Input
ImageType inputImageType = UNKNOWN;
VipsImage *in = vips_image_new();
VipsImage *image = vips_image_new();
vips_object_local(hook, image);
if (baton->buffer_in_len > 1) {
// From buffer
inputImageType = sharp_init_image_from_buffer(&in, baton->buffer_in, baton->buffer_in_len, baton->access_method);
inputImageType = sharp_init_image_from_buffer(&image, baton->buffer_in, baton->buffer_in_len, baton->access_method);
if (inputImageType == UNKNOWN) {
(baton->err).append("Input buffer contains unsupported image format");
}
} else {
// From file
inputImageType = sharp_init_image_from_file(&in, baton->file_in.c_str(), baton->access_method);
inputImageType = sharp_init_image_from_file(&image, baton->file_in.c_str(), baton->access_method);
if (inputImageType == UNKNOWN) {
(baton->err).append("File is of an unsupported image format");
}
}
if (inputImageType == UNKNOWN) {
return resize_error(baton, in);
return resize_error(baton, hook);
}
// Pre extraction
if (baton->topOffsetPre != -1) {
VipsImage *extractedPre = vips_image_new();
vips_object_local(hook, extractedPre);
if (vips_extract_area(image, &extractedPre, baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = extractedPre;
}
// Get input image width and height
int inputWidth = in->Xsize;
int inputHeight = in->Ysize;
int inputWidth = image->Xsize;
int inputHeight = image->Ysize;
// Calculate angle of rotation, to be carried out later
VipsAngle rotation = sharp_calc_rotation(baton->angle, in);
if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
Angle rotation = sharp_calc_rotation(baton->angle, image);
if (rotation == ANGLE_90 || rotation == ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = inputWidth;
inputWidth = inputHeight;
@@ -392,9 +457,9 @@ class ResizeWorker : public NanAsyncWorker {
// Fixed width and height
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
factor = (baton->canvas == CROP) ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
// if max is set, we need to compute the real size of the thumb image
if (baton->max) {
if (baton->canvas == MAX) {
if (xfactor > yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
@@ -432,9 +497,9 @@ class ResizeWorker : public NanAsyncWorker {
}
}
// Try to use libjpeg shrink-on-load
// Try to use libjpeg shrink-on-load, but not when applying gamma correction or pre-resize extract
int shrink_on_load = 1;
if (inputImageType == JPEG) {
if (inputImageType == JPEG && baton->gamma == 0 && baton->topOffsetPre == -1) {
if (shrink >= 8) {
factor = factor / 8;
shrink_on_load = 8;
@@ -446,37 +511,99 @@ class ResizeWorker : public NanAsyncWorker {
shrink_on_load = 2;
}
}
VipsImage *shrunk_on_load = vips_image_new();
if (shrink_on_load > 1) {
// Recalculate integral shrink and double residual
factor = std::max(factor, 1.0);
shrink = floor(factor);
residual = static_cast<double>(shrink) / factor;
// Reload input using shrink-on-load
g_object_unref(image);
if (baton->buffer_in_len > 1) {
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, in);
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &image, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, hook);
}
} else {
if (vips_jpegload((baton->file_in).c_str(), &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, in);
if (vips_jpegload((baton->file_in).c_str(), &image, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, hook);
}
}
} else {
vips_copy(in, &shrunk_on_load, NULL);
}
g_object_unref(in);
VipsImage *shrunk = vips_image_new();
if (shrink > 1) {
// Use vips_shrink with the integral reduction
if (vips_shrink(shrunk_on_load, &shrunk, shrink, shrink, NULL)) {
return resize_error(baton, shrunk_on_load);
// Handle colour profile, if any, for non sRGB images
if (image->Type != VIPS_INTERPRETATION_sRGB && vips_image_get_typeof(image, VIPS_META_ICC_NAME)) {
// Import embedded profile
VipsImage *profile = vips_image_new();
vips_object_local(hook, profile);
if (vips_icc_import(image, &profile, NULL, "embedded", TRUE, "pcs", VIPS_PCS_XYZ, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = profile;
// Convert to sRGB colour space
VipsImage *colourspaced = vips_image_new();
vips_object_local(hook, colourspaced);
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = colourspaced;
}
// Flatten image to remove alpha channel
if (baton->flatten && sharp_image_has_alpha(image)) {
// Background colour
VipsArrayDouble *background = vips_array_double_newv(
3, // Ignore alpha channel as we're about to remove it
baton->background[0],
baton->background[1],
baton->background[2]
);
VipsImage *flattened = vips_image_new();
vips_object_local(hook, flattened);
if (vips_flatten(image, &flattened, "background", background, NULL)) {
vips_area_unref(reinterpret_cast<VipsArea*>(background));
return resize_error(baton, hook);
};
vips_area_unref(reinterpret_cast<VipsArea*>(background));
g_object_unref(image);
image = flattened;
}
// Gamma encoding (darken)
if (baton->gamma >= 1 && baton->gamma <= 3) {
VipsImage *gamma_encoded = vips_image_new();
vips_object_local(hook, gamma_encoded);
if (vips_gamma(image, &gamma_encoded, "exponent", 1.0 / baton->gamma, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = gamma_encoded;
}
// Convert to greyscale (linear, therefore after gamma encoding, if any)
if (baton->greyscale) {
VipsImage *greyscale = vips_image_new();
vips_object_local(hook, greyscale);
if (vips_colourspace(image, &greyscale, VIPS_INTERPRETATION_B_W, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = greyscale;
}
if (shrink > 1) {
VipsImage *shrunk = vips_image_new();
vips_object_local(hook, shrunk);
// Use vips_shrink with the integral reduction
if (vips_shrink(image, &shrunk, shrink, shrink, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = shrunk;
// Recalculate residual float based on dimensions of required vs shrunk images
double shrunkWidth = shrunk->Xsize;
double shrunkHeight = shrunk->Ysize;
if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
if (rotation == ANGLE_90 || rotation == ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = shrunkWidth;
shrunkWidth = shrunkHeight;
@@ -484,145 +611,247 @@ class ResizeWorker : public NanAsyncWorker {
}
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
if (baton->crop || baton->max) {
residual = std::max(residualx, residualy);
} else {
if (baton->canvas == EMBED) {
residual = std::min(residualx, residualy);
} else {
residual = std::max(residualx, residualy);
}
} else {
vips_copy(shrunk_on_load, &shrunk, NULL);
}
g_object_unref(shrunk_on_load);
// Use vips_affine with the remaining float part
VipsImage *affined = vips_image_new();
if (residual != 0) {
VipsImage *affined = vips_image_new();
vips_object_local(hook, affined);
// Create interpolator - "bilinear" (default), "bicubic" or "nohalo"
VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str());
// Perform affine transformation
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) {
if (vips_affine(image, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) {
g_object_unref(interpolator);
return resize_error(baton, shrunk);
return resize_error(baton, hook);
}
g_object_unref(interpolator);
} else {
vips_copy(shrunk, &affined, NULL);
g_object_unref(image);
image = affined;
}
g_object_unref(shrunk);
// Rotate
VipsImage *rotated = vips_image_new();
if (rotation != VIPS_ANGLE_0) {
if (vips_rot(affined, &rotated, rotation, NULL)) {
return resize_error(baton, affined);
if (rotation != ANGLE_0) {
VipsImage *rotated = vips_image_new();
vips_object_local(hook, rotated);
if (vips_rot(image, &rotated, static_cast<VipsAngle>(rotation), NULL)) {
return resize_error(baton, hook);
}
} else {
vips_copy(affined, &rotated, NULL);
g_object_unref(image);
image = rotated;
}
g_object_unref(affined);
// Crop/embed
VipsImage *canvased = vips_image_new();
if (rotated->Xsize != baton->width || rotated->Ysize != baton->height) {
if (baton->crop || baton->max) {
if (image->Xsize != baton->width || image->Ysize != baton->height) {
if (baton->canvas == EMBED) {
// Match background colour space, namely sRGB
if (image->Type != VIPS_INTERPRETATION_sRGB) {
// Convert to sRGB colour space
VipsImage *colourspaced = vips_image_new();
vips_object_local(hook, colourspaced);
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = colourspaced;
}
// Add non-transparent alpha channel, if required
if (baton->background[3] < 255.0 && !sharp_image_has_alpha(image)) {
// Create single-channel transparency
VipsImage *black = vips_image_new();
vips_object_local(hook, black);
if (vips_black(&black, image->Xsize, image->Ysize, "bands", 1, NULL)) {
return resize_error(baton, hook);
}
// Invert to become non-transparent
VipsImage *alpha = vips_image_new();
vips_object_local(hook, alpha);
if (vips_invert(black, &alpha, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(black);
// Append alpha channel to existing image
VipsImage *joined = vips_image_new();
vips_object_local(hook, joined);
if (vips_bandjoin2(image, alpha, &joined, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(alpha);
g_object_unref(image);
image = joined;
}
// Create background
VipsArrayDouble *background;
if (baton->background[3] < 255.0) {
background = vips_array_double_newv(
4, baton->background[0], baton->background[1], baton->background[2], baton->background[3]
);
} else {
background = vips_array_double_newv(
3, baton->background[0], baton->background[1], baton->background[2]
);
}
// Embed
int left = (baton->width - image->Xsize) / 2;
int top = (baton->height - image->Ysize) / 2;
VipsImage *embedded = vips_image_new();
vips_object_local(hook, embedded);
if (vips_embed(image, &embedded, left, top, baton->width, baton->height,
"extend", VIPS_EXTEND_BACKGROUND, "background", background, NULL
)) {
vips_area_unref(reinterpret_cast<VipsArea*>(background));
return resize_error(baton, hook);
}
vips_area_unref(reinterpret_cast<VipsArea*>(background));
g_object_unref(image);
image = embedded;
} else {
// Crop/max
int left;
int top;
std::tie(left, top) = sharp_calc_crop(rotated->Xsize, rotated->Ysize, baton->width, baton->height, baton->gravity);
int width = std::min(rotated->Xsize, baton->width);
int height = std::min(rotated->Ysize, baton->height);
if (vips_extract_area(rotated, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, rotated);
}
} else {
// Embed
int left = (baton->width - rotated->Xsize) / 2;
int top = (baton->height - rotated->Ysize) / 2;
if (vips_embed(rotated, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) {
return resize_error(baton, rotated);
std::tie(left, top) = sharp_calc_crop(image->Xsize, image->Ysize, baton->width, baton->height, baton->gravity);
int width = std::min(image->Xsize, baton->width);
int height = std::min(image->Ysize, baton->height);
VipsImage *extracted = vips_image_new();
vips_object_local(hook, extracted);
if (vips_extract_area(image, &extracted, left, top, width, height, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = extracted;
}
} else {
vips_copy(rotated, &canvased, NULL);
}
g_object_unref(rotated);
// Post extraction
if (baton->topOffsetPost != -1) {
VipsImage *extractedPost = vips_image_new();
vips_object_local(hook, extractedPost);
if (vips_extract_area(image, &extractedPost, baton->leftOffsetPost, baton->topOffsetPost, baton->widthPost, baton->heightPost, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = extractedPost;
}
// Mild sharpen
VipsImage *sharpened = vips_image_new();
if (baton->sharpen) {
VipsImage *sharpened = vips_image_new();
vips_object_local(hook, sharpened);
VipsImage *sharpen = vips_image_new_matrixv(3, 3,
-1.0, -1.0, -1.0,
-1.0, 32.0, -1.0,
-1.0, -1.0, -1.0);
vips_image_set_double(sharpen, "scale", 24);
if (vips_conv(canvased, &sharpened, sharpen, NULL)) {
g_object_unref(sharpen);
return resize_error(baton, canvased);
vips_object_local(hook, sharpen);
if (vips_conv(image, &sharpened, sharpen, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(sharpen);
} else {
vips_copy(canvased, &sharpened, NULL);
g_object_unref(image);
image = sharpened;
}
g_object_unref(canvased);
// Always convert to sRGB colour space
VipsImage *colourspaced = vips_image_new();
vips_colourspace(sharpened, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL);
g_object_unref(sharpened);
// Gamma decoding (brighten)
if (baton->gamma >= 1 && baton->gamma <= 3) {
VipsImage *gamma_decoded = vips_image_new();
vips_object_local(hook, gamma_decoded);
if (vips_gamma(image, &gamma_decoded, "exponent", baton->gamma, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = gamma_decoded;
}
// Convert to sRGB colour space, if not already
if (image->Type != VIPS_INTERPRETATION_sRGB) {
VipsImage *colourspaced = vips_image_new();
vips_object_local(hook, colourspaced);
if (vips_colourspace(image, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL)) {
return resize_error(baton, hook);
}
g_object_unref(image);
image = colourspaced;
}
// Generate image tile cache when interlace output is required
VipsImage *cached = vips_image_new();
if (baton->progressive) {
if (vips_tilecache(colourspaced, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) {
return resize_error(baton, colourspaced);
VipsImage *cached = vips_image_new();
vips_object_local(hook, cached);
if (vips_tilecache(image, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) {
return resize_error(baton, hook);
}
} else {
vips_copy(colourspaced, &cached, NULL);
g_object_unref(image);
image = cached;
}
g_object_unref(colourspaced);
// Output
VipsImage *output = cached;
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == JPEG)) {
// Write JPEG to buffer
if (vips_jpegsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
if (vips_jpegsave_buffer(image, &baton->buffer_out, &baton->buffer_out_len, "strip", !baton->with_metadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, hook);
}
baton->output_format = "jpeg";
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == PNG)) {
// Write PNG to buffer
if (vips_pngsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
if (vips_pngsave_buffer(image, &baton->buffer_out, &baton->buffer_out_len, "strip", !baton->with_metadata,
"compression", baton->compression_level, "interlace", baton->progressive, NULL)) {
return resize_error(baton, hook);
}
baton->output_format = "png";
} else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == WEBP)) {
// Write WEBP to buffer
if (vips_webpsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
}
} else if (is_jpeg(baton->output)) {
// Write JPEG to file
if (vips_jpegsave(output, baton->output.c_str(), "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} else if (is_png(baton->output)) {
// Write PNG to file
if (vips_pngsave(output, baton->output.c_str(), "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} else if (is_webp(baton->output)) {
// Write WEBP to file
if (vips_webpsave(output, baton->output.c_str(), "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
}
} else if (is_tiff(baton->output)) {
// Write TIFF to file
if (vips_tiffsave(output, baton->output.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
if (vips_webpsave_buffer(image, &baton->buffer_out, &baton->buffer_out_len, "strip", !baton->with_metadata,
"Q", baton->quality, NULL)) {
return resize_error(baton, hook);
}
baton->output_format = "webp";
} else {
(baton->err).append("Unsupported output " + baton->output);
bool output_jpeg = is_jpeg(baton->output);
bool output_png = is_png(baton->output);
bool output_webp = is_webp(baton->output);
bool output_tiff = is_tiff(baton->output);
bool match_input = !(output_jpeg || output_png || output_webp || output_tiff);
if (output_jpeg || (match_input && inputImageType == JPEG)) {
// Write JPEG to file
if (vips_jpegsave(image, baton->output.c_str(), "strip", !baton->with_metadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, hook);
}
baton->output_format = "jpeg";
} else if (output_png || (match_input && inputImageType == PNG)) {
// Write PNG to file
if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->with_metadata,
"compression", baton->compression_level, "interlace", baton->progressive, NULL)) {
return resize_error(baton, hook);
}
baton->output_format = "png";
} else if (output_webp || (match_input && inputImageType == WEBP)) {
// Write WEBP to file
if (vips_webpsave(image, baton->output.c_str(), "strip", !baton->with_metadata,
"Q", baton->quality, NULL)) {
return resize_error(baton, hook);
}
baton->output_format = "webp";
} else if (output_tiff || (match_input && inputImageType == TIFF)) {
// Write TIFF to file
if (vips_tiffsave(image, baton->output.c_str(), "strip", !baton->with_metadata,
"compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
return resize_error(baton, hook);
}
baton->output_format = "tiff";
} else {
(baton->err).append("Unsupported output " + baton->output);
g_object_unref(image);
return resize_error(baton, hook);
}
}
g_object_unref(output);
// Clean up any dangling image references
g_object_unref(image);
g_object_unref(hook);
// Clean up libvips' per-request data and threads
vips_error_clear();
vips_thread_shutdown();
@@ -636,14 +865,25 @@ class ResizeWorker : public NanAsyncWorker {
// Error
argv[0] = NanNew<String>(baton->err.data(), baton->err.size());
} else {
int width = baton->width;
int height = baton->height;
if (baton->topOffsetPre != -1 && (baton->width == -1 || baton->height == -1)) {
width = baton->widthPre;
height = baton->heightPre;
}
if (baton->topOffsetPost != -1) {
width = baton->widthPost;
height = baton->heightPost;
}
// Info Object
Local<Object> info = NanNew<Object>();
info->Set(NanNew<String>("width"), NanNew<Number>(baton->width));
info->Set(NanNew<String>("height"), NanNew<Number>(baton->height));
info->Set(NanNew<String>("format"), NanNew<String>(baton->output_format));
info->Set(NanNew<String>("width"), NanNew<Number>(width));
info->Set(NanNew<String>("height"), NanNew<Number>(height));
if (baton->buffer_out_len > 0) {
// Buffer
argv[1] = NanNewBufferHandle((char *)baton->buffer_out, baton->buffer_out_len);
argv[1] = NanNewBufferHandle(static_cast<char*>(baton->buffer_out), baton->buffer_out_len);
g_free(baton->buffer_out);
argv[2] = info;
} else {
@@ -664,6 +904,7 @@ class ResizeWorker : public NanAsyncWorker {
resize_baton* baton;
};
/*
resize(options, output, callback)
*/
@@ -676,36 +917,54 @@ NAN_METHOD(resize) {
// Input filename
baton->file_in = *String::Utf8Value(options->Get(NanNew<String>("fileIn"))->ToString());
baton->access_method = options->Get(NanNew<String>("sequentialRead"))->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
// Input Buffer object
if (options->Get(NanNew<String>("bufferIn"))->IsObject()) {
Local<Object> buffer = options->Get(NanNew<String>("bufferIn"))->ToObject();
baton->buffer_in_len = Buffer::Length(buffer);
baton->buffer_in = Buffer::Data(buffer);
}
// Extract image options
baton->topOffsetPre = options->Get(NanNew<String>("topOffsetPre"))->Int32Value();
baton->leftOffsetPre = options->Get(NanNew<String>("leftOffsetPre"))->Int32Value();
baton->widthPre = options->Get(NanNew<String>("widthPre"))->Int32Value();
baton->heightPre = options->Get(NanNew<String>("heightPre"))->Int32Value();
baton->topOffsetPost = options->Get(NanNew<String>("topOffsetPost"))->Int32Value();
baton->leftOffsetPost = options->Get(NanNew<String>("leftOffsetPost"))->Int32Value();
baton->widthPost = options->Get(NanNew<String>("widthPost"))->Int32Value();
baton->heightPost = options->Get(NanNew<String>("heightPost"))->Int32Value();
// Output image dimensions
baton->width = options->Get(NanNew<String>("width"))->Int32Value();
baton->height = options->Get(NanNew<String>("height"))->Int32Value();
// Canvas options
// Canvas option
Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
if (canvas->Equals(NanNew<String>("c"))) {
baton->crop = true;
} else if (canvas->Equals(NanNew<String>("w"))) {
baton->extend = VIPS_EXTEND_WHITE;
} else if (canvas->Equals(NanNew<String>("b"))) {
baton->extend = VIPS_EXTEND_BLACK;
baton->canvas = CROP;
} else if (canvas->Equals(NanNew<String>("m"))) {
baton->max = true;
baton->canvas = MAX;
} else if (canvas->Equals(NanNew<String>("e"))) {
baton->canvas = EMBED;
}
// Other options
baton->gravity = options->Get(NanNew<String>("gravity"))->Int32Value();
baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
// Background colour
Local<Array> background = Local<Array>::Cast(options->Get(NanNew<String>("background")));
for (int i = 0; i < 4; i++) {
baton->background[i] = background->Get(i)->NumberValue();
}
// Resize options
baton->without_enlargement = options->Get(NanNew<String>("withoutEnlargement"))->BooleanValue();
baton->access_method = options->Get(NanNew<String>("sequentialRead"))->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
baton->gravity = options->Get(NanNew<String>("gravity"))->Int32Value();
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
// Operators
baton->flatten = options->Get(NanNew<String>("flatten"))->BooleanValue();
baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
baton->gamma = options->Get(NanNew<String>("gamma"))->NumberValue();
baton->greyscale = options->Get(NanNew<String>("greyscale"))->BooleanValue();
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
// Output options
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
baton->compression_level = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
baton->with_metadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
// Output filename or __format for Buffer
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
@@ -727,7 +986,12 @@ NAN_METHOD(cache) {
// Set cache memory limit
if (args[0]->IsInt32()) {
vips_cache_set_max_mem(args[0]->Int32Value() * 1048576);
int newMax = args[0]->Int32Value() * 1048576;
int oldMax = vips_cache_get_max_mem();
vips_cache_set_max_mem(newMax);
// Notify the V8 garbage collector of delta in max cache size
NanAdjustExternalMemory(newMax - oldMax);
}
// Set cache items limit
@@ -744,6 +1008,20 @@ NAN_METHOD(cache) {
NanReturnValue(cache);
}
/*
Get and set size of thread pool
*/
NAN_METHOD(concurrency) {
NanScope();
// Set concurrency
if (args[0]->IsInt32()) {
vips_concurrency_set(args[0]->Int32Value());
}
// Get concurrency
NanReturnValue(NanNew<Number>(vips_concurrency_get()));
}
/*
Get internal counters (queued tasks, processing tasks)
*/
@@ -762,15 +1040,21 @@ static void at_exit(void* arg) {
extern "C" void init(Handle<Object> target) {
NanScope();
vips_init("");
vips_init("sharp");
AtExit(at_exit);
// Set libvips operation cache limits
vips_cache_set_max_mem(100 * 1048576); // 100 MB
vips_cache_set_max(500); // 500 operations
// Notify the V8 garbage collector of max cache size
NanAdjustExternalMemory(vips_cache_get_max_mem());
// Methods available to JavaScript
NODE_SET_METHOD(target, "metadata", metadata);
NODE_SET_METHOD(target, "resize", resize);
NODE_SET_METHOD(target, "cache", cache);
NODE_SET_METHOD(target, "concurrency", concurrency);
NODE_SET_METHOD(target, "counters", counters);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

BIN
tests/fixtures/blackbug.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
tests/fixtures/gamma_dalai_lama_gray.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

7
tests/leak.sh Executable file
View 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 --num-callers=20 node unit.js

View File

@@ -1,37 +1,42 @@
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;
var timer = setInterval(function() {
console.dir(sharp.cache());
}, 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.cache());
});
/*jslint node: true */
'use strict';
var sharp = require('../index');
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) {
/*jslint unused: false */
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());
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +1,69 @@
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();
/*jslint node: true */
'use strict';
var sharp = require('../index');
var path = require('path');
var imagemagick = require('imagemagick');
var gm = require('gm');
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
View 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
}

View File

@@ -2,36 +2,47 @@
/*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 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 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 inputJpg = path.join(fixturesPath, '2569067123_aca715a2ee_o.jpg'); // http://www.flickr.com/photos/grizdave/2569067123/
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 inputJpgWithCmykProfile = path.join(fixturesPath, 'Channel_digital_image_CMYK_color.jpg'); // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.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 inputPng = path.join(fixturesPath, '50020484-00001.png'); // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
var inputPngWithTransparency = path.join(fixturesPath, 'blackbug.png'); // public domain
var inputJpgWithExif = path.join(fixturesPath, "Landscape_8.jpg"); // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
var inputWebP = path.join(fixturesPath, '4.webp'); // http://www.gstatic.com/webp/gallery/4.webp
var inputTiff = path.join(fixturesPath, 'G31D.TIF'); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
var inputGif = path.join(fixturesPath, 'Crash_test.gif'); // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
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
var outputJpg = path.join(fixturesPath, 'output.jpg');
var outputZoinks = path.join(fixturesPath, 'output.zoinks'); // an 'unknown' file extension
// 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();
@@ -42,6 +53,7 @@ async.series([
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();
@@ -52,6 +64,7 @@ async.series([
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();
@@ -62,6 +75,7 @@ async.series([
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();
@@ -72,11 +86,51 @@ async.series([
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();
});
},
// Embed - JPEG within PNG, no alpha channel
function(done) {
sharp(inputJpg)
.embed()
.resize(320, 240)
.png()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual(3, metadata.channels);
done();
});
});
},
// Embed - JPEG within WebP, to include alpha channel
function(done) {
sharp(inputJpg)
.resize(320, 240)
.background({r: 0, g: 0, b: 0, a: 0})
.embed()
.webp()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual(4, metadata.channels);
done();
});
});
},
// Quality
function(done) {
sharp(inputJpg).resize(320, 240).quality(70).toBuffer(function(err, buffer70) {
@@ -84,6 +138,7 @@ async.series([
sharp(inputJpg).resize(320, 240).toBuffer(function(err, buffer80) {
if (err) throw err;
sharp(inputJpg).resize(320, 240).quality(90).toBuffer(function(err, buffer90) {
if (err) throw err;
assert(buffer70.length < buffer80.length);
assert(buffer80.length < buffer90.length);
done();
@@ -93,9 +148,10 @@ async.series([
},
// TIFF with dimensions known to cause rounding errors
function(done) {
sharp(inputTiff).resize(240, 320).embedBlack().jpeg().toBuffer(function(err, data, info) {
sharp(inputTiff).resize(240, 320).embed().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();
@@ -105,6 +161,7 @@ async.series([
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();
@@ -115,6 +172,7 @@ async.series([
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();
@@ -125,6 +183,7 @@ async.series([
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();
@@ -135,6 +194,7 @@ async.series([
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();
@@ -152,6 +212,7 @@ async.series([
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();
@@ -162,6 +223,7 @@ async.series([
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();
@@ -172,6 +234,7 @@ async.series([
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();
@@ -182,6 +245,7 @@ async.series([
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();
@@ -202,6 +266,7 @@ async.series([
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();
@@ -212,6 +277,7 @@ async.series([
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();
@@ -223,6 +289,7 @@ async.series([
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();
@@ -243,7 +310,41 @@ async.series([
},
// Check colour space conversion occurs from TIFF to WebP (this used to segfault)
function(done) {
sharp(inputTiff).webp().toBuffer().then(function() {
sharp(inputTiff).webp().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
done();
});
},
// Check colour space conversion from CMYK to sRGB
function(done) {
sharp(inputJpgWithCmykProfile).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);
done();
});
},
// Check colour space conversion from CMYK to sRGB works with background colour (yellow=fail)
function(done) {
sharp(inputJpgWithCmykProfile).resize(320, 240).background('white').embed().toFile(path.join(fixturesPath, 'output.cmyk2srgb.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
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();
});
},
@@ -252,6 +353,7 @@ async.series([
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();
@@ -262,6 +364,7 @@ async.series([
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();
@@ -272,6 +375,7 @@ async.series([
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();
@@ -282,6 +386,7 @@ async.series([
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();
@@ -292,6 +397,7 @@ async.series([
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();
@@ -304,6 +410,7 @@ async.series([
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);
@@ -320,6 +427,7 @@ async.series([
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);
@@ -333,6 +441,7 @@ async.series([
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);
@@ -345,6 +454,8 @@ async.series([
var readable = fs.createReadStream(inputJpg);
var pipeline = sharp().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();
@@ -359,6 +470,7 @@ async.series([
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);
@@ -368,6 +480,35 @@ async.series([
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);
fs.unlinkSync(outputJpg);
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);
fs.unlinkSync(outputJpg);
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) {
@@ -422,6 +563,147 @@ async.series([
done();
});
},
// Extract jpg
function(done) {
sharp(inputJpg).extract(2,2,20,20).toFile(path.join(fixturesPath, 'output.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(20, info.width);
assert.strictEqual(20, info.height);
done();
});
},
// Extract png
function(done) {
sharp(inputPng).extract(300,200,400,200).toFile(path.join(fixturesPath, 'output.extract.png'), function(err, info) {
if (err) throw err;
assert.strictEqual(400, info.width);
assert.strictEqual(200, info.height);
done();
});
},
// Extract webp
function(done) {
sharp(inputWebP).extract(50, 100, 125, 200).toFile(path.join(fixturesPath, 'output.extract.webp'), function(err, info) {
if (err) throw err;
assert.strictEqual(125, info.width);
assert.strictEqual(200, info.height);
done();
});
},
// Extract tiff
function(done) {
sharp(inputTiff).extract(63, 34, 341, 529).toFile(path.join(fixturesPath, 'output.extract.tiff'), function(err, info) {
if (err) throw err;
assert.strictEqual(341, info.width);
assert.strictEqual(529, info.height);
done();
});
},
// Extract before resize
function(done) {
sharp(inputJpg).extract(10, 10, 10, 500, 500).resize(100, 100).toFile(path.join(fixturesPath, 'output.extract.resize.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
},
// Extract after resize and crop
function(done) {
sharp(inputJpg).resize(500, 500).crop(sharp.gravity.north).extract(10, 10, 100, 100).toFile(path.join(fixturesPath, 'output.resize.crop.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
},
// Extract before and after resize and crop
function(done) {
sharp(inputJpg).extract(0, 0, 700, 700).resize(500, 500).crop(sharp.gravity.north).extract(10, 10, 100, 100).toFile(path.join(fixturesPath, 'output.extract.resize.crop.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, 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(outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(outputZoinks);
done();
});
},
function(done) {
sharp(inputPng).resize(320, 80).toFile(outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(outputZoinks);
done();
});
},
function(done) {
sharp(inputPngWithTransparency).resize(320, 80).toFile(outputZoinks, 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(outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('webp', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(outputZoinks);
done();
});
},
function(done) {
sharp(inputTiff).resize(320, 80).toFile(outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('tiff', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
fs.unlinkSync(outputZoinks);
done();
});
},
function(done) {
sharp(inputGif).resize(320, 80).toFile(outputZoinks, function(err) {
assert(!!err);
done();
});
},
// Metadata - JPEG
function(done) {
sharp(inputJpg).metadata(function(err, metadata) {
@@ -444,6 +726,7 @@ async.series([
assert.strictEqual(600, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(8, metadata.orientation);
done();
});
@@ -457,6 +740,7 @@ async.series([
assert.strictEqual(3248, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
},
@@ -469,18 +753,33 @@ async.series([
assert.strictEqual(2074, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
},
// Metadata - Transparent PNG
function(done) {
sharp(inputPngWithTransparency).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual(2048, metadata.width);
assert.strictEqual(1536, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(4, metadata.channels);
assert.strictEqual(true, metadata.hasAlpha);
done();
});
},
// Metadata - WebP
function(done) {
sharp(inputWebp).metadata(function(err, metadata) {
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);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
},
@@ -492,6 +791,7 @@ async.series([
assert.strictEqual(800, metadata.width);
assert.strictEqual(533, metadata.height);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
},
@@ -503,6 +803,7 @@ async.series([
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
},
@@ -516,6 +817,7 @@ async.series([
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
readable.pipe(pipeline);
@@ -530,6 +832,7 @@ async.series([
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
image.resize(metadata.width / 2).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
@@ -539,11 +842,93 @@ async.series([
});
});
},
// 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();
});
},
// Greyscale conversion
function(done) {
sharp(inputJpg).resize(320, 240).greyscale().toFile(path.join(fixturesPath, 'output.greyscale-gamma-0.0.jpg'), function(err) {
if (err) throw err;
done();
});
},
function(done) {
sharp(inputJpg).resize(320, 240).gamma().greyscale().toFile(path.join(fixturesPath, 'output.greyscale-gamma-2.2.jpg'), function(err) {
if (err) throw err;
done();
});
},
// Flattening
function(done) {
sharp(inputPngWithTransparency).flatten().resize(400, 300).toFile(path.join(fixturesPath, 'output.flatten-black.jpg'), function(err) {
if (err) throw err;
done();
});
},
function(done) {
sharp(inputPngWithTransparency).flatten().background({r: 255, g: 102, b: 0}).resize(400, 300).toFile(path.join(fixturesPath, 'output.flatten-rgb-orange.jpg'), function(err) {
if (err) throw err;
done();
});
},
function(done) {
sharp(inputPngWithTransparency).flatten().background('#ff6600').resize(400, 300).toFile(path.join(fixturesPath, 'output.flatten-hex-orange.jpg'), function(err) {
if (err) throw err;
done();
});
},
function(done) {
sharp(inputJpg).background('#ff0000').flatten().resize(500, 400).toFile(path.join(fixturesPath, 'output.flatten-input-jpg.jpg'), function(err) {
if (err) throw err;
done();
});
},
// PNG compression level - valid
function(done) {
var isValid = false;
try {
sharp().compressionLevel(0);
isValid = true;
} catch (e) {}
assert(isValid);
done();
},
// PNG compression level - invalid
function(done) {
var isValid = false;
try {
sharp().compressionLevel(-1);
isValid = true;
} catch (e) {}
assert(!isValid);
done();
},
// Verify internal counters
function(done) {
var counters = sharp.counters();
assert.strictEqual(0, counters.queue);
assert.strictEqual(0, counters.process);
done();
},
// Empty cache
function(done) {
sharp.cache(0);
done();
}
]);