Compare commits

..

60 Commits

Author SHA1 Message Date
Lovell Fuller
8acb0ed5d0 Enable libvips C++ interpolators in Travis CI 2014-06-18 21:08:09 +01:00
Lovell Fuller
430e04d894 Update (and reduce) dependencies ahead of v0.5.1 2014-06-17 20:24:22 +01:00
Lovell Fuller
012edb4379 Support existing path to pkg-config lib for Heroku #57 2014-06-17 19:55:41 +01:00
Lovell Fuller
11ead360a9 Add Promises benchmark 2014-06-13 19:20:39 +01:00
Lovell Fuller
84a059d7e3 Rotate should throw Error, not String.
Minor JSLint and whitespace fixes.
2014-06-13 18:57:54 +01:00
Lovell Fuller
b1b070ae5c Revert "use native Promise if available"
This reverts commit 261a90c8a2.
2014-06-13 18:46:36 +01:00
Lovell Fuller
4eb910fec9 Add support for bicubic and Nohalo interpolation #41 2014-06-08 20:46:03 +01:00
Lovell Fuller
f0a9d82bf7 Always convert to sRGB colourspace to prevent libwebp segfault #58 2014-06-07 22:49:15 +01:00
Lovell Fuller
6d20a1ca81 Simplify gyp to support non-standard brew installs #56 2014-06-05 22:19:02 +01:00
Lovell Fuller
eca2787213 Add @jonathanong to contributors - thank you!
Bump versions
2014-06-05 22:18:47 +01:00
Lovell Fuller
8d146accf3 Correct docs - callback is optional for png 2014-06-05 22:00:39 +01:00
Lovell Fuller
b635d015cd Merge pull request #55 from jonathanong/native-promises
use native Promise if available
2014-06-04 20:48:17 +01:00
Jonathan Ong
261a90c8a2 use native Promise if available 2014-06-04 09:03:37 -07:00
Lovell Fuller
4ae22b3425 Merge pull request #51 from jonathanong/deprecate-write
show deprecation warning for .write()
2014-06-04 10:43:59 +01:00
Lovell Fuller
c9aa9c7723 Merge pull request #49 from jonathanong/strings-are-not-errors
fix error types, support promises on convenience methods
2014-06-04 10:35:06 +01:00
Jonathan Ong
5e0b5969da show deprecation warning for .write() 2014-06-03 20:53:37 -07:00
Jonathan Ong
ae6d5e69b1 fix error types, support promises on convenience methods
closes #48
2014-06-03 20:41:15 -07:00
Lovell Fuller
5ccc2bca97 Warn about liborc 0.4.19 buffer overflows #21 2014-06-02 22:18:00 +01:00
Lovell Fuller
46b701c85c Fail fast when input Buffer is empty #37 2014-06-02 21:11:25 +01:00
Lovell Fuller
7319533969 Add support for Promises/A+ #33 2014-06-01 11:27:30 +01:00
Lovell Fuller
9a05684302 Add withoutEnlargement option #36 2014-05-29 20:01:43 +01:00
Lovell Fuller
906311d403 Replace write() with toFile() to allow streams in the future #30 2014-05-29 19:54:43 +01:00
Lovell Fuller
4de9a2435f Correct typo 2014-05-29 16:05:53 +01:00
Lovell Fuller
a94dd2b354 Add support for image rotation including EXIF auto-orient 2014-05-29 00:48:58 +01:00
Lovell Fuller
bc3311cbad Version bump 2014-05-26 09:23:41 +01:00
Lovell Fuller
7b03eb89d7 Merge pull request #39 from pierreinglebert/update-nan
update to nan 1.1.0
2014-05-26 09:15:35 +01:00
Pierre Inglebert
15a519ebd9 update to nan 1.1.0 2014-05-26 09:51:53 +02:00
Lovell Fuller
3f8e9f6487 Merge pull request #38 from pierreinglebert/fix-max-cropping-29
fix max factor #29
2014-05-24 11:54:59 +01:00
Pierre Inglebert
39688371a8 fix max factor #29 2014-05-24 10:29:33 +02:00
Lovell Fuller
e32faac17a Prevent writing output file over input file - closes #28 2014-05-22 22:27:02 +01:00
Lovell Fuller
6c96bd0d37 Version bumps 2014-05-22 00:18:43 +01:00
Lovell Fuller
6b5f2028b7 Add support for 32 bit Linux #27 2014-05-21 23:46:10 +01:00
Lovell Fuller
276ba5228b Add usage example and further unit test for new max option
Simplify max vs crop logic
2014-05-19 21:52:47 +01:00
Lovell Fuller
ad7735a0a6 Merge pull request #18 from pierreinglebert/master
Add 'max' canvas option to specify the maximum width and/or height.
Approximately equivalent to GraphicsMagick's geometry option.
2014-05-19 21:03:04 +01:00
Pierre Inglebert
88edad3fae add max option #18 2014-05-19 20:18:26 +02:00
Lovell Fuller
308d1971d8 Recalculate residual float value for affine after shrink #26
Switch to static casts for double values

Add unit tests that previously would have failed
2014-05-18 11:39:05 +01:00
Lovell Fuller
6622045172 Merge pull request #25 from pierreinglebert/fix-progressive-typo
Fix progressive typo
2014-05-14 11:17:41 +01:00
Pierre Inglebert
f68ba8ea57 fix progressive typo 2014-05-14 08:56:12 +02:00
Lovell Fuller
2e427bb28a Remove liborc from CI config (to match README) 2014-05-13 16:47:57 +01:00
Lovell Fuller
efc7504961 Merge branch 'pierreinglebert-0.11-wip' 2014-05-13 16:45:32 +01:00
Lovell Fuller
8118613fa0 Merge branch '0.11-wip' of https://github.com/pierreinglebert/sharp into pierreinglebert-0.11-wip
Conflicts:
	package.json
