Compare commits

..

25 Commits

Author SHA1 Message Date
Lovell Fuller
740838b47c Prevent auto-rotate fail for libmagick formats #117 2014-11-08 12:07:47 +00:00
Lovell Fuller
f7c2a839ad Add support for profile-less CMYK images #99 2014-11-06 11:30:19 +00:00
Lovell Fuller
62fcfb3dba Add link to gulp-responsive module 2014-11-04 11:23:40 +00:00
Lovell Fuller
333e8789f4 Include cmath, erroneously removed in 3a9a137f 2014-10-29 13:32:19 +00:00
Lovell Fuller
3a9a137f40 Modify C++ 11 to be MS-friendly #19 2014-10-29 13:16:16 +00:00
Lovell Fuller
5c51612982 Version bumps ahead of v0.7.1 2014-10-28 11:12:40 +00:00
Lovell Fuller
a472adeb74 Fail fast for Buffer with unsupported format #105 2014-10-28 10:53:11 +00:00
Lovell Fuller
2e61839387 Prepare preinstall.sh for inclusion in master 2014-10-28 09:59:28 +00:00
Lovell Fuller
51805ef657 Add root check and support for Debian 7, Mint 13 2014-10-27 22:32:39 +00:00
Lovell Fuller
5856e41a62 Merge pull request #111 from papandreou/preinstallOnDebianWheezy
Build from source on Debian Wheezy
2014-10-27 15:47:29 +00:00
Andreas Lind
ffbe6b7d76 Build from source on Debian Wheezy. 2014-10-27 15:23:40 +01:00
Lovell Fuller
ed6a966534 Add shell script to install libvips 2014-10-24 10:57:45 +01:00
Lovell Fuller
97fc2a2a3a Ensure resize error from C++ is wrapped
Thanks to @papandreou
2014-10-23 09:39:37 +01:00
Lovell Fuller
3e1be7a33a Merge pull request #107 from papandreou/doNotUnrefUnparsableImages
Do not call g_object_unref when imageType comes out as UNKNOWN.
2014-10-23 09:23:37 +01:00
Lovell Fuller
4a4dd7f987 Merge pull request #106 from papandreou/errorInstances
Pass errors as Error instances rather than strings.
2014-10-23 09:22:15 +01:00
Andreas Lind
005c628352 Do not call g_object_unref when imageType comes out as UNKNOWN.
Avoids "(sharp:23220): GLib-GObject-CRITICAL **: g_object_unref: assertion 'G_IS_OBJECT (object)' failed" dumped to the console when an unsupported or invalid image is loaded.
2014-10-23 08:32:23 +02:00
Andreas Lind
1bd316de80 Pass errors as Error instances rather than strings. 2014-10-23 08:27:10 +02:00
Lovell Fuller
49b44d8238 Ensure compilation on RHEL/Centos 6 gcc 2014-10-21 22:01:43 +01:00
Lovell Fuller
8bc1981891 Refactor C++ code
Split features into their own file

Style is becoming less C, more C++
2014-10-21 21:38:19 +01:00
Lovell Fuller
db6dc6431b Add support for mirroring #62 2014-10-21 14:47:08 +01:00
Lovell Fuller
f214673c3c Add Coveralls badge 2014-10-21 12:57:24 +01:00
Lovell Fuller
ffe00ee398 Travis CI + Coveralls integration 2014-10-21 12:40:11 +01:00
Lovell Fuller
6cade5bd7f Increase unit test coverage to ~95% 2014-10-21 12:22:23 +01:00
Lovell Fuller
a531b5917e Add details about running leak and bench tests 2014-10-20 15:54:44 +01:00
Lovell Fuller
ca561daedf Test code refactor #63
Use mocha for feature-specific unit tests

Add test coverage report via istanbul
2014-10-20 15:32:56 +01:00
54 changed files with 3191 additions and 2336 deletions

18
.gitignore vendored
View File

@@ -1,19 +1,9 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
build
node_modules
tests/fixtures/output.*
tests/libvips.supp
coverage
test/bench/node_modules
test/fixtures/output.*
test/leak/libvips.supp
# Mac OS X
.DS_Store

3
.jshintignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
test/bench/node_modules
coverage

8
.jshintrc Normal file
View File

@@ -0,0 +1,8 @@
{
"strict": true,
"node": true,
"globals": {
"describe": true,
"it": true
}
}

View File

@@ -1,18 +1,8 @@
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
build
node_modules
coverage
.jshintignore
.jshintrc
.gitignore
tests
.travis.yml
test
.travis.yml

View File

@@ -3,15 +3,6 @@ node_js:
- "0.10"
- "0.11"
before_install:
- sudo add-apt-repository ppa:lyrasis/precise-backports -y
- sudo apt-get update -qq
- sudo apt-get install -qq automake gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libexif-dev libxml2-dev swig graphicsmagick libmagick++-dev
- git clone https://github.com/jcupitt/libvips.git
- cd libvips
- git checkout 7.38
- ./bootstrap.sh
- ./configure --enable-debug=no --enable-cxx=yes --without-orc --without-python --without-fftw
- make
- sudo make install
- sudo ldconfig
- cd $TRAVIS_BUILD_DIR
- curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
after_success:
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js

120
README.md
View File

@@ -31,9 +31,32 @@ This module is powered by the blazingly fast [libvips](https://github.com/jcupit
* Node.js v0.10+
* [libvips](https://github.com/jcupitt/libvips) v7.38.5+
_libvips_ can take advantage of [liborc](http://code.entropywave.com/orc/) if present. Warning: versions of _liborc_ prior to 0.4.19 suffer [memory leaks](https://github.com/lovell/sharp/issues/21#issuecomment-42367306) and version 0.4.19 suffers [buffer overflows](https://github.com/lovell/sharp/issues/21#issuecomment-44813498).
To install the latest version of libvips on the following Operating Systems:
### Install libvips on Mac OS
* Mac OS
* Homebrew
* MacPorts
* Debian Linux
* Debian 7, 8
* Ubuntu 12.04, 14.04, 14.10
* Mint 13, 17
* Red Hat Linux
* RHEL/Centos/Scientific 6, 7
* Fedora 21, 22
run the following as a user with `sudo` access:
curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | sudo bash -
or run the following as `root`:
curl -s https://raw.githubusercontent.com/lovell/sharp/master/preinstall.sh | bash -
The [preinstall.sh](https://github.com/lovell/sharp/blob/master/preinstall.sh) script requires `curl` and `pkg-config`.
### Mac OS tips
Manual install via homebrew:
brew install homebrew/science/vips --with-webp --with-graphicsmagick
@@ -45,54 +68,13 @@ The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp
brew link gettext --force
### Install libvips on Linux
#### Ubuntu 14.04 LTS
sudo apt-get install libvips-dev
#### Ubuntu 12.04 LTS
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.40
./bootstrap.sh
./configure --enable-debug=no --enable-cxx=yes --without-python --without-orc --without-fftw
make
sudo make install
sudo ldconfig
#### Debian Jessie
apt-get install libvips-dev
#### Centos 6
sudo yum groupinstall -y "Development Tools"
sudo yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel ImageMagick-devel
sudo yum install -y http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm
sudo yum install -y --enablerepo=nux-dextop gobject-introspection-devel
sudo yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
sudo yum install -y --enablerepo=remi libwebp-devel
git clone https://github.com/jcupitt/libvips.git
cd libvips
git checkout 7.40
./bootstrap.sh
./configure --prefix=/usr --enable-docs=no --enable-debug=no --enable-cxx=yes --without-orc --without-python --without-fftw
make
sudo make install
sudo ldconfig
### Install libvips on Heroku
[Alessandro Tagliapietra](https://github.com/alex88) maintains an [Heroku buildpack for libvips](https://github.com/alex88/heroku-buildpack-vips) and its dependencies.
### Using with gulp.js
[Mohammad Prabowo](https://github.com/rizalp) maintains a [gulp.js plugin](https://github.com/rizalp/gulp-sharp).
[Eugeny Vlasenko](https://github.com/mahnunchik) maintains [gulp-responsive](https://www.npmjs.org/package/gulp-responsive) and [Mohammad Prabowo](https://github.com/rizalp) maintains [gulp-sharp](https://www.npmjs.org/package/gulp-sharp).
## Usage examples
@@ -321,7 +303,15 @@ Rotate the output image by either an explicit angle or auto-orient based on the
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
Use this method without `angle` to determine the angle from EXIF data. Mirroring is currently unsupported.
Use this method without `angle` to determine the angle from EXIF data. Mirroring is supported and may infer the use of a `flip` operation.
#### flip()
Flip the image about the vertical Y axis. This always occurs after rotation, if any.
#### flop()
Flop the image about the horizontal X axis. This always occurs after rotation, if any.
#### withoutEnlargement()
@@ -465,21 +455,52 @@ var counters = sharp.counters(); // { queue: 2, process: 4 }
## Testing
### Ubuntu 12.04
### Functional tests
#### Coverage
[![Test Coverage](https://coveralls.io/repos/lovell/sharp/badge.png?branch=master)](https://coveralls.io/r/lovell/sharp?branch=master)
#### Ubuntu 12.04
[![Ubuntu 12.04 Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp)
### Centos 6.5
#### Centos 6.5
[![Centos 6.5 Build Status](https://snap-ci.com/lovell/sharp/branch/master/build_image)](https://snap-ci.com/lovell/sharp/branch/master)
### It worked on my machine
#### It worked on my machine
```
npm test
```
Running the comparative performance tests requires _ImageMagick_ and _GraphicsMagick_.
### Memory leak tests
```
cd sharp/test/leak
./leak.sh
```
Requires _valgrind_:
```
brew install valgrind
```
```
sudo apt-get install -qq valgrind
```
### Benchmark tests
```
cd sharp/test/bench
npm install
npm test
```
Requires both _ImageMagick_ and _GraphicsMagick_:
```
brew install imagemagick
@@ -551,6 +572,7 @@ This module would never have been possible without the help and code contributio
* [Julian Walker](https://github.com/julianwa)
* [Amit Pitaru](https://github.com/apitaru)
* [Brandon Aaron](https://github.com/brandonaaron)
* [Andreas Lind](https://github.com/papandreou)
Thank you!

View File

@@ -1,7 +1,13 @@
{
'targets': [{
'target_name': 'sharp',
'sources': ['src/sharp.cc'],
'sources': [
'src/common.cc',
'src/utilities.cc',
'src/metadata.cc',
'src/resize.cc',
'src/sharp.cc'
],
'variables': {
'PKG_CONFIG_PATH': '<!(which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true):$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig'
},
@@ -12,11 +18,20 @@
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips glib-2.0)',
'<!(node -e "require(\'nan\')")'
],
'cflags': ['-fexceptions', '-Wall', '-O3'],
'cflags_cc': ['-std=c++0x', '-fexceptions', '-Wall', '-O3'],
'cflags_cc': [
'-std=c++0x',
'-fexceptions',
'-Wall',
'-O3'
],
'xcode_settings': {
'OTHER_CFLAGS': ['-std=c++11', '-stdlib=libc++', '-fexceptions', '-Wall', '-O3'],
'OTHER_CPLUSPLUSFLAGS': ['-std=c++11', '-stdlib=libc++', '-fexceptions', '-Wall', '-O3'],
'OTHER_CPLUSPLUSFLAGS': [
'-std=c++11',
'-stdlib=libc++',
'-fexceptions',
'-Wall',
'-O3'
],
'MACOSX_DEPLOYMENT_TARGET': '10.7'
}
}]

BIN
icc/USWebCoatedSWOP.icc Normal file

Binary file not shown.

View File

@@ -1,11 +1,11 @@
/*jslint node: true */
'use strict';
var path = require('path');
var util = require('util');
var stream = require('stream');
var color = require('color');
var Promise = require('bluebird');
var BluebirdPromise = require('bluebird');
var sharp = require('./build/Release/sharp');
@@ -18,6 +18,8 @@ var Sharp = function(input) {
// input options
streamIn: false,
sequentialRead: false,
// ICC profile to use when input CMYK image has no embedded profile
iccProfileCmyk: path.join(__dirname, 'icc', 'USWebCoatedSWOP.icc'),
// resize options
topOffsetPre: -1,
leftOffsetPre: -1,
@@ -32,6 +34,8 @@ var Sharp = function(input) {
canvas: 'c',
gravity: 0,
angle: 0,
flip: false,
flop: false,
withoutEnlargement: false,
interpolator: 'bilinear',
// operations
@@ -53,10 +57,15 @@ var Sharp = function(input) {
this.options.fileIn = input;
} else if (typeof input === 'object' && input instanceof Buffer) {
// input=buffer
if (input.length > 0) {
if (
(input.length > 1) &&
(input[0] === 0xff && input[1] === 0xd8) || // JPEG
(input[0] === 0x89 && input[1] === 0x50) || // PNG
(input[0] === 0x52 && input[1] === 0x49) // WebP
) {
this.options.bufferIn = input;
} else {
throw new Error('Buffer is empty');
throw new Error('Buffer contains an unsupported image format. JPEG, PNG and WebP are currently supported.');
}
} else {
// input=stream
@@ -73,7 +82,7 @@ util.inherits(Sharp, stream.Duplex);
Sharp.prototype._write = function(chunk, encoding, callback) {
/*jslint unused: false */
if (this.options.streamIn) {
if (typeof chunk === 'object' || chunk instanceof Buffer) {
if (typeof chunk === 'object' && chunk instanceof Buffer) {
if (typeof this.options.bufferIn === 'undefined') {
// Create new Buffer
this.options.bufferIn = new Buffer(chunk.length);
@@ -99,13 +108,10 @@ module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'sout
Sharp.prototype.crop = function(gravity) {
this.options.canvas = 'c';
if (typeof gravity !== 'undefined') {
// Is this a supported gravity?
if (!Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
this.options.gravity = gravity;
} else {
throw new Error('Unsupported crop gravity ' + gravity);
}
if (typeof gravity === 'number' && !Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
this.options.gravity = gravity;
} else {
throw new Error('Unsupported crop gravity ' + gravity);
}
return this;
};
@@ -172,6 +178,22 @@ Sharp.prototype.rotate = function(angle) {
return this;
};
/*
Flip the image vertically, about the Y axis
*/
Sharp.prototype.flip = function(flip) {
this.options.flip = (typeof flip === 'boolean') ? flip : true;
return this;
};
/*
Flop the image horizontally, about the X axis
*/
Sharp.prototype.flop = function(flop) {
this.options.flop = (typeof flop === 'boolean') ? flop : true;
return this;
};
/*
Do not enlarge the output if the input width *or* height are already less than the required dimensions
This is equivalent to GraphicsMagick's ">" geometry option:
@@ -265,7 +287,7 @@ Sharp.prototype.resize = function(width, height) {
if (!width) {
this.options.width = -1;
} else {
if (!Number.isNaN(width)) {
if (typeof width === 'number' && !Number.isNaN(width)) {
this.options.width = width;
} else {
throw new Error('Invalid width ' + width);
@@ -274,7 +296,7 @@ Sharp.prototype.resize = function(width, height) {
if (!height) {
this.options.height = -1;
} else {
if (!Number.isNaN(height)) {
if (typeof height === 'number' && !Number.isNaN(height)) {
this.options.height = height;
} else {
throw new Error('Invalid height ' + height);
@@ -292,7 +314,7 @@ Sharp.prototype.toFile = function(output, callback) {
if (typeof callback === 'function') {
callback(errOutputInvalid);
} else {
return Promise.reject(errOutputInvalid);
return BluebirdPromise.reject(errOutputInvalid);
}
} else {
if (this.options.fileIn === output) {
@@ -300,7 +322,7 @@ Sharp.prototype.toFile = function(output, callback) {
if (typeof callback === 'function') {
callback(errOutputIsInput);
} else {
return Promise.reject(errOutputIsInput);
return BluebirdPromise.reject(errOutputIsInput);
}
} else {
this.options.output = output;
@@ -387,7 +409,7 @@ Sharp.prototype._sharp = function(callback) {
// output=promise
if (this.options.streamIn) {
// output=promise, input=stream
return new Promise(function(resolve, reject) {
return new BluebirdPromise(function(resolve, reject) {
that.on('finish', function() {
sharp.resize(that.options, function(err, data) {
if (err) {
@@ -400,7 +422,7 @@ Sharp.prototype._sharp = function(callback) {
});
} else {
// output=promise, input=file/buffer
return new Promise(function(resolve, reject) {
return new BluebirdPromise(function(resolve, reject) {
sharp.resize(that.options, function(err, data) {
if (err) {
reject(err);
@@ -430,7 +452,7 @@ Sharp.prototype.metadata = function(callback) {
return this;
} else {
if (this.options.streamIn) {
return new Promise(function(resolve, reject) {
return new BluebirdPromise(function(resolve, reject) {
that.on('finish', function() {
sharp.metadata(that.options, function(err, data) {
if (err) {
@@ -442,7 +464,7 @@ Sharp.prototype.metadata = function(callback) {
});
});
} else {
return new Promise(function(resolve, reject) {
return new BluebirdPromise(function(resolve, reject) {
sharp.metadata(that.options, function(err, data) {
if (err) {
reject(err);
@@ -459,10 +481,10 @@ Sharp.prototype.metadata = function(callback) {
Get and set cache memory and item limits
*/
module.exports.cache = function(memory, items) {
if (Number.isNaN(memory)) {
if (typeof memory !== 'number' || Number.isNaN(memory)) {
memory = null;
}
if (Number.isNaN(items)) {
if (typeof items !== 'number' || Number.isNaN(items)) {
items = null;
}
return sharp.cache(memory, items);
@@ -472,7 +494,7 @@ module.exports.cache = function(memory, items) {
Get and set size of thread pool
*/
module.exports.concurrency = function(concurrency) {
if (Number.isNaN(concurrency)) {
if (typeof concurrency !== 'number' || Number.isNaN(concurrency)) {
concurrency = null;
}
return sharp.concurrency(concurrency);

View File

@@ -1,6 +1,6 @@
{
"name": "sharp",
"version": "0.7.0",
"version": "0.7.2",
"author": "Lovell Fuller <npm@lovell.info>",
"contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>",
@@ -10,11 +10,12 @@
"Daniel Gasienica <daniel@gasienica.ch>",
"Julian Walker <julian@fiftythree.com>",
"Amit Pitaru <pitaru.amit@gmail.com>",
"Brandon Aaron <hello.brandon@aaron.sh>"
"Brandon Aaron <hello.brandon@aaron.sh>",
"Andreas Lind <andreas@one.com>"
],
"description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library",
"scripts": {
"test": "node tests/unit && node tests/perf"
"test": "node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- --slow=5000 --timeout=10000 ./test/unit/*.js"
},
"main": "index.js",
"repository": {
@@ -40,16 +41,15 @@
"stream"
],
"dependencies": {
"bluebird": "^2.3.5",
"bluebird": "^2.3.10",
"color": "^0.7.1",
"nan": "^1.3.0"
"nan": "^1.4.0"
},
"devDependencies": {
"imagemagick": "^0.1.3",
"imagemagick-native": "^1.4.0",
"gm": "^1.16.0",
"async": "^0.9.0",
"benchmark": "^1.0.0"
"mocha": "^2.0.1",
"mocha-jshint": "^0.0.9",
"istanbul": "^0.3.2",
"coveralls": "^2.11.2"
},
"license": "Apache 2.0",
"engines": {

145
preinstall.sh Executable file
View File

@@ -0,0 +1,145 @@
#!/bin/sh
# Ensures libvips is installed and attempts to install it if not
# Currently supports:
# * Mac OS
# * Debian Linux
# * Debian 7, 8
# * Ubuntu 12.04, 14.04, 14.10
# * Mint 13, 17
# * Red Hat Linux
# * RHEL/Centos/Scientific 6, 7
# * Fedora 21, 22
vips_version_minimum=7.38.5
vips_version_latest_major=7.40
vips_version_latest_minor=11
install_libvips_from_source() {
echo "Compiling libvips $vips_version_latest_major.$vips_version_latest_minor from source"
curl -O http://www.vips.ecs.soton.ac.uk/supported/$vips_version_latest_major/vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
tar zvxf vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
cd vips-$vips_version_latest_major.$vips_version_latest_minor
./configure --enable-debug=no --enable-docs=no --enable-cxx=yes --without-python --without-orc --without-fftw $1
make
make install
cd ..
rm -rf vips-$vips_version_latest_major.$vips_version_latest_minor
rm vips-$vips_version_latest_major.$vips_version_latest_minor.tar.gz
ldconfig
echo "Installed libvips $vips_version_latest_major.$vips_version_latest_minor"
}
sorry() {
echo "Sorry, I don't yet know how to install libvips on $1"
exit 1
}
# Is libvips already installed, and is it at least the minimum required version?
if ! type pkg-config >/dev/null; then
sorry "a system without pkg-config"
fi
pkg_config_path_homebrew=`which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true`
pkg_config_path="$pkg_config_path_homebrew:$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig"
PKG_CONFIG_PATH=$pkg_config_path pkg-config --exists vips
if [ $? -eq 0 ]; then
vips_version_found=$(PKG_CONFIG_PATH=$pkg_config_path pkg-config --modversion vips)
pkg-config --atleast-version=$vips_version_minimum vips
if [ $? -eq 0 ]; then
# Found suitable version of libvips
echo "Found libvips $vips_version_found"
exit 0
fi
echo "Found libvips $vips_version_found but require $vips_version_minimum"
else
echo "Could not find libvips using a PKG_CONFIG_PATH of '$pkg_config_path'"
fi
# Verify root/sudo access
if [ "$(id -u)" -ne "0" ]; then
echo "Sorry, I need root/sudo access to continue"
exit 1
fi
# OS-specific installations of libvips follows
case $(uname -s) in
*[Dd]arwin*)
# Mac OS
echo "Detected Mac OS"
if type "brew" > /dev/null; then
echo "Installing libvips via homebrew"
brew install homebrew/science/vips --with-webp --with-graphicsmagick
elif type "port" > /dev/null; then
echo "Installing libvips via MacPorts"
port install vips
else
sorry "Mac OS without homebrew or MacPorts"
fi
;;
*)
if [ -f /etc/debian_version ]; then
# Debian Linux
DISTRO=$(lsb_release -c -s)
echo "Detected Debian Linux '$DISTRO'"
case "$DISTRO" in
jessie|trusty|utopic|qiana)
# Debian 8, Ubuntu 14, Mint 17
echo "Installing libvips via apt-get"
apt-get install -y libvips-dev
;;
precise|wheezy|maya)
# Debian 7, Ubuntu 12.04, Mint 13
echo "Installing libvips dependencies via apt-get"
add-apt-repository -y ppa:lyrasis/precise-backports
apt-get update
apt-get install -y automake build-essential gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libexif-dev libxml2-dev swig libmagickwand-dev curl
install_libvips_from_source
;;
*)
# Unsupported Debian-based OS
sorry "Debian-based $DISTRO"
;;
esac
elif [ -f /etc/redhat-release ]; then
# Red Hat Linux
RELEASE=$(cat /etc/redhat-release)
echo "Detected Red Hat Linux '$RELEASE'"
case $RELEASE in
"Red Hat Enterprise Linux release 7."*|"CentOS Linux release 7."*|"Scientific Linux release 7."*)
# RHEL/CentOS 7
echo "Installing libvips dependencies via yum"
yum groupinstall -y "Development Tools"
yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel ImageMagick-devel gobject-introspection-devel libwebp-devel curl
install_libvips_from_source "--prefix=/usr"
;;
"Red Hat Enterprise Linux release 6."*|"CentOS release 6."*|"Scientific Linux release 6."*)
# RHEL/CentOS 6
echo "Installing libvips dependencies via yum"
yum groupinstall -y "Development Tools"
yum install -y gtk-doc libxml2-devel libjpeg-turbo-devel libpng-devel libtiff-devel libexif-devel ImageMagick-devel curl
yum install -y http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm
yum install -y --enablerepo=nux-dextop gobject-introspection-devel
yum install -y http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
yum install -y --enablerepo=remi libwebp-devel
install_libvips_from_source "--prefix=/usr"
;;
"Fedora release 21 "*|"Fedora release 22 "*)
# Fedora 21, 22
echo "Installing libvips via yum"
yum install vips-devel
;;
*)
# Unsupported RHEL-based OS
sorry "$RELEASE"
;;
esac
else
# Unsupported OS
sorry "$(uname -a)"
fi
;;
esac

99
src/common.cc Executable file
View File

@@ -0,0 +1,99 @@
#include <string>
#include <string.h>
#include <vips/vips.h>
#include "common.h"
// How many tasks are in the queue?
volatile int counter_queue = 0;
// How many tasks are being processed?
volatile int counter_process = 0;
// Filename extension checkers
static bool ends_with(std::string const &str, std::string const &end) {
return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
}
bool is_jpeg(std::string const &str) {
return ends_with(str, ".jpg") || ends_with(str, ".jpeg") || ends_with(str, ".JPG") || ends_with(str, ".JPEG");
}
bool is_png(std::string const &str) {
return ends_with(str, ".png") || ends_with(str, ".PNG");
}
bool is_webp(std::string const &str) {
return ends_with(str, ".webp") || ends_with(str, ".WEBP");
}
bool is_tiff(std::string const &str) {
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
}
unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
unsigned char const MARKER_PNG[] = {0x89, 0x50};
unsigned char const MARKER_WEBP[] = {0x52, 0x49};
/*
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
Returns the ImageType detected, if any.
*/
ImageType
sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const length, VipsAccess const access) {
ImageType imageType = UNKNOWN;
if (memcmp(MARKER_JPEG, buffer, 2) == 0) {
if (!vips_jpegload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = JPEG;
}
} else if(memcmp(MARKER_PNG, buffer, 2) == 0) {
if (!vips_pngload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = PNG;
}
} else if(memcmp(MARKER_WEBP, buffer, 2) == 0) {
if (!vips_webpload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = WEBP;
}
}
return imageType;
}
/*
Initialise a VipsImage from a file.
Returns the ImageType detected, if any.
*/
ImageType
sharp_init_image_from_file(VipsImage **image, char const *file, VipsAccess const access) {
ImageType imageType = UNKNOWN;
if (vips_foreign_is_a("jpegload", file)) {
if (!vips_jpegload(file, image, "access", access, NULL)) {
imageType = JPEG;
}
} else if (vips_foreign_is_a("pngload", file)) {
if (!vips_pngload(file, image, "access", access, NULL)) {
imageType = PNG;
}
} else if (vips_foreign_is_a("webpload", file)) {
if (!vips_webpload(file, image, "access", access, NULL)) {
imageType = WEBP;
}
} else if (vips_foreign_is_a("tiffload", file)) {
if (!vips_tiffload(file, image, "access", access, NULL)) {
imageType = TIFF;
}
} else if(vips_foreign_is_a("magickload", file)) {
if (!vips_magickload(file, image, "access", access, NULL)) {
imageType = MAGICK;
}
}
return imageType;
}
/*
Does this image have an alpha channel?
Uses colour space interpretation with number of channels to guess this.
*/
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)
);
}

46
src/common.h Executable file
View File

@@ -0,0 +1,46 @@
#ifndef SHARP_COMMON_H
#define SHARP_COMMON_H
typedef enum {
UNKNOWN,
JPEG,
PNG,
WEBP,
TIFF,
MAGICK
} ImageType;
// Filename extension checkers
bool is_jpeg(std::string const &str);
bool is_png(std::string const &str);
bool is_webp(std::string const &str);
bool is_tiff(std::string const &str);
// How many tasks are in the queue?
extern volatile int counter_queue;
// How many tasks are being processed?
extern volatile int counter_process;
/*
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
Returns the ImageType detected, if any.
*/
ImageType
sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const length, VipsAccess const access);
/*
Initialise a VipsImage from a file.
Returns the ImageType detected, if any.
*/
ImageType
sharp_init_image_from_file(VipsImage **image, char const *file, VipsAccess const access);
/*
Does this image have an alpha channel?
Uses colour space interpretation with number of channels to guess this.
*/
bool
sharp_image_has_alpha(VipsImage *image);
#endif

144
src/metadata.cc Executable file
View File

@@ -0,0 +1,144 @@
#include <node.h>
#include <vips/vips.h>
#include "nan.h"
#include "common.h"
#include "metadata.h"
using namespace v8;
struct MetadataBaton {
// Input
std::string fileIn;
void* bufferIn;
size_t bufferInLength;
// Output
std::string format;
int width;
int height;
std::string space;
int channels;
bool hasAlpha;
int orientation;
std::string err;
MetadataBaton():
bufferInLength(0),
orientation(0) {}
};
class MetadataWorker : public NanAsyncWorker {
public:
MetadataWorker(NanCallback *callback, MetadataBaton *baton) : NanAsyncWorker(callback), baton(baton) {}
~MetadataWorker() {}
void Execute() {
// Decrement queued task counter
g_atomic_int_dec_and_test(&counter_queue);
ImageType imageType = UNKNOWN;
VipsImage *image;
if (baton->bufferInLength > 1) {
// From buffer
imageType = sharp_init_image_from_buffer(&image, baton->bufferIn, baton->bufferInLength, VIPS_ACCESS_RANDOM);
if (imageType == UNKNOWN) {
(baton->err).append("Input buffer contains unsupported image format");
}
} else {
// From file
imageType = sharp_init_image_from_file(&image, baton->fileIn.c_str(), VIPS_ACCESS_RANDOM);
if (imageType == UNKNOWN) {
(baton->err).append("File is of an unsupported image format");
}
}
if (imageType != UNKNOWN) {
// Image type
switch (imageType) {
case JPEG: baton->format = "jpeg"; break;
case PNG: baton->format = "png"; break;
case WEBP: baton->format = "webp"; break;
case TIFF: baton->format = "tiff"; break;
case MAGICK: baton->format = "magick"; break;
case UNKNOWN: default: baton->format = "";
}
// VipsImage attributes
baton->width = image->Xsize;
baton->height = image->Ysize;
baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type);
baton->channels = image->Bands;
baton->hasAlpha = sharp_image_has_alpha(image);
// EXIF Orientation
const char *exif;
if (!vips_image_get_string(image, "exif-ifd0-Orientation", &exif)) {
baton->orientation = atoi(&exif[0]);
}
}
// Clean up
if (imageType != UNKNOWN) {
g_object_unref(image);
}
vips_error_clear();
vips_thread_shutdown();
}
void HandleOKCallback () {
NanScope();
Handle<Value> argv[2] = { NanNull(), NanNull() };
if (!baton->err.empty()) {
// Error
argv[0] = Exception::Error(NanNew<String>(baton->err.data(), baton->err.size()));
} else {
// Metadata Object
Local<Object> info = NanNew<Object>();
info->Set(NanNew<String>("format"), NanNew<String>(baton->format));
info->Set(NanNew<String>("width"), NanNew<Number>(baton->width));
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->hasAlpha));
if (baton->orientation > 0) {
info->Set(NanNew<String>("orientation"), NanNew<Number>(baton->orientation));
}
argv[1] = info;
}
delete baton;
// Return to JavaScript
callback->Call(2, argv);
}
private:
MetadataBaton* baton;
};
/*
metadata(options, callback)
*/
NAN_METHOD(metadata) {
NanScope();
// V8 objects are converted to non-V8 types held in the baton struct
MetadataBaton *baton = new MetadataBaton;
Local<Object> options = args[0]->ToObject();
// Input filename
baton->fileIn = *String::Utf8Value(options->Get(NanNew<String>("fileIn"))->ToString());
// Input Buffer object
if (options->Get(NanNew<String>("bufferIn"))->IsObject()) {
Local<Object> buffer = options->Get(NanNew<String>("bufferIn"))->ToObject();
baton->bufferInLength = node::Buffer::Length(buffer);
baton->bufferIn = node::Buffer::Data(buffer);
}
// Join queue for worker thread
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
NanAsyncQueueWorker(new MetadataWorker(callback, baton));
// Increment queued task counter
g_atomic_int_inc(&counter_queue);
NanReturnUndefined();
}

8
src/metadata.h Executable file
View File

@@ -0,0 +1,8 @@
#ifndef SHARP_METADATA_H
#define SHARP_METADATA_H
#include "nan.h"
NAN_METHOD(metadata);
#endif

813
src/resize.cc Executable file
View File

@@ -0,0 +1,813 @@
#include <tuple>
#include <algorithm>
#include <cmath>
#include <node.h>
#include <node_buffer.h>
#include <vips/vips.h>
#include "nan.h"
#include "common.h"
#include "resize.h"
using namespace v8;
typedef enum {
CROP,
MAX,
EMBED
} Canvas;
typedef enum {
ANGLE_0,
ANGLE_90,
ANGLE_180,
ANGLE_270,
ANGLE_LAST
} Angle;
struct ResizeBaton {
std::string fileIn;
void* bufferIn;
size_t bufferInLength;
std::string iccProfileCmyk;
std::string output;
std::string outputFormat;
void* bufferOut;
size_t bufferOutLength;
int topOffsetPre;
int leftOffsetPre;
int widthPre;
int heightPre;
int topOffsetPost;
int leftOffsetPost;
int widthPost;
int heightPost;
int width;
int height;
Canvas canvas;
int gravity;
std::string interpolator;
double background[4];
bool flatten;
bool sharpen;
double gamma;
bool greyscale;
int angle;
bool flip;
bool flop;
bool progressive;
bool withoutEnlargement;
VipsAccess accessMethod;
int quality;
int compressionLevel;
std::string err;
bool withMetadata;
ResizeBaton():
bufferInLength(0),
outputFormat(""),
bufferOutLength(0),
topOffsetPre(-1),
topOffsetPost(-1),
canvas(CROP),
gravity(0),
flatten(false),
sharpen(false),
gamma(0.0),
greyscale(false),
flip(false),
flop(false),
progressive(false),
withoutEnlargement(false),
withMetadata(false) {
background[0] = 0.0;
background[1] = 0.0;
background[2] = 0.0;
background[3] = 255.0;
}
};
class ResizeWorker : public NanAsyncWorker {
public:
ResizeWorker(NanCallback *callback, ResizeBaton *baton) : NanAsyncWorker(callback), baton(baton) {}
~ResizeWorker() {}
/*
libuv worker
*/
void Execute() {
// Decrement queued task counter
g_atomic_int_dec_and_test(&counter_queue);
// 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 *image = vips_image_new();
vips_object_local(hook, image);
if (baton->bufferInLength > 1) {
// From buffer
inputImageType = sharp_init_image_from_buffer(&image, baton->bufferIn, baton->bufferInLength, baton->accessMethod);
if (inputImageType == UNKNOWN) {
(baton->err).append("Input buffer contains unsupported image format");
}
} else {
// From file
inputImageType = sharp_init_image_from_file(&image, baton->fileIn.c_str(), baton->accessMethod);
if (inputImageType == UNKNOWN) {
(baton->err).append("File is of an unsupported image format");
}
}
if (inputImageType == UNKNOWN) {
return 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 Error(baton, hook);
}
g_object_unref(image);
image = extractedPre;
}
// Get input image width and height
int inputWidth = image->Xsize;
int inputHeight = image->Ysize;
// Calculate angle of rotation, to be carried out later
Angle rotation;
bool flip;
std::tie(rotation, flip) = CalculateRotationAndFlip(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;
inputHeight = swap;
}
if (flip && !baton->flip) {
// Add flip operation due to EXIF mirroring
baton->flip = TRUE;
}
// Scaling calculations
double factor;
if (baton->width > 0 && baton->height > 0) {
// 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->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->canvas == MAX) {
if (xfactor > yfactor) {
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
baton->width = round(static_cast<double>(inputWidth) / yfactor);
}
}
} else if (baton->width > 0) {
// Fixed width, auto height
factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
baton->height = floor(static_cast<double>(inputHeight) / factor);
} else if (baton->height > 0) {
// Fixed height, auto width
factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
baton->width = floor(static_cast<double>(inputWidth) / factor);
} else {
// Identity transform
factor = 1;
baton->width = inputWidth;
baton->height = inputHeight;
}
int shrink = floor(factor);
if (shrink < 1) {
shrink = 1;
}
double residual = static_cast<double>(shrink) / factor;
// Do not enlarge the output if the input width *or* height are already less than the required dimensions
if (baton->withoutEnlargement) {
if (inputWidth < baton->width || inputHeight < baton->height) {
factor = 1;
shrink = 1;
residual = 0;
baton->width = inputWidth;
baton->height = inputHeight;
}
}
// 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 && baton->gamma == 0 && baton->topOffsetPre == -1) {
if (shrink >= 8) {
factor = factor / 8;
shrink_on_load = 8;
} else if (shrink >= 4) {
factor = factor / 4;
shrink_on_load = 4;
} else if (shrink >= 2) {
factor = factor / 2;
shrink_on_load = 2;
}
}
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->bufferInLength > 1) {
if (vips_jpegload_buffer(baton->bufferIn, baton->bufferInLength, &image, "shrink", shrink_on_load, NULL)) {
return Error(baton, hook);
}
} else {
if (vips_jpegload((baton->fileIn).c_str(), &image, "shrink", shrink_on_load, NULL)) {
return Error(baton, hook);
}
}
}
// Handle colour profile, if any, for non sRGB images
if (image->Type != VIPS_INTERPRETATION_sRGB) {
// Get the input colour profile
if (vips_image_get_typeof(image, VIPS_META_ICC_NAME)) {
// Use embedded profile
VipsImage *profile = vips_image_new();
vips_object_local(hook, profile);
if (vips_icc_import(image, &profile, "pcs", VIPS_PCS_XYZ, "embedded", TRUE, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = profile;
} else if (image->Type == VIPS_INTERPRETATION_CMYK) {
// CMYK with no embedded profile
VipsImage *profile = vips_image_new();
vips_object_local(hook, profile);
if (vips_icc_import(image, &profile, "pcs", VIPS_PCS_XYZ, "input_profile", (baton->iccProfileCmyk).c_str(), NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = profile;
}
// Attempt to 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 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 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 *gammaEncoded = vips_image_new();
vips_object_local(hook, gammaEncoded);
if (vips_gamma(image, &gammaEncoded, "exponent", 1.0 / baton->gamma, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = gammaEncoded;
}
// 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 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 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 == ANGLE_90 || rotation == ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = shrunkWidth;
shrunkWidth = shrunkHeight;
shrunkHeight = swap;
}
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
if (baton->canvas == EMBED) {
residual = std::min(residualx, residualy);
} else {
residual = std::max(residualx, residualy);
}
}
// Use vips_affine with the remaining float part
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(image, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) {
g_object_unref(interpolator);
return Error(baton, hook);
}
g_object_unref(interpolator);
g_object_unref(image);
image = affined;
}
// Rotate
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 Error(baton, hook);
}
g_object_unref(image);
image = rotated;
}
// Flip (mirror about Y axis)
if (baton->flip) {
VipsImage *flipped = vips_image_new();
vips_object_local(hook, flipped);
if (vips_flip(image, &flipped, VIPS_DIRECTION_VERTICAL, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = flipped;
}
// Flop (mirror about X axis)
if (baton->flop) {
VipsImage *flopped = vips_image_new();
vips_object_local(hook, flopped);
if (vips_flip(image, &flopped, VIPS_DIRECTION_HORIZONTAL, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = flopped;
}
// Crop/embed
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 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 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 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 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 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) = CalculateCrop(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 Error(baton, hook);
}
g_object_unref(image);
image = extracted;
}
}
// 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 Error(baton, hook);
}
g_object_unref(image);
image = extractedPost;
}
// Mild sharpen
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);
vips_object_local(hook, sharpen);
if (vips_conv(image, &sharpened, sharpen, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = sharpened;
}
// Gamma decoding (brighten)
if (baton->gamma >= 1 && baton->gamma <= 3) {
VipsImage *gammaDecoded = vips_image_new();
vips_object_local(hook, gammaDecoded);
if (vips_gamma(image, &gammaDecoded, "exponent", baton->gamma, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = gammaDecoded;
}
// 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 Error(baton, hook);
}
g_object_unref(image);
image = colourspaced;
}
// Generate image tile cache when interlace output is required
if (baton->progressive) {
VipsImage *cached = vips_image_new();
vips_object_local(hook, cached);
if (vips_tilecache(image, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) {
return Error(baton, hook);
}
g_object_unref(image);
image = cached;
}
// Output
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == JPEG)) {
// Write JPEG to buffer
if (vips_jpegsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "jpeg";
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == PNG)) {
// Write PNG to buffer
if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "png";
} else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == WEBP)) {
// Write WEBP to buffer
if (vips_webpsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata,
"Q", baton->quality, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "webp";
} else {
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->withMetadata,
"Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "jpeg";
} else if (output_png || (match_input && inputImageType == PNG)) {
// Write PNG to file
if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "png";
} else if (output_webp || (match_input && inputImageType == WEBP)) {
// Write WEBP to file
if (vips_webpsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"Q", baton->quality, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "webp";
} else if (output_tiff || (match_input && inputImageType == TIFF)) {
// Write TIFF to file
if (vips_tiffsave(image, baton->output.c_str(), "strip", !baton->withMetadata,
"compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
return Error(baton, hook);
}
baton->outputFormat = "tiff";
} else {
(baton->err).append("Unsupported output " + baton->output);
g_object_unref(image);
return Error(baton, hook);
}
}
// 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();
}
void HandleOKCallback () {
NanScope();
Handle<Value> argv[3] = { NanNull(), NanNull(), NanNull() };
if (!baton->err.empty()) {
// Error
argv[0] = Exception::Error(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>("format"), NanNew<String>(baton->outputFormat));
info->Set(NanNew<String>("width"), NanNew<Number>(width));
info->Set(NanNew<String>("height"), NanNew<Number>(height));
if (baton->bufferOutLength > 0) {
// Buffer
argv[1] = NanNewBufferHandle(static_cast<char*>(baton->bufferOut), baton->bufferOutLength);
g_free(baton->bufferOut);
argv[2] = info;
} else {
// File
argv[1] = info;
}
}
delete baton;
// Decrement processing task counter
g_atomic_int_dec_and_test(&counter_process);
// Return to JavaScript
callback->Call(3, argv);
}
private:
ResizeBaton* baton;
/*
Calculate the angle of rotation and need-to-flip for the output image.
In order of priority:
1. Use explicitly requested angle (supports 90, 180, 270)
2. Use input image EXIF Orientation header - supports mirroring
3. Otherwise default to zero, i.e. no rotation
*/
std::tuple<Angle, bool>
CalculateRotationAndFlip(int const angle, VipsImage const *input) {
Angle rotate = ANGLE_0;
bool flip = FALSE;
if (angle == -1) {
const char *exif;
if (
vips_image_get_typeof(input, "exif-ifd0-Orientation") != 0 &&
!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)
) {
if (exif[0] == 0x36) { // "6"
rotate = ANGLE_90;
} else if (exif[0] == 0x33) { // "3"
rotate = ANGLE_180;
} else if (exif[0] == 0x38) { // "8"
rotate = ANGLE_270;
} else if (exif[0] == 0x32) { // "2" (flip 1)
flip = TRUE;
} else if (exif[0] == 0x37) { // "7" (flip 6)
rotate = ANGLE_90;
flip = TRUE;
} else if (exif[0] == 0x34) { // "4" (flip 3)
rotate = ANGLE_180;
flip = TRUE;
} else if (exif[0] == 0x35) { // "5" (flip 8)
rotate = ANGLE_270;
flip = TRUE;
}
}
} else {
if (angle == 90) {
rotate = ANGLE_90;
} else if (angle == 180) {
rotate = ANGLE_180;
} else if (angle == 270) {
rotate = ANGLE_270;
}
}
return std::make_tuple(rotate, flip);
}
/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given gravity.
*/
std::tuple<int, int>
CalculateCrop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const gravity) {
int left = 0;
int top = 0;
switch (gravity) {
case 1: // North
left = (inWidth - outWidth + 1) / 2;
break;
case 2: // East
left = inWidth - outWidth;
top = (inHeight - outHeight + 1) / 2;
break;
case 3: // South
left = (inWidth - outWidth + 1) / 2;
top = inHeight - outHeight;
break;
case 4: // West
top = (inHeight - outHeight + 1) / 2;
break;
default: // Centre
left = (inWidth - outWidth + 1) / 2;
top = (inHeight - outHeight + 1) / 2;
}
return std::make_tuple(left, top);
}
/*
Copy then clear the error message.
Unref all transitional images on the hook.
Clear all thread-local data.
*/
void Error(ResizeBaton *baton, VipsObject *hook) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(hook);
vips_thread_shutdown();
}
};
/*
resize(options, output, callback)
*/
NAN_METHOD(resize) {
NanScope();
// V8 objects are converted to non-V8 types held in the baton struct
ResizeBaton *baton = new ResizeBaton;
Local<Object> options = args[0]->ToObject();
// Input filename
baton->fileIn = *String::Utf8Value(options->Get(NanNew<String>("fileIn"))->ToString());
baton->accessMethod = 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->bufferInLength = node::Buffer::Length(buffer);
baton->bufferIn = node::Buffer::Data(buffer);
}
// ICC profile to use when input CMYK image has no embedded profile
baton->iccProfileCmyk = *String::Utf8Value(options->Get(NanNew<String>("iccProfileCmyk"))->ToString());
// 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 option
Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
if (canvas->Equals(NanNew<String>("c"))) {
baton->canvas = CROP;
} else if (canvas->Equals(NanNew<String>("m"))) {
baton->canvas = MAX;
} else if (canvas->Equals(NanNew<String>("e"))) {
baton->canvas = EMBED;
}
// 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->withoutEnlargement = options->Get(NanNew<String>("withoutEnlargement"))->BooleanValue();
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();
baton->flip = options->Get(NanNew<String>("flip"))->BooleanValue();
baton->flop = options->Get(NanNew<String>("flop"))->BooleanValue();
// Output options
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
baton->withMetadata = options->Get(NanNew<String>("withMetadata"))->BooleanValue();
// Output filename or __format for Buffer
baton->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
// Join queue for worker thread
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
NanAsyncQueueWorker(new ResizeWorker(callback, baton));
// Increment queued task counter
g_atomic_int_inc(&counter_queue);
NanReturnUndefined();
}

8
src/resize.h Executable file
View File

@@ -0,0 +1,8 @@
#ifndef SHARP_RESIZE_H
#define SHARP_RESIZE_H
#include "nan.h"
NAN_METHOD(resize);
#endif

File diff suppressed because it is too large Load Diff

64
src/utilities.cc Executable file
View File

@@ -0,0 +1,64 @@
#include <node.h>
#include <vips/vips.h>
#include "nan.h"
#include "common.h"
#include "utilities.h"
using namespace v8;
/*
Get and set cache memory and item limits
*/
NAN_METHOD(cache) {
NanScope();
// Set cache memory limit
if (args[0]->IsInt32()) {
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
if (args[1]->IsInt32()) {
vips_cache_set_max(args[1]->Int32Value());
}
// Get cache statistics
Local<Object> cache = NanNew<Object>();
cache->Set(NanNew<String>("current"), NanNew<Number>(vips_tracked_get_mem() / 1048576));
cache->Set(NanNew<String>("high"), NanNew<Number>(vips_tracked_get_mem_highwater() / 1048576));
cache->Set(NanNew<String>("memory"), NanNew<Number>(vips_cache_get_max_mem() / 1048576));
cache->Set(NanNew<String>("items"), NanNew<Number>(vips_cache_get_max()));
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)
*/
NAN_METHOD(counters) {
NanScope();
Local<Object> counters = NanNew<Object>();
counters->Set(NanNew<String>("queue"), NanNew<Number>(counter_queue));
counters->Set(NanNew<String>("process"), NanNew<Number>(counter_process));
NanReturnValue(counters);
}

10
src/utilities.h Executable file
View File

@@ -0,0 +1,10 @@
#ifndef SHARP_UTILITIES_H
#define SHARP_UTILITIES_H
#include "nan.h"
NAN_METHOD(cache);
NAN_METHOD(concurrency);
NAN_METHOD(counters);
#endif

21
test/bench/package.json Executable file
View File

@@ -0,0 +1,21 @@
{
"name": "sharp-benchmark",
"version": "0.0.1",
"private": true,
"author": "Lovell Fuller <npm@lovell.info>",
"description": "Benchmark and performance tests for sharp",
"scripts": {
"test": "node perf && node random && node parallel"
},
"devDependencies": {
"imagemagick": "^0.1.3",
"imagemagick-native": "^1.4.0",
"gm": "^1.16.0",
"async": "^0.9.0",
"benchmark": "^1.0.0"
},
"license": "Apache 2.0",
"engines": {
"node": ">=0.10"
}
}

View File

@@ -1,12 +1,11 @@
/*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 sharp = require('../../index');
var fixtures = require('../fixtures');
var width = 720;
var height = 480;
@@ -21,7 +20,7 @@ async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
async.times(parallelism,
function(id, callback) {
/*jslint unused: false */
sharp(inputJpg).resize(width, height).toBuffer(function(err, buffer) {
sharp(fixtures.inputJpg).resize(width, height).toBuffer(function(err, buffer) {
buffer = null;
callback(err, new Date().getTime() - start);
});

View File

@@ -1,31 +1,17 @@
/*jslint node: true */
'use strict';
var sharp = require('../index');
var fs = require('fs');
var path = require('path');
var imagemagick = require('imagemagick');
var imagemagickNative = require('imagemagick-native');
var gm = require('gm');
var async = require('async');
var assert = require('assert');
var Benchmark = require('benchmark');
var fixturesPath = path.join(__dirname, 'fixtures');
var imagemagick = require('imagemagick');
var imagemagickNative = require('imagemagick-native');
var gm = require('gm');
var sharp = require('../../index');
var inputJpg = path.join(fixturesPath, '2569067123_aca715a2ee_o.jpg'); // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = path.join(fixturesPath, 'output.jpg');
var inputPng = path.join(fixturesPath, '50020484-00001.png'); // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
var outputPng = path.join(fixturesPath, 'output.png');
var inputWebp = path.join(fixturesPath, '4.webp'); // http://www.gstatic.com/webp/gallery/4.webp
var outputWebp = path.join(fixturesPath, 'output.webp');
var inputTiff = path.join(fixturesPath, 'G31D.TIF'); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
var outputTiff = path.join(fixturesPath, 'output.tiff');
var inputGif = path.join(fixturesPath, 'Crash_test.gif'); // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
var fixtures = require('../fixtures');
var width = 720;
var height = 480;
@@ -35,13 +21,13 @@ sharp.cache(0);
async.series({
jpeg: function(callback) {
var inputJpgBuffer = fs.readFileSync(inputJpg);
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
(new Benchmark.Suite('jpeg')).add('imagemagick-file-file', {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: inputJpg,
dstPath: outputJpg,
srcPath: fixtures.inputJpg,
dstPath: fixtures.outputJpg,
quality: 0.8,
width: width,
height: height
@@ -61,14 +47,14 @@ async.series({
quality: 80,
width: width,
height: height,
format: 'JPEG'
format: 'JPEG'
});
deferred.resolve();
deferred.resolve();
}
}).add('gm-buffer-file', {
defer: true,
fn: function(deferred) {
gm(inputJpgBuffer).resize(width, height).quality(80).write(outputJpg, function (err) {
gm(inputJpgBuffer).resize(width, height).quality(80).write(fixtures.outputJpg, function (err) {
if (err) {
throw err;
} else {
@@ -91,7 +77,7 @@ async.series({
}).add('gm-file-file', {
defer: true,
fn: function(deferred) {
gm(inputJpg).resize(width, height).quality(80).write(outputJpg, function (err) {
gm(fixtures.inputJpg).resize(width, height).quality(80).write(fixtures.outputJpg, function (err) {
if (err) {
throw err;
} else {
@@ -102,7 +88,7 @@ async.series({
}).add('gm-file-buffer', {
defer: true,
fn: function(deferred) {
gm(inputJpg).resize(width, height).quality(80).toBuffer(function (err, buffer) {
gm(fixtures.inputJpg).resize(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
@@ -114,7 +100,7 @@ async.series({
}).add('sharp-buffer-file', {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).toFile(outputJpg, function(err) {
sharp(inputJpgBuffer).resize(width, height).toFile(fixtures.outputJpg, function(err) {
if (err) {
throw err;
} else {
@@ -137,7 +123,7 @@ async.series({
}).add('sharp-file-file', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).toFile(outputJpg, function(err) {
sharp(fixtures.inputJpg).resize(width, height).toFile(fixtures.outputJpg, function(err) {
if (err) {
throw err;
} else {
@@ -148,8 +134,8 @@ async.series({
}).add('sharp-stream-stream', {
defer: true,
fn: function(deferred) {
var readable = fs.createReadStream(inputJpg);
var writable = fs.createWriteStream(outputJpg);
var readable = fs.createReadStream(fixtures.inputJpg);
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
deferred.resolve();
});
@@ -159,7 +145,7 @@ async.series({
}).add('sharp-file-buffer', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).toBuffer(function(err, buffer) {
sharp(fixtures.inputJpg).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -168,18 +154,18 @@ async.series({
}
});
}
}).add('sharp-file-buffer-promise', {
}).add('sharp-promise', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).toBuffer().then(function(buffer) {
sharp(inputJpgBuffer).resize(width, height).toBuffer().then(function(buffer) {
assert.notStrictEqual(null, buffer);
deferred.resolve();
});
}
}).add('sharp-file-buffer-sharpen', {
}).add('sharp-sharpen', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).sharpen().toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -188,10 +174,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-nearest-neighbour', {
}).add('sharp-nearest-neighbour', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.nearest).toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.nearest).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -200,10 +186,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-bicubic', {
}).add('sharp-bicubic', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.bicubic).toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.bicubic).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -212,10 +198,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-nohalo', {
}).add('sharp-nohalo', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.nohalo).toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.nohalo).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -224,10 +210,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-locallyBoundedBicubic', {
}).add('sharp-locallyBoundedBicubic', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.locallyBoundedBicubic).toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.locallyBoundedBicubic).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -236,10 +222,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-vertexSplitQuadraticBasisSpline', {
}).add('sharp-vertexSplitQuadraticBasisSpline', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline).toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -248,10 +234,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-gamma', {
}).add('sharp-gamma', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).gamma().toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).gamma().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -260,10 +246,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-greyscale', {
}).add('sharp-greyscale', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).greyscale().toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).greyscale().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -272,10 +258,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-greyscale-gamma', {
}).add('sharp-greyscale-gamma', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).gamma().greyscale().toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).gamma().greyscale().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -284,10 +270,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-progressive', {
}).add('sharp-progressive', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).progressive().toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).progressive().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -296,10 +282,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-rotate', {
}).add('sharp-rotate', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).rotate(90).resize(width, height).toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).rotate(90).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -308,10 +294,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-sequentialRead', {
}).add('sharp-sequentialRead', {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).sequentialRead().toBuffer(function(err, buffer) {
sharp(inputJpgBuffer).resize(width, height).sequentialRead().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -327,13 +313,13 @@ async.series({
}).run();
},
png: function(callback) {
var inputPngBuffer = fs.readFileSync(inputPng);
var inputPngBuffer = fs.readFileSync(fixtures.inputPng);
(new Benchmark.Suite('png')).add('imagemagick-file-file', {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: inputPng,
dstPath: outputPng,
srcPath: fixtures.inputPng,
dstPath: fixtures.outputPng,
width: width,
height: height
}, function(err) {
@@ -351,14 +337,14 @@ async.series({
srcData: inputPngBuffer,
width: width,
height: height,
format: 'PNG'
format: 'PNG'
});
deferred.resolve();
deferred.resolve();
}
}).add('gm-file-file', {
defer: true,
fn: function(deferred) {
gm(inputPng).resize(width, height).write(outputPng, function (err) {
gm(fixtures.inputPng).resize(width, height).write(fixtures.outputPng, function (err) {
if (err) {
throw err;
} else {
@@ -369,7 +355,7 @@ async.series({
}).add('gm-file-buffer', {
defer: true,
fn: function(deferred) {
gm(inputPng).resize(width, height).quality(80).toBuffer(function (err, buffer) {
gm(fixtures.inputPng).resize(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
@@ -381,7 +367,7 @@ async.series({
}).add('sharp-buffer-file', {
defer: true,
fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).toFile(outputPng, function(err) {
sharp(inputPngBuffer).resize(width, height).toFile(fixtures.outputPng, function(err) {
if (err) {
throw err;
} else {
@@ -404,7 +390,7 @@ async.series({
}).add('sharp-file-file', {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).toFile(outputPng, function(err) {
sharp(fixtures.inputPng).resize(width, height).toFile(fixtures.outputPng, function(err) {
if (err) {
throw err;
} else {
@@ -415,7 +401,7 @@ async.series({
}).add('sharp-file-buffer', {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).toBuffer(function(err, buffer) {
sharp(fixtures.inputPng).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -424,22 +410,10 @@ async.series({
}
});
}
}).add('sharp-file-buffer-sharpen', {
}).add('sharp-progressive', {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-file-buffer-progressive', {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).progressive().toBuffer(function(err, buffer) {
sharp(inputPngBuffer).resize(width, height).progressive().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -455,11 +429,11 @@ async.series({
}).run();
},
webp: function(callback) {
var inputWebpBuffer = fs.readFileSync(inputWebp);
var inputWebPBuffer = fs.readFileSync(fixtures.inputWebP);
(new Benchmark.Suite('webp')).add('sharp-buffer-file', {
defer: true,
fn: function(deferred) {
sharp(inputWebpBuffer).resize(width, height).toFile(outputWebp, function(err) {
sharp(inputWebPBuffer).resize(width, height).toFile(fixtures.outputWebP, function(err) {
if (err) {
throw err;
} else {
@@ -470,7 +444,7 @@ async.series({
}).add('sharp-buffer-buffer', {
defer: true,
fn: function(deferred) {
sharp(inputWebpBuffer).resize(width, height).toBuffer(function(err, buffer) {
sharp(inputWebPBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -482,7 +456,7 @@ async.series({
}).add('sharp-file-file', {
defer: true,
fn: function(deferred) {
sharp(inputWebp).resize(width, height).toFile(outputWebp, function(err) {
sharp(fixtures.inputWebP).resize(width, height).toFile(fixtures.outputWebP, function(err) {
if (err) {
throw err;
} else {
@@ -493,19 +467,7 @@ async.series({
}).add('sharp-file-buffer', {
defer: true,
fn: function(deferred) {
sharp(inputWebp).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add('sharp-file-buffer-sharpen', {
defer: true,
fn: function(deferred) {
sharp(inputWebp).resize(width, height).sharpen().toBuffer(function(err, buffer) {
sharp(fixtures.inputWebp).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
@@ -519,75 +481,6 @@ async.series({
}).on('complete', function() {
callback(null, this.filter('fastest').pluck('name'));
}).run();
},
tiff: function(callback) {
(new Benchmark.Suite('tiff')).add('sharp-file-file', {
defer: true,
fn: function(deferred) {
sharp(inputTiff).resize(width, height).toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-file-file-sharpen', {
defer: true,
fn: function(deferred) {
sharp(inputTiff).resize(width, height).sharpen().toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).on('cycle', function(event) {
console.log('tiff ' + String(event.target));
}).on('complete', function() {
callback(null, this.filter('fastest').pluck('name'));
}).run();
},
gif: function(callback) {
(new Benchmark.Suite('gif')).add('sharp-file-file', {
defer: true,
fn: function(deferred) {
sharp(inputGif).resize(width, height).toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-file-file-sharpen', {
defer: true,
fn: function(deferred) {
sharp(inputGif).resize(width, height).sharpen().toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add('sharp-file-file-sequentialRead', {
defer: true,
fn: function(deferred) {
sharp(inputGif).sequentialRead().resize(width, height).toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).on('cycle', function(event) {
console.log('gif ' + String(event.target));
}).on('complete', function() {
callback(null, this.filter('fastest').pluck('name'));
}).run();
}
}, function(err, results) {
assert(!err, err);

65
test/bench/random.js Executable file
View File

@@ -0,0 +1,65 @@
'use strict';
var imagemagick = require('imagemagick');
var gm = require('gm');
var assert = require('assert');
var Benchmark = require('benchmark');
var sharp = require('../../index');
var fixtures = require('../fixtures');
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: fixtures.inputJpg,
dstPath: fixtures.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(fixtures.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(fixtures.inputJpg).resize(randomDimension(), randomDimension()).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on('cycle', function(event) {
console.log(String(event.target));
}).on('complete', function() {
var winner = this.filter('fastest').pluck('name');
assert.strictEqual('sharp', String(winner), 'sharp was slower than ' + winner);
console.dir(sharp.cache());
}).run();

View File

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 813 KiB

View File

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

View File

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

Before

Width:  |  Height:  |  Size: 714 KiB

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 278 KiB

BIN
test/fixtures/Landscape_5.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

32
test/fixtures/index.js vendored Executable file
View File

@@ -0,0 +1,32 @@
'use strict';
var path = require('path');
var getPath = function(filename) {
return path.join(__dirname, filename);
};
module.exports = {
inputJpg: getPath('2569067123_aca715a2ee_o.jpg'), // http://www.flickr.com/photos/grizdave/2569067123/
inputJpgWithExif: getPath('Landscape_8.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
inputJpgWithExifMirroring: getPath('Landscape_5.jpg'), // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg
inputJpgWithGammaHoliness: getPath('gamma_dalai_lama_gray.jpg'), // http://www.4p8.com/eric.brasseur/gamma.html
inputJpgWithCmykProfile: getPath('Channel_digital_image_CMYK_color.jpg'), // http://en.wikipedia.org/wiki/File:Channel_digital_image_CMYK_color.jpg
inputJpgWithCmykNoProfile: getPath('Channel_digital_image_CMYK_color_no_profile.jpg'),
inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
inputPngWithTransparency: getPath('blackbug.png'), // public domain
inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
inputTiff: getPath('G31D.TIF'), // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
inputGif: getPath('Crash_test.gif'), // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
outputJpg: getPath('output.jpg'),
outputPng: getPath('output.png'),
outputWebP: getPath('output.webp'),
outputZoinks: getPath('output.zoinks'), // an 'unknown' file extension
path: getPath // allows tests to write files to fixtures directory (for testing with human eyes)
};

View File

@@ -3,5 +3,6 @@ if ! type valgrind >/dev/null; then
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
curl -O https://raw.githubusercontent.com/jcupitt/libvips/master/libvips.supp test/leak/libvips.supp
cd ../../
G_SLICE=always-malloc G_DEBUG=gc-friendly valgrind --suppressions=test/leak/libvips.supp --suppressions=test/leak/sharp.supp --leak-check=full --show-leak-kinds=definite,indirect,possible --num-callers=20 npm test

0
tests/sharp.supp → test/leak/sharp.supp Normal file → Executable file
View File

62
test/unit/alpha.js Executable file
View File

@@ -0,0 +1,62 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Alpha transparency', function() {
it('Flatten to black', function(done) {
sharp(fixtures.inputPngWithTransparency)
.flatten()
.resize(400, 300)
.toFile(fixtures.path('output.flatten-black.jpg'), done);
});
it('Flatten to RGB orange', function(done) {
sharp(fixtures.inputPngWithTransparency)
.flatten()
.background({r: 255, g: 102, b: 0})
.resize(400, 300)
.toFile(fixtures.path('output.flatten-rgb-orange.jpg'), done);
});
it('Flatten to CSS/hex orange', function(done) {
sharp(fixtures.inputPngWithTransparency)
.flatten()
.background('#ff6600')
.resize(400, 300)
.toFile(fixtures.path('output.flatten-hex-orange.jpg'), done);
});
it('Do not flatten', function(done) {
sharp(fixtures.inputPngWithTransparency)
.flatten(false)
.toBuffer(function(err, data) {
if (err) throw err;
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual(4, metadata.channels);
done();
});
});
});
it('Ignored for JPEG', function(done) {
sharp(fixtures.inputJpg)
.background('#ff0000')
.flatten()
.toBuffer(function(err, data) {
if (err) throw err;
sharp(data).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(3, metadata.channels);
done();
});
});
});
});

81
test/unit/colourspace.js Executable file
View File

@@ -0,0 +1,81 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Colour space conversion', function() {
it('To greyscale', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.greyscale()
.toFile(fixtures.path('output.greyscale-gamma-0.0.jpg'), done);
});
it('To greyscale with gamma correction', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.gamma()
.greyscale()
.toFile(fixtures.path('output.greyscale-gamma-2.2.jpg'), done);
});
it('Not to greyscale', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.greyscale(false)
.toFile(fixtures.path('output.greyscale-not.jpg'), done);
});
it('From 1-bit TIFF to sRGB WebP [slow]', function(done) {
sharp(fixtures.inputTiff)
.webp()
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('webp', info.format);
done();
});
});
it('From CMYK to sRGB', function(done) {
sharp(fixtures.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();
});
});
it('From CMYK to sRGB with white background, not yellow', function(done) {
sharp(fixtures.inputJpgWithCmykProfile)
.resize(320, 240)
.background('white')
.embed()
.toFile(fixtures.path('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();
});
});
it('From profile-less CMYK to sRGB', function(done) {
sharp(fixtures.inputJpgWithCmykNoProfile)
.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();
});
});
});

93
test/unit/crop.js Executable file
View File

@@ -0,0 +1,93 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Crop gravities', function() {
it('North', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 80)
.crop(sharp.gravity.north)
.toFile(fixtures.path('output.gravity-north.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
it('East', function(done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop(sharp.gravity.east)
.toFile(fixtures.path('output.gravity-east.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('South', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 80)
.crop(sharp.gravity.south)
.toFile(fixtures.path('output.gravity-south.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
it('West', function(done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop(sharp.gravity.west)
.toFile(fixtures.path('output.gravity-west.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Center', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 80)
.crop(sharp.gravity.center)
.toFile(fixtures.path('output.gravity-center.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
it('Centre', function(done) {
sharp(fixtures.inputJpg)
.resize(80, 320)
.crop(sharp.gravity.centre)
.toFile(fixtures.path('output.gravity-centre.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
});
it('Invalid', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).crop(5);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
});

49
test/unit/embed.js Executable file
View File

@@ -0,0 +1,49 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Embed', function() {
it('JPEG within PNG, no alpha channel', function(done) {
sharp(fixtures.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();
});
});
});
it('JPEG within WebP, to include alpha channel', function(done) {
sharp(fixtures.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();
});
});
});
});

93
test/unit/extract.js Executable file
View File

@@ -0,0 +1,93 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Partial image extraction', function() {
it('JPEG', function(done) {
sharp(fixtures.inputJpg)
.extract(2, 2, 20, 20)
.toFile(fixtures.path('output.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(20, info.width);
assert.strictEqual(20, info.height);
done();
});
});
it('PNG', function(done) {
sharp(fixtures.inputPng)
.extract(300, 200, 400, 200)
.toFile(fixtures.path('output.extract.png'), function(err, info) {
if (err) throw err;
assert.strictEqual(400, info.width);
assert.strictEqual(200, info.height);
done();
});
});
it('WebP', function(done) {
sharp(fixtures.inputWebP)
.extract(50, 100, 125, 200)
.toFile(fixtures.path('output.extract.webp'), function(err, info) {
if (err) throw err;
assert.strictEqual(125, info.width);
assert.strictEqual(200, info.height);
done();
});
});
it('TIFF', function(done) {
sharp(fixtures.inputTiff)
.extract(63, 34, 341, 529)
.toFile(fixtures.path('output.extract.tiff'), function(err, info) {
if (err) throw err;
assert.strictEqual(341, info.width);
assert.strictEqual(529, info.height);
done();
});
});
it('Before resize', function(done) {
sharp(fixtures.inputJpg)
.extract(10, 10, 10, 500, 500)
.resize(100, 100)
.toFile(fixtures.path('output.extract.resize.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
it('After resize and crop', function(done) {
sharp(fixtures.inputJpg)
.resize(500, 500)
.crop(sharp.gravity.north)
.extract(10, 10, 100, 100)
.toFile(fixtures.path('output.resize.crop.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
it('Before and after resize and crop', function(done) {
sharp(fixtures.inputJpg)
.extract(0, 0, 700, 700)
.resize(500, 500)
.crop(sharp.gravity.north)
.extract(10, 10, 100, 100)
.toFile(fixtures.path('output.extract.resize.crop.extract.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(100, info.width);
assert.strictEqual(100, info.height);
done();
});
});
});

41
test/unit/gamma.js Executable file
View File

@@ -0,0 +1,41 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Gamma correction', function() {
it('value of 0.0 (disabled)', function(done) {
sharp(fixtures.inputJpgWithGammaHoliness)
.resize(129, 111)
.toFile(fixtures.path('output.gamma-0.0.jpg'), done);
});
it('value of 2.2 (default)', function(done) {
sharp(fixtures.inputJpgWithGammaHoliness)
.resize(129, 111)
.gamma()
.toFile(fixtures.path('output.gamma-2.2.jpg'), done);
});
it('value of 3.0', function(done) {
sharp(fixtures.inputJpgWithGammaHoliness)
.resize(129, 111)
.gamma(3)
.toFile(fixtures.path('output.gamma-3.0.jpg'), done);
});
it('invalid value', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpgWithGammaHoliness).gamma(4);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
});

94
test/unit/interpolation.js Executable file
View File

@@ -0,0 +1,94 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Interpolation', function() {
it('nearest neighbour', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.nearest)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('bilinear', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.bilinear)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('bicubic', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.bicubic)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('nohalo', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.nohalo)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('locally bounded bicubic (LBB)', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.locallyBoundedBicubic)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('vertex split quadratic basis spline (VSQBS)', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
});

370
test/unit/io.js Executable file
View File

@@ -0,0 +1,370 @@
'use strict';
var fs = require('fs');
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Input/output', function() {
it('Read from File and write to Stream', function(done) {
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
sharp(fixtures.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(fixtures.outputJpg);
done();
});
});
sharp(fixtures.inputJpg).resize(320, 240).pipe(writable);
});
it('Read from Buffer and write to Stream', function(done) {
var inputJpgBuffer = fs.readFileSync(fixtures.inputJpg);
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
sharp(fixtures.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(fixtures.outputJpg);
done();
});
});
sharp(inputJpgBuffer).resize(320, 240).pipe(writable);
});
it('Read from Stream and write to File', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp().resize(320, 240).toFile(fixtures.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(fixtures.outputJpg);
done();
});
readable.pipe(pipeline);
});
it('Read from Stream and write to Buffer', function(done) {
var readable = fs.createReadStream(fixtures.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();
});
readable.pipe(pipeline);
});
it('Read from Stream and write to Buffer via Promise', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp().resize(1, 1);
pipeline.toBuffer().then(function(data) {
assert.strictEqual(true, data.length > 0);
done();
}).catch(function(err) {
throw err;
});
readable.pipe(pipeline);
});
it('Read from Stream and write to Stream', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var writable = fs.createWriteStream(fixtures.outputJpg);
writable.on('finish', function() {
sharp(fixtures.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(fixtures.outputJpg);
done();
});
});
var pipeline = sharp().resize(320, 240);
readable.pipe(pipeline).pipe(writable);
});
it('Handle Stream to Stream error ', 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(fixtures.outputJpg);
done();
});
var readableButNotAnImage = fs.createReadStream(__filename);
var writable = fs.createWriteStream(fixtures.outputJpg);
readableButNotAnImage.pipe(pipeline).pipe(writable);
});
it('Handle File to Stream error', 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(fixtures.outputJpg);
done();
});
var writable = fs.createWriteStream(fixtures.outputJpg);
readableButNotAnImage.pipe(writable);
});
it('Sequential read', function(done) {
sharp(fixtures.inputJpg)
.sequentialRead()
.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();
});
});
it('Not sequential read', function(done) {
sharp(fixtures.inputJpg)
.sequentialRead(false)
.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();
});
});
it('Fail when output File is input File', function(done) {
sharp(fixtures.inputJpg).toFile(fixtures.inputJpg, function(err) {
assert(!!err);
done();
});
});
it('Fail when output File is input File via Promise', function(done) {
sharp(fixtures.inputJpg).toFile(fixtures.inputJpg).then(function(data) {
assert(false);
done();
}).catch(function(err) {
assert(!!err);
done();
});
});
it('Fail when output File is empty', function(done) {
sharp(fixtures.inputJpg).toFile('', function(err) {
assert(!!err);
done();
});
});
it('Fail when output File is empty via Promise', function(done) {
sharp(fixtures.inputJpg).toFile('').then(function(data) {
assert(false);
done();
}).catch(function(err) {
assert(!!err);
done();
});
});
it('Fail when input is empty Buffer', function(done) {
var failed = true;
try {
sharp(new Buffer(0));
failed = false;
} catch (err) {
assert(err instanceof Error);
}
assert(failed);
done();
});
it('Fail when input is invalid Buffer', function(done) {
var failed = true;
try {
sharp(new Buffer([0x1, 0x2, 0x3, 0x4]));
failed = false;
} catch (err) {
assert(err instanceof Error);
}
assert(failed);
done();
});
it('Promises/A+', function(done) {
sharp(fixtures.inputJpg).resize(320, 240).toBuffer().then(function(data) {
sharp(data).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
}).catch(function(err) {
throw err;
});
});
it('JPEG quality', function(done) {
sharp(fixtures.inputJpg).resize(320, 240).quality(70).toBuffer(function(err, buffer70) {
if (err) throw err;
sharp(fixtures.inputJpg).resize(320, 240).toBuffer(function(err, buffer80) {
if (err) throw err;
sharp(fixtures.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();
});
});
});
});
it('Invalid quality', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).quality(-1);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Progressive image', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.png()
.progressive(false)
.toBuffer(function(err, nonProgressive, info) {
if (err) throw err;
assert.strictEqual(true, nonProgressive.length > 0);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(nonProgressive)
.progressive()
.toBuffer(function(err, progressive, info) {
if (err) throw err;
assert.strictEqual(true, progressive.length > 0);
assert.strictEqual(true, progressive.length > nonProgressive.length);
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
});
describe('Output filename without extension uses input format', function() {
it('JPEG', function(done) {
sharp(fixtures.inputJpg).resize(320, 80).toFile(fixtures.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(fixtures.outputZoinks);
done();
});
});
it('PNG', function(done) {
sharp(fixtures.inputPng).resize(320, 80).toFile(fixtures.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(fixtures.outputZoinks);
done();
});
});
it('Transparent PNG', function(done) {
sharp(fixtures.inputPngWithTransparency).resize(320, 80).toFile(fixtures.outputZoinks, function(err, info) {
if (err) throw err;
assert.strictEqual('png', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
});
it('WebP', function(done) {
sharp(fixtures.inputWebP).resize(320, 80).toFile(fixtures.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(fixtures.outputZoinks);
done();
});
});
it('TIFF', function(done) {
sharp(fixtures.inputTiff).resize(320, 80).toFile(fixtures.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(fixtures.outputZoinks);
done();
});
});
it('Fail with GIF', function(done) {
sharp(fixtures.inputGif).resize(320, 80).toFile(fixtures.outputZoinks, function(err) {
assert(!!err);
done();
});
});
});
describe('PNG compression level', function() {
it('valid', function(done) {
var isValid = false;
try {
sharp().compressionLevel(0);
isValid = true;
} catch (e) {}
assert(isValid);
done();
});
it('invalid', function(done) {
var isValid = false;
try {
sharp().compressionLevel(-1);
isValid = true;
} catch (e) {}
assert(!isValid);
done();
});
});
});

1
test/unit/jshint.js Executable file
View File

@@ -0,0 +1 @@
require('mocha-jshint')();

188
test/unit/metadata.js Executable file
View File

@@ -0,0 +1,188 @@
'use strict';
var fs = require('fs');
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Image metadata', function() {
it('JPEG', function(done) {
sharp(fixtures.inputJpg).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.orientation);
done();
});
});
it('JPEG with EXIF', function(done) {
sharp(fixtures.inputJpgWithExif).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(450, metadata.width);
assert.strictEqual(600, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(8, metadata.orientation);
done();
});
});
it('TIFF', function(done) {
sharp(fixtures.inputTiff).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('tiff', metadata.format);
assert.strictEqual(2464, metadata.width);
assert.strictEqual(3248, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('PNG', function(done) {
sharp(fixtures.inputPng).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual(2809, metadata.width);
assert.strictEqual(2074, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('Transparent PNG', function(done) {
sharp(fixtures.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();
});
});
it('WebP', function(done) {
sharp(fixtures.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();
});
});
it('GIF via libmagick', function(done) {
sharp(fixtures.inputGif).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('magick', metadata.format);
assert.strictEqual(800, metadata.width);
assert.strictEqual(533, metadata.height);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('File in, Promise out', function(done) {
sharp(fixtures.inputJpg).metadata().then(function(metadata) {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
});
it('Stream in, Promise out', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp();
pipeline.metadata().then(function(metadata) {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
}).catch(function(err) {
throw err;
});
readable.pipe(pipeline);
});
it('Stream', function(done) {
var readable = fs.createReadStream(fixtures.inputJpg);
var pipeline = sharp().metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
readable.pipe(pipeline);
});
it('Resize to half width using metadata', function(done) {
var image = sharp(fixtures.inputJpg);
image.metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
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);
assert.strictEqual(1362, info.width);
assert.strictEqual(1112, info.height);
done();
});
});
});
it('Keep EXIF metadata after a resize', function(done) {
sharp(fixtures.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();
});
});
});
it('Remove EXIF metadata after a resize', function(done) {
sharp(fixtures.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();
});
});
});
});

184
test/unit/resize.js Executable file
View File

@@ -0,0 +1,184 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Resize dimensions', function() {
it('Exact crop', function(done) {
sharp(fixtures.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();
});
});
it('Fixed width', function(done) {
sharp(fixtures.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();
});
});
it('Fixed height', function(done) {
sharp(fixtures.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();
});
});
it('Identity transform', function(done) {
sharp(fixtures.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();
});
});
it('Upscale', function(done) {
sharp(fixtures.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();
});
});
it('Invalid width', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).resize('spoons', 240);
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('Invalid height', function(done) {
var isValid = true;
try {
sharp(fixtures.inputJpg).resize(320, 'spoons');
} catch (err) {
isValid = false;
}
assert.strictEqual(false, isValid);
done();
});
it('TIFF embed known to cause rounding errors', function(done) {
sharp(fixtures.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();
});
});
it('TIFF known to cause rounding errors', function(done) {
sharp(fixtures.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();
});
});
it('Max width or height considering ratio (landscape)', function(done) {
sharp(fixtures.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();
});
});
it('Max width or height considering ratio (portrait)', function(done) {
sharp(fixtures.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();
});
});
it('Provide only one dimension with max, should default to crop', function(done) {
sharp(fixtures.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();
});
});
it('Do not enlarge when input width is already less than output width', function(done) {
sharp(fixtures.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();
});
});
it('Do not enlarge when input height is already less than output height', function(done) {
sharp(fixtures.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();
});
});
it('Do enlarge when input width is less than output width', function(done) {
sharp(fixtures.inputJpg)
.resize(2800)
.withoutEnlargement(false)
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2800, info.width);
assert.strictEqual(2286, info.height);
done();
});
});
});

149
test/unit/rotate.js Executable file
View File

@@ -0,0 +1,149 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Rotation', function() {
it('Rotate by 90 degrees, respecting output input size', function(done) {
sharp(fixtures.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();
});
});
it('Input image has Orientation EXIF tag but do not rotate output', function(done) {
sharp(fixtures.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();
});
});
it('Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate', function(done) {
sharp(fixtures.inputJpgWithExif)
.rotate()
.resize(320)
.toFile(fixtures.path('output.exif.8.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Input image has Orientation EXIF tag value of 5 (270 degrees + flip), auto-rotate', function(done) {
sharp(fixtures.inputJpgWithExifMirroring)
.rotate()
.resize(320)
.toFile(fixtures.path('output.exif.5.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
it('Attempt to auto-rotate using image that has no EXIF', function(done) {
sharp(fixtures.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();
});
});
it('Attempt to auto-rotate image format without EXIF support', function(done) {
sharp(fixtures.inputGif)
.rotate()
.resize(320)
.jpeg()
.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(213, info.height);
done();
});
});
it('Rotate to an invalid angle, should fail', function(done) {
var fail = false;
try {
sharp(fixtures.inputJpg).rotate(1);
fail = true;
} catch (e) {}
assert(!fail);
done();
});
it('Flip - vertical', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flip()
.toFile(fixtures.path('output.flip.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Flop - horizontal', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flop()
.toFile(fixtures.path('output.flop.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Flip and flop', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flop()
.toFile(fixtures.path('output.flip.flop.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
it('Neither flip nor flop', function(done) {
sharp(fixtures.inputJpg)
.resize(320)
.flip(false)
.flop(false)
.toFile(fixtures.path('output.flip.flop.nope.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
});
});

34
test/unit/sharpen.js Executable file
View File

@@ -0,0 +1,34 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var fixtures = require('../fixtures');
describe('Sharpen', function() {
it('sharpen image is larger than non-sharpen', function(done) {
sharp(fixtures.inputJpg)
.resize(320, 240)
.sharpen(false)
.toBuffer(function(err, notSharpened, info) {
if (err) throw err;
assert.strictEqual(true, notSharpened.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
sharp(notSharpened)
.sharpen()
.toBuffer(function(err, sharpened, info) {
if (err) throw err;
assert.strictEqual(true, sharpened.length > 0);
assert.strictEqual(true, sharpened.length > notSharpened.length);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
});
});
});

53
test/unit/util.js Executable file
View File

@@ -0,0 +1,53 @@
'use strict';
var assert = require('assert');
var sharp = require('../../index');
var defaultConcurrency = sharp.concurrency();
describe('Utilities', function() {
describe('Cache', function() {
it('Can be disabled', function() {
var cache = sharp.cache(0, 0);
assert.strictEqual(0, cache.memory);
assert.strictEqual(0, cache.items);
});
it('Can be set to a maximum of 50MB and 500 items', function() {
var cache = sharp.cache(50, 500);
assert.strictEqual(50, cache.memory);
assert.strictEqual(500, cache.items);
});
it('Ignores invalid values', function() {
sharp.cache(50, 500);
var cache = sharp.cache('spoons');
assert.strictEqual(50, cache.memory);
assert.strictEqual(500, cache.items);
});
});
describe('Concurrency', function() {
it('Can be set to use 16 threads', function() {
sharp.concurrency(16);
assert.strictEqual(16, sharp.concurrency());
});
it('Can be reset to default', function() {
sharp.concurrency(0);
assert.strictEqual(defaultConcurrency, sharp.concurrency());
});
it('Ignores invalid values', function() {
sharp.concurrency(0);
sharp.concurrency('spoons');
assert.strictEqual(defaultConcurrency, sharp.concurrency());
});
});
describe('Counters', function() {
it('Have zero value at rest', function() {
var counters = sharp.counters();
assert.strictEqual(0, counters.queue);
assert.strictEqual(0, counters.process);
});
});
});

View File

@@ -1,69 +0,0 @@
/*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();

View File

@@ -1,934 +0,0 @@
/*jslint node: true */
/*jslint es5: true */
'use strict';
var sharp = require('../index');
var fs = require('fs');
var path = require('path');
var assert = require('assert');
var async = require('async');
var fixturesPath = path.join(__dirname, 'fixtures');
var inputJpg = path.join(fixturesPath, '2569067123_aca715a2ee_o.jpg'); // http://www.flickr.com/photos/grizdave/2569067123/
var 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 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 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 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();
});
},
// Resize to fixed width
function(done) {
sharp(inputJpg).resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
},
// Resize to fixed height
function(done) {
sharp(inputJpg).resize(null, 320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(391, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// Identity transform
function(done) {
sharp(inputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
},
// Upscale
function(done) {
sharp(inputJpg).resize(3000).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3000, info.width);
assert.strictEqual(2449, info.height);
done();
});
},
// 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) {
if (err) throw err;
sharp(inputJpg).resize(320, 240).toBuffer(function(err, buffer80) {
if (err) throw err;
sharp(inputJpg).resize(320, 240).quality(90).toBuffer(function(err, buffer90) {
if (err) throw err;
assert(buffer70.length < buffer80.length);
assert(buffer80.length < buffer90.length);
done();
});
});
});
},
// TIFF with dimensions known to cause rounding errors
function(done) {
sharp(inputTiff).resize(240, 320).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();
});
},
function(done) {
sharp(inputTiff).resize(240, 320).jpeg().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(240, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// Resize to max width or height considering ratio (landscape)
function(done) {
sharp(inputJpg).resize(320, 320).max().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
},
// Resize to max width or height considering ratio (portrait)
function(done) {
sharp(inputTiff).resize(320, 320).max().jpeg().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(243, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// Attempt to resize to max but only provide one dimension, so should default to crop
function(done) {
sharp(inputJpg).resize(320).max().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
},
// Attempt to output to input, should fail
function(done) {
sharp(inputJpg).toFile(inputJpg, function(err) {
assert(!!err);
done();
});
},
// Rotate by 90 degrees, respecting output input size
function(done) {
sharp(inputJpg).rotate(90).resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Input image has Orientation EXIF tag but do not rotate output
function(done) {
sharp(inputJpgWithExif).resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(426, info.height);
done();
});
},
// Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate
function(done) {
sharp(inputJpgWithExif).rotate().resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Attempt to auto-rotate using image that has no EXIF
function(done) {
sharp(inputJpg).rotate().resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
},
// Rotate to an invalid angle, should fail
function(done) {
var fail = false;
try {
sharp(inputJpg).rotate(1);
fail = true;
} catch (e) {}
assert(!fail);
done();
},
// Do not enlarge the output if the input width is already less than the output width
function(done) {
sharp(inputJpg).resize(2800).withoutEnlargement().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
},
// Do not enlarge the output if the input height is already less than the output height
function(done) {
sharp(inputJpg).resize(null, 2300).withoutEnlargement().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
},
// Promises/A+
function(done) {
sharp(inputJpg).resize(320, 240).toBuffer().then(function(data) {
sharp(data).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
}).catch(function(err) {
throw err;
});
},
// Empty Buffer, should fail
function(done) {
var fail = false;
try {
sharp(new Buffer(0));
fail = true;
} catch (e) {}
assert(!fail);
done();
},
// Check colour space conversion occurs from TIFF to WebP (this used to segfault)
function(done) {
sharp(inputTiff).webp().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(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();
});
},
// Interpolation: bilinear
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.bilinear).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Interpolation: bicubic
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.bicubic).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Interpolation: nohalo
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.nohalo).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Interpolation: locally bounded bicubic (LBB)
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.locallyBoundedBicubic).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Interpolation: vertex split quadratic basis spline (VSQBS)
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// File-Stream
function(done) {
var writable = fs.createWriteStream(outputJpg);
writable.on('finish', function() {
sharp(outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(outputJpg);
done();
});
});
sharp(inputJpg).resize(320, 240).pipe(writable);
},
// Buffer-Stream
function(done) {
var inputJpgBuffer = fs.readFileSync(inputJpg);
var writable = fs.createWriteStream(outputJpg);
writable.on('finish', function() {
sharp(outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(outputJpg);
done();
});
});
sharp(inputJpgBuffer).resize(320, 240).pipe(writable);
},
// Stream-File
function(done) {
var readable = fs.createReadStream(inputJpg);
var pipeline = sharp().resize(320, 240).toFile(outputJpg, function(err, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(outputJpg);
done();
});
readable.pipe(pipeline);
},
// Stream-Buffer
function(done) {
var readable = fs.createReadStream(inputJpg);
var pipeline = sharp().resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
readable.pipe(pipeline);
},
// Stream-Stream
function(done) {
var readable = fs.createReadStream(inputJpg);
var writable = fs.createWriteStream(outputJpg);
writable.on('finish', function() {
sharp(outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual('jpeg', info.format);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(outputJpg);
done();
});
});
var pipeline = sharp().resize(320, 240);
readable.pipe(pipeline).pipe(writable);
},
// Stream-Stream error handling
function(done) {
var pipeline = sharp().resize(320, 240);
var anErrorWasEmitted = false;
pipeline.on('error', function(err) {
anErrorWasEmitted = !!err;
}).on('end', function() {
assert(anErrorWasEmitted);
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) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
},
// Crop, gravity=east
function(done) {
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.east).toFile(path.join(fixturesPath, 'output.gravity-east.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// Crop, gravity=south
function(done) {
sharp(inputJpg).resize(320, 80).crop(sharp.gravity.south).toFile(path.join(fixturesPath, 'output.gravity-south.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
},
// Crop, gravity=west
function(done) {
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.west).toFile(path.join(fixturesPath, 'output.gravity-west.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// Crop, gravity=center
function(done) {
sharp(inputJpg).resize(320, 80).crop(sharp.gravity.center).toFile(path.join(fixturesPath, 'output.gravity-center.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
},
// Crop, gravity=centre
function(done) {
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.centre).toFile(path.join(fixturesPath, 'output.gravity-centre.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// 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) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.orientation);
done();
});
},
// Metadata - JPEG with EXIF
function(done) {
sharp(inputJpgWithExif).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(450, metadata.width);
assert.strictEqual(600, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
assert.strictEqual(8, metadata.orientation);
done();
});
},
// Metadata - TIFF
function(done) {
sharp(inputTiff).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('tiff', metadata.format);
assert.strictEqual(2464, metadata.width);
assert.strictEqual(3248, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
},
// Metadata - PNG
function(done) {
sharp(inputPng).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual(2809, metadata.width);
assert.strictEqual(2074, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
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) {
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();
});
},
// Metadata - GIF (via libmagick)
function(done) {
sharp(inputGif).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('magick', metadata.format);
assert.strictEqual(800, metadata.width);
assert.strictEqual(533, metadata.height);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
},
// Metadata - Promise
function(done) {
sharp(inputJpg).metadata().then(function(metadata) {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
},
// Metadata - Stream
function(done) {
var readable = fs.createReadStream(inputJpg);
var pipeline = sharp().metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(false, metadata.hasAlpha);
done();
});
readable.pipe(pipeline);
},
// Get metadata then resize to half width
function(done) {
var image = sharp(inputJpg);
image.metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
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);
assert.strictEqual(1362, info.width);
assert.strictEqual(1112, info.height);
done();
});
});
},
// Gamma correction
function(done) {
sharp(inputJpgWithGammaHoliness).resize(129, 111).toFile(path.join(fixturesPath, 'output.gamma-0.0.jpg'), function(err) {
if (err) throw err;
done();
});
},
function(done) {
sharp(inputJpgWithGammaHoliness).resize(129, 111).gamma().toFile(path.join(fixturesPath, 'output.gamma-2.2.jpg'), function(err) {
if (err) throw err;
done();
});
},
function(done) {
sharp(inputJpgWithGammaHoliness).resize(129, 111).gamma(3).toFile(path.join(fixturesPath, 'output.gamma-3.0.jpg'), function(err) {
if (err) throw err;
done();
});
},
// 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();
}
]);