2014-05-13 16:45:12 +01:00
Pierre Inglebert
eb6a221cee use master branch of image-magick-native until the 0.11 compatible is out 2014-05-13 08:03:24 +02:00
Pierre Inglebert
acdfe02502 make travis test on node 0.11 2014-05-12 21:30:44 +02:00
Lovell Fuller
2e106f8e2e Version bumps 2014-05-12 21:15:23 +02:00
Lovell Fuller
10496881f1 Add quality and compressionLevel options for output image. #24 2014-05-12 21:15:23 +02:00
Lovell Fuller
e275f6f5dd Add warning about liborc memory leaks. #21 2014-05-12 21:15:23 +02:00
Lovell Fuller
d635c297a2 Replace use of deprecated libvips conv method.
Ensure unref of mask to fix minor memory leak of ~150 bytes/image.
2014-05-12 21:15:23 +02:00
Pierre Inglebert
817c0a2a5a do not publish tests files 2014-05-12 21:15:23 +02:00
Pierre Inglebert
92fd34c627 remove unnecessary gitignore file 2014-05-12 21:15:23 +02:00
Lovell Fuller
43086cf134 Merge branch 'master' into quality-option 2014-05-11 18:26:16 +01:00
Lovell Fuller
e607bac31c Version bumps 2014-05-10 20:23:08 +01:00
Lovell Fuller
f8338e7c4f Add quality and compressionLevel options for output image. #24 2014-05-10 19:45:12 +01:00
Lovell Fuller
8322b442e0 Add warning about liborc memory leaks. #21 2014-05-10 14:39:34 +01:00
Lovell Fuller
afc51df4d8 Replace use of deprecated libvips conv method.
Ensure unref of mask to fix minor memory leak of ~150 bytes/image.
2014-05-03 22:14:21 +01:00
Lovell Fuller
e3a70c1075 Merge pull request #22 from pierreinglebert/add-npmignore
Add npmignore to minimise published package size
2014-04-24 14:09:42 +01:00
Pierre Inglebert
e3ee2b2976 do not publish tests files 2014-04-24 11:28:02 +02:00
Pierre Inglebert
cb285a6fb3 remove unnecessary gitignore file 2014-04-24 11:27:34 +02:00
Pierre Inglebert
aed3ca63b3 use NanNull, NanNew & NanSymbol for 0.11.11+ compat 2014-04-23 10:03:11 +02:00
Lovell Fuller
cbcf5e0dcc Update instructions for libvips installation on Ubuntu 2014-04-18 13:09:45 +01:00
Lovell Fuller
59f5c2d31b Update async dependency 2014-04-18 09:11:01 +01:00
11 changed files with 981 additions and 404 deletions

2
.gitignore vendored
View File

@@ -13,5 +13,3 @@ results
build
node_modules
tests/fixtures/output.*
npm-debug.log

18
.npmignore Normal file
View File

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

View File

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

153
README.md
View File

@@ -9,7 +9,7 @@
The typical use case for this high speed Node.js module is to convert large images of many formats to smaller, web-friendly JPEG, PNG and WebP images of varying dimensions.
The performance of JPEG resizing is typically 8x faster than ImageMagick and GraphicsMagick, based mainly on the number of CPU cores available. Everything remains non-blocking thanks to _libuv_.
The performance of JPEG resizing is typically 8x faster than ImageMagick and GraphicsMagick, based mainly on the number of CPU cores available. Everything remains non-blocking thanks to _libuv_ and Promises/A+ are supported.
This module supports reading and writing images of JPEG, PNG and WebP to and from both Buffer objects and the filesystem. It also supports reading images of many other types from the filesystem via libmagick++ or libgraphicsmagick++ if present.
@@ -28,6 +28,8 @@ 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).
### Install libvips on Mac OS
brew install homebrew/science/vips --with-webp --with-graphicsmagick
@@ -36,24 +38,36 @@ The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp
brew link gettext --force
### Install libvips on Ubuntu/Debian Linux
### Install libvips on Ubuntu Linux
sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev liborc-0.4-dev libxml2-dev swig
#### Ubuntu 14.x
sudo apt-get install libvips-dev
#### Ubuntu 13.x
Compiling from source is recommended:
sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev libexif-dev libxml2-dev swig
git clone https://github.com/jcupitt/libvips.git
cd libvips
git checkout 7.38
./bootstrap.sh
./configure --enable-debug=no --enable-cxx=no --without-python
./configure --enable-debug=no --enable-cxx=yes --without-python --without-orc
make
sudo make install
sudo ldconfig
Ubuntu 12.04 requires `libtiff4-dev` instead of `libtiff5-dev` and has [a bug](https://bugs.launchpad.net/ubuntu/+source/libwebp/+bug/1108731) in the libwebp package. Work around these problems by running these command first:
#### Ubuntu 12.x
Requires `libtiff4-dev` instead of `libtiff5-dev` and has [a bug](https://bugs.launchpad.net/ubuntu/+source/libwebp/+bug/1108731) in the libwebp package. Work around these problems by running these commands first:
sudo add-apt-repository ppa:lyrasis/precise-backports
sudo apt-get update
sudo apt-get install libtiff4-dev
Then follow Ubuntu 13.x instructions.
## Usage examples
```javascript
@@ -61,7 +75,7 @@ var sharp = require('sharp');
```
```javascript
sharp('input.jpg').resize(300, 200).write('output.jpg', function(err) {
sharp('input.jpg').resize(300, 200).toFile('output.jpg', function(err) {
if (err) {
throw err;
}
@@ -71,43 +85,44 @@ sharp('input.jpg').resize(300, 200).write('output.jpg', function(err) {
```
```javascript
sharp('input.jpg').resize(null, 200).progressive().toBuffer(function(err, buffer) {
sharp('input.jpg').rotate().resize(null, 200).progressive().toBuffer(function(err, outputBuffer) {
if (err) {
throw err;
}
// buffer contains progressive JPEG image data, 200 pixels high
// outputBuffer contains 200px high progressive JPEG image data, auto-rotated using EXIF Orientation tag
});
```
```javascript
sharp('input.png').resize(300).sharpen().webp(function(err, buffer) {
if (err) {
throw err;
}
// buffer contains sharpened WebP image data (converted from PNG), 300 pixels wide
sharp('input.png').rotate(180).resize(300).sharpen().quality(90).webp().then(function(outputBuffer) {
// outputBuffer contains 300px wide, upside down, sharpened, 90% quality WebP image data
});
```
```javascript
sharp(buffer).resize(200, 300).embedWhite().write('output.tiff', function(err) {
if (err) {
throw err;
}
// output.tiff is a 200 pixels wide and 300 pixels high image containing a scaled
sharp(inputBuffer).resize(200, 300).bicubicInterpolation().embedWhite().toFile('output.tiff').then(function() {
// output.tiff is a 200 pixels wide and 300 pixels high image containing a bicubic scaled
// version, embedded on a white canvas, of the image data in buffer
});
```
```javascript
sharp('input.gif').resize(200, 300).embedBlack().webp(function(err, buffer) {
sharp('input.gif').resize(200, 300).embedBlack().webp(function(err, outputBuffer) {
if (err) {
throw err;
}
// buffer contains WebP image data of a 200 pixels wide and 300 pixels high image
// outputBuffer contains WebP image data of a 200 pixels wide and 300 pixels high
// containing a scaled version, embedded on a black canvas, of input.gif
});
```
```javascript
sharp(inputBuffer).resize(200, 200).max().jpeg().then(function(outputBuffer) {
// outputBuffer contains JPEG image data no wider than 200 pixels and no higher
// than 200 pixels regardless of the inputBuffer image dimensions
});
```
## API
### sharp(input)
@@ -119,7 +134,7 @@ Constructor to which further methods are chained. `input` can be one of:
### resize(width, [height])
Scale to `width` x `height`. By default, the resized image is cropped to the exact size specified.
Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified.
`width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
@@ -129,6 +144,12 @@ Scale to `width` x `height`. By default, the resized image is cropped to the exa
Crop the resized image to the exact size specified, the default behaviour.
### max()
Preserving aspect ratio, resize the image to the maximum width or height specified.
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
### embedWhite()
Embed the resized image on a white background of the exact size specified.
@@ -137,47 +158,95 @@ Embed the resized image on a white background of the exact size specified.
Embed the resized image on a black background of the exact size specified.
### rotate([angle])
Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag. Mirroring is not supported.
`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.
### withoutEnlargement()
Do not enlarge the output image if the input image width *or* height are already less than the required dimensions.
This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification".
### sharpen()
Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
### bilinearInterpolation()
Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation) for image resizing, the default (and fastest) interpolation if none is specified.
### bicubicInterpolation()
Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation) for image resizing. This typically reduces performance by 5%.
### nohaloInterpolation()
Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/) for image resizing. This typically reduces performance by a factor of 2.
### progressive()
Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed.
### quality(quality)
The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`.
`quality` is a Number between 1 and 100.
### compressionLevel(compressionLevel)
An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`.
`compressionLevel` is a Number between -1 and 9.
### sequentialRead()
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
### write(filename, callback)
### toFile(filename, [callback])
`filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP and TIFF supported.
`callback` is called with a single argument `(err)` containing an error message, if any.
`callback`, if present, is called with a single argument `(err)` containing an error message, if any.
### jpeg(callback)
A Promises/A+ promise is returned when `callback` is not provided.
Write JPEG image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant JPEG image data.
### png(callback)
Write PNG image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant PNG image data.
### webp(callback)
Write WebP image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant WebP image data.
### toBuffer(callback)
### toBuffer([callback])
Write image data to a Buffer, the format of which will match the input image. JPEG, PNG and WebP are supported.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data.
`callback`, if present, gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data.
A Promises/A+ promise is returned when `callback` is not provided.
### jpeg([callback])
Write JPEG image data to a Buffer.
`callback`, if present, gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant JPEG image data.
A Promises/A+ promise is returned when `callback` is not provided.
### png([callback])
Write PNG image data to a Buffer.
`callback`, if present, gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant PNG image data.
A Promises/A+ promise is returned when `callback` is not provided.
### webp([callback])
Write WebP image data to a Buffer.
`callback`, if present, gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant WebP image data.
A Promises/A+ promise is returned when `callback` is not provided.
### sharp.cache([limit])

View File

@@ -2,15 +2,14 @@
'targets': [{
'target_name': 'sharp',
'sources': ['src/sharp.cc'],
'variables': {
'PKG_CONFIG_PATH': '<!(which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true):$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig'
},
'libraries': [
'<!@(PKG_CONFIG_PATH="/usr/local/Library/ENV/pkgconfig/10.8:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig" pkg-config --libs vips)'
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --libs vips)'
],
'include_dirs': [
'/usr/local/include/glib-2.0',
'/usr/local/lib/glib-2.0/include',
'/usr/include/glib-2.0',
'/usr/lib/glib-2.0/include',
'/usr/lib/x86_64-linux-gnu/glib-2.0/include',
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips glib-2.0)',
'<!(node -e "require(\'nan\')")'
],
'cflags': ['-fexceptions', '-pedantic', '-Wall', '-O3'],

369
index.js
View File

@@ -1,128 +1,241 @@
/*jslint node: true */
'use strict';
var sharp = require('./build/Release/sharp');
var Sharp = function(input) {
if (!(this instanceof Sharp)) {
return new Sharp(input);
}
this.options = {
width: -1,
height: -1,
canvas: 'c',
sharpen: false,
progressive: false,
sequentialRead: false,
output: '__jpeg'
};
if (typeof input === 'string') {
this.options.inFile = input;
} else if (typeof input ==='object' && input instanceof Buffer) {
this.options.inBuffer = input;
} else {
throw 'Unsupported input ' + typeof input;
}
return this;
};
module.exports = Sharp;
Sharp.prototype.crop = function() {
this.options.canvas = 'c';
return this;
};
Sharp.prototype.embedWhite = function() {
this.options.canvas = 'w';
return this;
};
Sharp.prototype.embedBlack = function() {
this.options.canvas = 'b';
return this;
};
Sharp.prototype.sharpen = function(sharpen) {
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
return this;
};
Sharp.prototype.progressive = function(progressive) {
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
return this;
};
Sharp.prototype.sequentialRead = function(sequentialRead) {
this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true;
return this;
};
Sharp.prototype.resize = function(width, height) {
if (!width) {
this.options.width = -1;
} else {
if (!Number.isNaN(width)) {
this.options.width = width;
} else {
throw 'Invalid width ' + width;
}
}
if (!height) {
this.options.height = -1;
} else {
if (!Number.isNaN(height)) {
this.options.height = height;
} else {
throw 'Invalid height ' + height;
}
}
return this;
};
Sharp.prototype.write = function(output, callback) {
if (!output || output.length === 0) {
throw 'Invalid output';
} else {
this._sharp(output, callback);
}
return this;
};
Sharp.prototype.toBuffer = function(callback) {
return this._sharp('__input', callback);
};
Sharp.prototype.jpeg = function(callback) {
return this._sharp('__jpeg', callback);
};
Sharp.prototype.png = function(callback) {
return this._sharp('__png', callback);
};
Sharp.prototype.webp = function(callback) {
return this._sharp('__webp', callback);
};
Sharp.prototype._sharp = function(output, callback) {
sharp.resize(
this.options.inFile,
this.options.inBuffer,
output,
this.options.width,
this.options.height,
this.options.canvas,
this.options.sharpen,
this.options.progressive,
this.options.sequentialRead,
callback
);
return this;
};
module.exports.cache = function(limit) {
if (Number.isNaN(limit)) {
limit = null;
}
return sharp.cache(limit);
};
/*jslint node: true */
'use strict';
var Promise = require('bluebird');
var sharp = require('./build/Release/sharp');
var Sharp = function(input) {
if (!(this instanceof Sharp)) {
return new Sharp(input);
}
this.options = {
width: -1,
height: -1,
canvas: 'c',
angle: 0,
withoutEnlargement: false,
sharpen: false,
interpolator: 'bilinear',
progressive: false,
sequentialRead: false,
quality: 80,
compressionLevel: 6,
output: '__jpeg'
};
if (typeof input === 'string') {
this.options.fileIn = input;
} else if (typeof input ==='object' && input instanceof Buffer) {
if (input.length > 0) {
this.options.bufferIn = input;
} else {
throw new Error('Buffer is empty');
}
} else {
throw new Error('Unsupported input ' + typeof input);
}
return this;
};
module.exports = Sharp;
Sharp.prototype.crop = function() {
this.options.canvas = 'c';
return this;
};
Sharp.prototype.embedWhite = function() {
this.options.canvas = 'w';
return this;
};
Sharp.prototype.embedBlack = function() {
this.options.canvas = 'b';
return this;
};
Sharp.prototype.max = function() {
this.options.canvas = 'm';
return this;
};
/*
Rotate output image by 0, 90, 180 or 270 degrees
Auto-rotation based on the EXIF Orientation tag is represented by an angle of -1
*/
Sharp.prototype.rotate = function(angle) {
if (typeof angle === 'undefined') {
this.options.angle = -1;
} else if (!Number.isNaN(angle) && [0, 90, 180, 270].indexOf(angle) !== -1) {
this.options.angle = angle;
} else {
throw new Error('Unsupported angle (0, 90, 180, 270) ' + angle);
}
return this;
};
/*
Do not enlarge the output if the input width *or* height are already less than the required dimensions
This is equivalent to GraphicsMagick's ">" geometry option:
"change the dimensions of the image only if its width or height exceeds the geometry specification"
*/
Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {
this.options.withoutEnlargement = (typeof withoutEnlargement === 'boolean') ? withoutEnlargement : true;
return this;
};
Sharp.prototype.sharpen = function(sharpen) {
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
return this;
};
/*
Use bilinear interpolation for the affine transformation (fastest, default)
*/
Sharp.prototype.bilinearInterpolation = function() {
this.options.interpolator = 'bilinear';
return this;
};
/*
Use bicubic interpolation for the affine transformation
*/
Sharp.prototype.bicubicInterpolation = function() {
this.options.interpolator = 'bicubic';
return this;
};
/*
Use Nohalo interpolation for the affine transformation
*/
Sharp.prototype.nohaloInterpolation = function() {
this.options.interpolator = 'nohalo';
return this;
};
Sharp.prototype.progressive = function(progressive) {
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
return this;
};
Sharp.prototype.sequentialRead = function(sequentialRead) {
this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true;
return this;
};
Sharp.prototype.quality = function(quality) {
if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) {
this.options.quality = quality;
} else {
throw new Error('Invalid quality (1 to 100) ' + quality);
}
return this;
};
Sharp.prototype.compressionLevel = function(compressionLevel) {
if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) {
this.options.compressionLevel = compressionLevel;
} else {
throw new Error('Invalid compressionLevel (-1 to 9) ' + compressionLevel);
}
return this;
};
Sharp.prototype.resize = function(width, height) {
if (!width) {
this.options.width = -1;
} else {
if (!Number.isNaN(width)) {
this.options.width = width;
} else {
throw new Error('Invalid width ' + width);
}
}
if (!height) {
this.options.height = -1;
} else {
if (!Number.isNaN(height)) {
this.options.height = height;
} else {
throw new Error('Invalid height ' + height);
}
}
return this;
};
/*
Write output image data to a file
*/
Sharp.prototype.toFile = function(output, callback) {
if (!output || output.length === 0) {
var errOutputInvalid = new Error('Invalid output');
if (typeof callback === 'function') {
callback(errOutputInvalid);
} else {
return Promise.reject(errOutputInvalid);
}
} else {
if (this.options.fileIn === output) {
var errOutputIsInput = new Error('Cannot use same file for input and output');
if (typeof callback === 'function') {
callback(errOutputIsInput);
} else {
return Promise.reject(errOutputIsInput);
}
} else {
return this._sharp(output, callback);
}
}
return this;
};
// Deprecated to make way for future stream support - remove in v0.6.0
Sharp.prototype.write = require('util').deprecate(
Sharp.prototype.toFile,
'.write() is deprecated and will be removed in v0.6.0. Use .toFile() instead.'
);
Sharp.prototype.toBuffer = function(callback) {
return this._sharp('__input', callback);
};
Sharp.prototype.jpeg = function(callback) {
return this._sharp('__jpeg', callback);
};
Sharp.prototype.png = function(callback) {
return this._sharp('__png', callback);
};
Sharp.prototype.webp = function(callback) {
return this._sharp('__webp', callback);
};
/*
Invoke the C++ image processing pipeline
Supports callback and promise variants
*/
Sharp.prototype._sharp = function(output, callback) {
if (typeof callback === 'function') {
// I like callbacks
sharp.resize(this.options, output, callback);
return this;
} else {
// I like promises
var options = this.options;
return new Promise(function(resolve, reject) {
sharp.resize(options, output, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
};
module.exports.cache = function(limit) {
if (Number.isNaN(limit)) {
limit = null;
}
return sharp.cache(limit);
};

View File

@@ -1,9 +1,10 @@
{
"name": "sharp",
"version": "0.3.0",
"version": "0.5.1",
"author": "Lovell Fuller <npm@lovell.info>",
"contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>"
"Pierre Inglebert <pierre.inglebert@gmail.com>",
"Jonathan Ong <jonathanrichardong@gmail.com>"
],
"description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library",
"scripts": {
@@ -31,13 +32,14 @@
"buffer"
],
"dependencies": {
"nan": "^0.8.0"
"nan": "^1.2.0",
"bluebird": "^2.1.2"
},
"devDependencies": {
"imagemagick": "^0.1.3",
"imagemagick-native": "^0.2.9",
"gm": "^1.14.2",
"async": "^0.6.2",
"imagemagick-native": "^1.1.1",
"gm": "^1.16.0",
"async": "^0.9.0",
"benchmark": "^1.0.0"
},
"license": "Apache 2.0",

View File

@@ -20,13 +20,26 @@ struct resize_baton {
int width;
int height;
bool crop;
bool max;
VipsExtend extend;
bool sharpen;
bool progessive;
std::string interpolator;
bool progressive;
bool without_enlargement;
VipsAccess access_method;
int quality;
int compressionLevel;
int angle;
std::string err;
resize_baton(): buffer_in_len(0), buffer_out_len(0) {}
resize_baton():
buffer_in_len(0),
buffer_out_len(0),
crop(false),
max(false),
sharpen(false),
progressive(false),
without_enlargement(false) {}
};
typedef enum {
@@ -37,31 +50,31 @@ typedef enum {
MAGICK
} ImageType;
unsigned char MARKER_JPEG[] = {0xff, 0xd8};
unsigned char MARKER_PNG[] = {0x89, 0x50};
unsigned char MARKER_WEBP[] = {0x52, 0x49};
unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
unsigned char const MARKER_PNG[] = {0x89, 0x50};
unsigned char const MARKER_WEBP[] = {0x52, 0x49};
bool ends_with(std::string const &str, std::string const &end) {
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) {
static 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) {
static bool is_png(std::string const &str) {
return ends_with(str, ".png") || ends_with(str, ".PNG");
}
bool is_webp(std::string const &str) {
static bool is_webp(std::string const &str) {
return ends_with(str, ".webp") || ends_with(str, ".WEBP");
}
bool is_tiff(std::string const &str) {
static bool is_tiff(std::string const &str) {
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
}
void resize_error(resize_baton *baton, VipsImage *unref) {
static void resize_error(resize_baton *baton, VipsImage *unref) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(unref);
@@ -69,6 +82,38 @@ void resize_error(resize_baton *baton, VipsImage *unref) {
return;
}
/*
Calculate the angle of rotation for the output image.
In order of priority:
1. Use explicitly requested angle (supports 90, 180, 270)
2. Use input image EXIF Orientation header (does not support mirroring)
3. Otherwise default to zero, i.e. no rotation
*/
static VipsAngle calc_rotation(int const angle, VipsImage const *input) {
VipsAngle rotate = VIPS_ANGLE_0;
if (angle == -1) {
const char *exif;
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
if (exif[0] == 0x36) { // "6"
rotate = VIPS_ANGLE_90;
} else if (exif[0] == 0x33) { // "3"
rotate = VIPS_ANGLE_180;
} else if (exif[0] == 0x38) { // "8"
rotate = VIPS_ANGLE_270;
}
}
} else {
if (angle == 90) {
rotate = VIPS_ANGLE_90;
} else if (angle == 180) {
rotate = VIPS_ANGLE_180;
} else if (angle == 270) {
rotate = VIPS_ANGLE_270;
}
}
return rotate;
}
class ResizeWorker : public NanAsyncWorker {
public:
ResizeWorker(NanCallback *callback, resize_baton *baton)
@@ -129,32 +174,64 @@ class ResizeWorker : public NanAsyncWorker {
return;
}
// Get input image width and height
int inputWidth = in->Xsize;
int inputHeight = in->Ysize;
// Calculate angle of rotation, to be carried out later
VipsAngle rotation = calc_rotation(baton->angle, in);
if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
// Swap input output width and height when rotating by 90 or 270 degrees
int swap = inputWidth;
inputWidth = inputHeight;
inputHeight = swap;
}
// Scaling calculations
double factor;
if (baton->width > 0 && baton->height > 0) {
// Fixed width and height
double xfactor = (double)(in->Xsize) / (double)(baton->width);
double yfactor = (double)(in->Ysize) / (double)(baton->height);
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
// if max is set, we need to compute the real size of the thumb image
if (baton->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 = (double)(in->Xsize) / (double)(baton->width);
baton->height = floor((double)(in->Ysize) / factor);
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 = (double)(in->Ysize) / (double)(baton->height);
baton->width = floor((double)(in->Xsize) / factor);
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 = in->Xsize;
baton->height = in->Ysize;
baton->width = inputWidth;
baton->height = inputHeight;
}
int shrink = floor(factor);
if (shrink < 1) {
shrink = 1;
}
double residual = shrink / (double)factor;
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->without_enlargement) {
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
int shrink_on_load = 1;
@@ -175,8 +252,8 @@ class ResizeWorker : public NanAsyncWorker {
// Recalculate integral shrink and double residual
factor = std::max(factor, 1.0);
shrink = floor(factor);
residual = shrink / factor;
// Reload input using shrink-on-load
residual = static_cast<double>(shrink) / factor;
// Reload input using shrink-on-load
if (baton->buffer_in_len > 1) {
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, in);
@@ -197,113 +274,152 @@ class ResizeWorker : public NanAsyncWorker {
if (vips_shrink(shrunk_on_load, &shrunk, shrink, shrink, NULL)) {
return resize_error(baton, shrunk_on_load);
}
// Recalculate residual float based on dimensions of required vs shrunk images
double shrunkWidth = shrunk->Xsize;
double shrunkHeight = shrunk->Ysize;
if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
// 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->crop || baton->max) {
residual = std::max(residualx, residualy);
} else {
residual = std::min(residualx, residualy);
}
} else {
vips_copy(shrunk_on_load, &shrunk, NULL);
}
g_object_unref(shrunk_on_load);
// Use vips_affine with the remaining float part using bilinear interpolation
// Use vips_affine with the remaining float part
VipsImage *affined = vips_image_new();
if (residual != 0) {
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) {
// Create interpolator - "bilinear" (default), "bicubic" or "nohalo"
VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str());
// Perform affine transformation
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) {
g_object_unref(interpolator);
return resize_error(baton, shrunk);
}
g_object_unref(interpolator);
} else {
vips_copy(shrunk, &affined, NULL);
}
g_object_unref(shrunk);
// Rotate
VipsImage *rotated = vips_image_new();
if (rotation != VIPS_ANGLE_0) {
if (vips_rot(affined, &rotated, rotation, NULL)) {
return resize_error(baton, affined);
}
} else {
vips_copy(affined, &rotated, NULL);
}
g_object_unref(affined);
// Crop/embed
VipsImage *canvased = vips_image_new();
if (affined->Xsize != baton->width || affined->Ysize != baton->height) {
if (baton->crop) {
// Crop
int width = std::min(affined->Xsize, baton->width);
int height = std::min(affined->Ysize, baton->height);
int left = (affined->Xsize - width + 1) / 2;
int top = (affined->Ysize - height + 1) / 2;
if (vips_extract_area(affined, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, affined);
if (rotated->Xsize != baton->width || rotated->Ysize != baton->height) {
if (baton->crop || baton->max) {
// Crop/max
int width = std::min(rotated->Xsize, baton->width);
int height = std::min(rotated->Ysize, baton->height);
int left = (rotated->Xsize - width + 1) / 2;
int top = (rotated->Ysize - height + 1) / 2;
if (vips_extract_area(rotated, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, rotated);
}
} else {
// Embed
int left = (baton->width - affined->Xsize) / 2;
int top = (baton->height - affined->Ysize) / 2;
if (vips_embed(affined, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) {
return resize_error(baton, affined);
int left = (baton->width - rotated->Xsize) / 2;
int top = (baton->height - rotated->Ysize) / 2;
if (vips_embed(rotated, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) {
return resize_error(baton, rotated);
}
}
} else {
vips_copy(affined, &canvased, NULL);
vips_copy(rotated, &canvased, NULL);
}
g_object_unref(affined);
g_object_unref(rotated);
// Mild sharpen
VipsImage *sharpened = vips_image_new();
if (baton->sharpen) {
INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3,
-1, -1, -1,
-1, 32, -1,
-1, -1, -1);
sharpen->scale = 24;
if (im_conv(canvased, sharpened, sharpen)) {
VipsImage *sharpen = vips_image_new_matrixv(3, 3,
-1.0, -1.0, -1.0,
-1.0, 32.0, -1.0,
-1.0, -1.0, -1.0);
vips_image_set_double(sharpen, "scale", 24);
if (vips_conv(canvased, &sharpened, sharpen, NULL)) {
g_object_unref(sharpen);
return resize_error(baton, canvased);
}
g_object_unref(sharpen);
} else {
vips_copy(canvased, &sharpened, NULL);
}
g_object_unref(canvased);
// Always convert to sRGB colour space
VipsImage *colourspaced = vips_image_new();
vips_colourspace(sharpened, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL);
g_object_unref(sharpened);
// Output
if (baton->file_out == "__jpeg" || (baton->file_out == "__input" && inputImageType == JPEG)) {
// Write JPEG to buffer
if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened);
if (vips_jpegsave_buffer(colourspaced, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, colourspaced);
}
} else if (baton->file_out == "__png" || (baton->file_out == "__input" && inputImageType == PNG)) {
// Write PNG to buffer
if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened);
if (vips_pngsave_buffer(colourspaced, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, colourspaced);
}
} else if (baton->file_out == "__webp" || (baton->file_out == "__input" && inputImageType == WEBP)) {
// Write WEBP to buffer
if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, NULL)) {
return resize_error(baton, sharpened);
if (vips_webpsave_buffer(colourspaced, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, colourspaced);
}
} else if (is_jpeg(baton->file_out)) {
// Write JPEG to file
if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened);
if (vips_jpegsave(colourspaced, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, colourspaced);
}
} else if (is_png(baton->file_out)) {
// Write PNG to file
if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened);
if (vips_pngsave(colourspaced, baton->file_out.c_str(), "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, colourspaced);
}
} else if (is_webp(baton->file_out)) {
// Write WEBP to file
if (vips_webpsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, NULL)) {
return resize_error(baton, sharpened);
if (vips_webpsave(colourspaced, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, colourspaced);
}
} else if (is_tiff(baton->file_out)) {
// Write TIFF to file
if (vips_tiffsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", 80, NULL)) {
return resize_error(baton, sharpened);
if (vips_tiffsave(colourspaced, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
return resize_error(baton, colourspaced);
}
} else {
(baton->err).append("Unsupported output " + baton->file_out);
}
g_object_unref(sharpened);
g_object_unref(colourspaced);
vips_thread_shutdown();
}
void HandleOKCallback () {
NanScope();
Handle<Value> argv[2] = { Null(), Null() };
Handle<Value> argv[2] = { NanNull(), NanNull() };
if (!baton->err.empty()) {
// Error
argv[0] = String::New(baton->err.data(), baton->err.size());
argv[0] = NanNew<String>(baton->err.data(), baton->err.size());
} else if (baton->buffer_out_len > 0) {
// Buffer
argv[1] = NanNewBufferHandle((char *)baton->buffer_out, baton->buffer_out_len);
@@ -317,52 +433,69 @@ class ResizeWorker : public NanAsyncWorker {
resize_baton* baton;
};
/*
resize(options, output, callback)
*/
NAN_METHOD(resize) {
NanScope();
// V8 objects are converted to non-V8 types held in the baton struct
resize_baton *baton = new resize_baton;
baton->file_in = *String::Utf8Value(args[0]->ToString());
if (args[1]->IsObject()) {
Local<Object> buffer = args[1]->ToObject();
Local<Object> options = args[0]->ToObject();
// Input filename
baton->file_in = *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->buffer_in_len = Buffer::Length(buffer);
baton->buffer_in = Buffer::Data(buffer);
}
baton->file_out = *String::Utf8Value(args[2]->ToString());
baton->width = args[3]->Int32Value();
baton->height = args[4]->Int32Value();
Local<String> canvas = args[5]->ToString();
if (canvas->Equals(String::NewSymbol("c"))) {
// Output image dimensions
baton->width = options->Get(NanNew<String>("width"))->Int32Value();
baton->height = options->Get(NanNew<String>("height"))->Int32Value();
// Canvas options
Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
if (canvas->Equals(NanNew<String>("c"))) {
baton->crop = true;
} else if (canvas->Equals(String::NewSymbol("w"))) {
baton->crop = false;
} else if (canvas->Equals(NanNew<String>("w"))) {
baton->extend = VIPS_EXTEND_WHITE;
} else if (canvas->Equals(String::NewSymbol("b"))) {
baton->crop = false;
} else if (canvas->Equals(NanNew<String>("b"))) {
baton->extend = VIPS_EXTEND_BLACK;
} else if (canvas->Equals(NanNew<String>("m"))) {
baton->max = true;
}
baton->sharpen = args[6]->BooleanValue();
baton->progessive = args[7]->BooleanValue();
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
NanCallback *callback = new NanCallback(args[9].As<v8::Function>());
// Other options
baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
baton->without_enlargement = options->Get(NanNew<String>("withoutEnlargement"))->BooleanValue();
baton->access_method = options->Get(NanNew<String>("sequentialRead"))->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
// Output filename or __format for Buffer
baton->file_out = *String::Utf8Value(args[1]->ToString());
// Join queue for worker thread
NanCallback *callback = new NanCallback(args[2].As<v8::Function>());
NanAsyncQueueWorker(new ResizeWorker(callback, baton));
NanReturnUndefined();
}
NAN_METHOD(cache) {
NanScope();
// Set cache limit
if (args[0]->IsInt32()) {
vips_cache_set_max_mem(args[0]->Int32Value() * 1048576);
}
// Get cache statistics
Local<Object> cache = Object::New();
cache->Set(String::NewSymbol("current"), Number::New(vips_tracked_get_mem() / 1048576));
cache->Set(String::NewSymbol("high"), Number::New(vips_tracked_get_mem_highwater() / 1048576));
cache->Set(String::NewSymbol("limit"), Number::New(vips_cache_get_max_mem() / 1048576));
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>("limit"), NanNew<Number>(vips_cache_get_max_mem() / 1048576));
NanReturnValue(cache);
}

BIN
tests/fixtures/Landscape_8.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -111,7 +111,7 @@ async.series({
}).add("sharp-buffer-file", {
defer: true,
fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).write(outputJpg, function(err) {
sharp(inputJpgBuffer).resize(width, height).toFile(outputJpg, function(err) {
if (err) {
throw err;
} else {
@@ -134,7 +134,7 @@ async.series({
}).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).write(outputJpg, function(err) {
sharp(inputJpg).resize(width, height).toFile(outputJpg, function(err) {
if (err) {
throw err;
} else {
@@ -154,6 +154,14 @@ async.series({
}
});
}
}).add("sharp-file-buffer-promise", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).toBuffer().then(function(buffer) {
assert.notStrictEqual(null, buffer);
deferred.resolve();
});
}
}).add("sharp-file-buffer-sharpen", {
defer: true,
fn: function(deferred) {
@@ -166,6 +174,30 @@ async.series({
}
});
}
}).add("sharp-file-buffer-bicubic", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).bicubicInterpolation().toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-nohalo", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).nohaloInterpolation().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) {
@@ -178,6 +210,18 @@ async.series({
}
});
}
}).add("sharp-file-buffer-rotate", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).rotate(90).resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-sequentialRead", {
defer: true,
fn: function(deferred) {
@@ -251,7 +295,7 @@ async.series({
}).add("sharp-buffer-file", {
defer: true,
fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).write(outputPng, function(err) {
sharp(inputPngBuffer).resize(width, height).toFile(outputPng, function(err) {
if (err) {
throw err;
} else {
@@ -274,7 +318,7 @@ async.series({
}).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputPng).resize(width, height).write(outputPng, function(err) {
sharp(inputPng).resize(width, height).toFile(outputPng, function(err) {
if (err) {
throw err;
} else {
@@ -318,18 +362,6 @@ async.series({
}
});
}
}).add("sharp-file-buffer-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputPng).sequentialRead().resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log(" png " + String(event.target));
}).on("complete", function() {
@@ -341,7 +373,7 @@ async.series({
(new Benchmark.Suite("webp")).add("sharp-buffer-file", {
defer: true,
fn: function(deferred) {
sharp(inputWebpBuffer).resize(width, height).write(outputWebp, function(err) {
sharp(inputWebpBuffer).resize(width, height).toFile(outputWebp, function(err) {
if (err) {
throw err;
} else {
@@ -364,7 +396,7 @@ async.series({
}).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputWebp).resize(width, height).write(outputWebp, function(err) {
sharp(inputWebp).resize(width, height).toFile(outputWebp, function(err) {
if (err) {
throw err;
} else {
@@ -396,18 +428,6 @@ async.series({
}
});
}
}).add("sharp-file-buffer-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputWebp).sequentialRead().resize(width, height).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log("webp " + String(event.target));
}).on("complete", function() {
@@ -418,7 +438,7 @@ async.series({
(new Benchmark.Suite("tiff")).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputTiff).resize(width, height).write(outputTiff, function(err) {
sharp(inputTiff).resize(width, height).toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
@@ -429,18 +449,7 @@ async.series({
}).add("sharp-file-file-sharpen", {
defer: true,
fn: function(deferred) {
sharp(inputTiff).resize(width, height).sharpen().write(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-file-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputTiff).sequentialRead().resize(width, height).write(outputTiff, function(err) {
sharp(inputTiff).resize(width, height).sharpen().toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
@@ -458,7 +467,7 @@ async.series({
(new Benchmark.Suite("gif")).add("sharp-file-file", {
defer: true,
fn: function(deferred) {
sharp(inputGif).resize(width, height).write(outputTiff, function(err) {
sharp(inputGif).resize(width, height).toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
@@ -469,7 +478,7 @@ async.series({
}).add("sharp-file-file-sharpen", {
defer: true,
fn: function(deferred) {
sharp(inputGif).resize(width, height).sharpen().write(outputTiff, function(err) {
sharp(inputGif).resize(width, height).sharpen().toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
@@ -480,7 +489,7 @@ async.series({
}).add("sharp-file-file-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputGif).sequentialRead().resize(width, height).write(outputTiff, function(err) {
sharp(inputGif).sequentialRead().resize(width, height).toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {

View File

@@ -1,73 +1,308 @@
var sharp = require("../index");
var path = require("path");
var imagemagick = require("imagemagick");
var assert = require("assert");
var async = require("async");
var fixturesPath = path.join(__dirname, "fixtures");
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = path.join(fixturesPath, "output.jpg");
async.series([
// Resize with exact crop
function(done) {
sharp(inputJpg).resize(320, 240).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Resize to fixed width
function(done) {
sharp(inputJpg).resize(320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(261, features.height);
done();
});
});
},
// Resize to fixed height
function(done) {
sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(391, features.width);
assert.strictEqual(320, features.height);
done();
});
});
},
// Identity transform
function(done) {
sharp(inputJpg).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(2725, features.width);
assert.strictEqual(2225, features.height);
done();
});
});
},
// Upscale
function(done) {
sharp(inputJpg).resize(3000).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(3000, features.width);
assert.strictEqual(2449, features.height);
done();
});
});
}
]);
/*jslint node: true */
/*jslint es5: true */
'use strict';
var sharp = require("../index");
var path = require("path");
var imagemagick = require("imagemagick");
var assert = require("assert");
var async = require("async");
var fixturesPath = path.join(__dirname, "fixtures");
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = path.join(fixturesPath, "output.jpg");
var inputTiff = path.join(fixturesPath, "G31D.TIF"); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
var outputTiff = path.join(fixturesPath, "output.tiff");
var inputJpgWithExif = path.join(fixturesPath, "Landscape_8.jpg"); // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
async.series([
// Resize with exact crop
function(done) {
sharp(inputJpg).resize(320, 240).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Resize to fixed width
function(done) {
sharp(inputJpg).resize(320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(261, features.height);
done();
});
});
},
// Resize to fixed height
function(done) {
sharp(inputJpg).resize(null, 320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(391, features.width);
assert.strictEqual(320, features.height);
done();
});
});
},
// Identity transform
function(done) {
sharp(inputJpg).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(2725, features.width);
assert.strictEqual(2225, features.height);
done();
});
});
},
// Upscale
function(done) {
sharp(inputJpg).resize(3000).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(3000, features.width);
assert.strictEqual(2449, features.height);
done();
});
});
},
// Quality
function(done) {
sharp(inputJpg).resize(320, 240).quality(70).jpeg(function(err, buffer70) {
if (err) throw err;
sharp(inputJpg).resize(320, 240).jpeg(function(err, buffer80) {
if (err) throw err;
sharp(inputJpg).resize(320, 240).quality(90).jpeg(function(err, buffer90) {
assert(buffer70.length < buffer80.length);
assert(buffer80.length < buffer90.length);
done();
});
});
});
},
// TIFF with dimensions known to cause rounding errors
function(done) {
sharp(inputTiff).resize(240, 320).embedBlack().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(240, features.width);
assert.strictEqual(320, features.height);
done();
});
});
},
function(done) {
sharp(inputTiff).resize(240, 320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(240, features.width);
assert.strictEqual(320, features.height);
done();
});
});
},
// Resize to max width or height considering ratio (landscape)
function(done) {
sharp(inputJpg).resize(320, 320).max().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(261, features.height);
done();
});
});
},
// Resize to max width or height considering ratio (portrait)
function(done) {
sharp(inputTiff).resize(320, 320).max().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(243, features.width);
assert.strictEqual(320, features.height);
done();
});
});
},
// Attempt to resize to max but only provide one dimension, so should default to crop
function(done) {
sharp(inputJpg).resize(320).max().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(261, features.height);
done();
});
});
},
// 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).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Input image has Orientation EXIF tag but do not rotate output
function(done) {
sharp(inputJpgWithExif).resize(320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(426, features.height);
done();
});
});
},
// Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate
function(done) {
sharp(inputJpgWithExif).rotate().resize(320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Attempt to auto-rotate using image that has no EXIF
function(done) {
sharp(inputJpg).rotate().resize(320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(261, features.height);
done();
});
});
},
// 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().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(2725, features.width);
assert.strictEqual(2225, features.height);
done();
});
});
},
// Do not enlarge the output if the input height is already less than the output height
function(done) {
sharp(inputJpg).resize(null, 2300).withoutEnlargement().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(2725, features.width);
assert.strictEqual(2225, features.height);
done();
});
});
},
// Promises/A+
function(done) {
sharp(inputJpg).resize(320, 240).toFile(outputJpg).then(function() {
imagemagick.identify(outputJpg, function(err, features) {
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.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().then(function() {
done();
});
},
// Interpolation: bilinear
function(done) {
sharp(inputJpg).resize(320, 240).bilinearInterpolation().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Interpolation: bicubic
function(done) {
sharp(inputJpg).resize(320, 240).bicubicInterpolation().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
// Interpolation: nohalo
function(done) {
sharp(inputJpg).resize(320, 240).nohaloInterpolation().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
done();
});
});
},
]);