Compare commits

..

29 Commits

Author SHA1 Message Date
Lovell Fuller
1cce56b024 Use tile cache for interlace output
Doubles performance of Adam7 interlaced PNG output
2014-07-09 23:47:04 +01:00
Lovell Fuller
2126f9afc1 Ensure xcode/clang uses cflags
Silence pedantic warnings triggered by V8 in 0.11
2014-07-09 21:14:40 +01:00
Lovell Fuller
41420eedcf Add details of Heroku buildpack, thanks @alex88 #57 2014-07-06 13:20:41 +01:00
Lovell Fuller
1b6ab19b6d Remove fftw dependency when compiling libvips from src #57 2014-07-03 21:12:18 +01:00
Lovell Fuller
fbe5c18762 Expose depth of task queue 2014-06-25 23:21:39 +01:00
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
10 changed files with 630 additions and 217 deletions

View File

@@ -5,12 +5,12 @@ node_js:
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 libxml2-dev swig graphicsmagick libmagick++-dev
- 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=no --without-orc --without-python
- ./configure --enable-debug=no --enable-cxx=yes --without-orc --without-python --without-fftw
- make
- sudo make install
- sudo ldconfig

125
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,7 +28,7 @@ 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_ will take advantage of [liborc](http://code.entropywave.com/orc/) if present, however versions of _liborc_ prior to 0.4.19 suffer memory leaks.
_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
@@ -48,19 +48,19 @@ The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp
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 libxml2-dev swig
sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev libexif-dev libxml2-dev swig
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 --without-orc
./configure --enable-debug=no --enable-cxx=yes --without-python --without-orc --without-fftw
make
sudo make install
sudo ldconfig
#### Ubuntu 12.x
Requires `libtiff4-dev` instead of `libtiff5-dev` and has [a bug](https://bugs.launchpad.net/ubuntu/+source/libwebp/+bug/1108731) in the libwebp package. Work around these problems by running these command first:
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
@@ -68,6 +68,10 @@ Requires `libtiff4-dev` instead of `libtiff5-dev` and has [a bug](https://bugs.l
Then follow Ubuntu 13.x instructions.
### Install libvips on Heroku
[Alessandro Tagliapietra](https://github.com/alex88) maintains an [Heroku buildpack for libvips](https://github.com/alex88/heroku-buildpack-vips) and its dependencies.
## Usage examples
```javascript
@@ -75,7 +79,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;
}
@@ -85,29 +89,23 @@ sharp('input.jpg').resize(300, 200).write('output.jpg', function(err) {
```
```javascript
sharp('input.jpg').resize(null, 200).progressive().toBuffer(function(err, outputBuffer) {
sharp('input.jpg').rotate().resize(null, 200).progressive().toBuffer(function(err, outputBuffer) {
if (err) {
throw err;
}
// outputBuffer 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().quality(90).webp(function(err, outputBuffer) {
if (err) {
throw err;
}
// outputBuffer contains 300 pixels wide, sharpened, 90% quality WebP image data
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(inputBuffer).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
});
```
@@ -123,10 +121,7 @@ sharp('input.gif').resize(200, 300).embedBlack().webp(function(err, outputBuffer
```
```javascript
sharp(inputBuffer).resize(200, 200).max().jpeg(function(err, outputBuffer) {
if (err) {
throw err;
}
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
});
@@ -143,7 +138,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.
@@ -167,10 +162,36 @@ 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.
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
Use this method without `angle` to determine the angle from EXIF data. Mirroring is currently unsupported.
### withoutEnlargement()
Do not enlarge the output image if the input image width *or* height are already less than the required dimensions.
This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification".
### sharpen()
Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
### 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.
@@ -191,35 +212,45 @@ An advanced setting for the _zlib_ compression level of the lossless PNG output
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])
@@ -230,9 +261,9 @@ This method always returns cache statistics, useful for determining how much wor
Warnings such as _Application transferred too many scanlines_ are a good indicator you've set this value too low.
```javascript
var stats = sharp.cache(); // { current: 98, high: 115, limit: 100 }
sharp.cache(200); // { current: 98, high: 115, limit: 200 }
sharp.cache(50); // { current: 49, high: 115, limit: 50 }
var stats = sharp.cache(); // { current: 98, high: 115, limit: 100, queue: 0 }
sharp.cache(200); // { current: 98, high: 115, limit: 200, queue: 0 }
sharp.cache(50); // { current: 49, high: 115, limit: 50, queue: 0 }
```
## Testing

View File

@@ -2,19 +2,20 @@
'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',
'/usr/lib/i386-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'],
'cflags_cc': ['-fexceptions', '-pedantic', '-Wall', '-O3']
'cflags': ['-fexceptions', '-Wall', '-O3'],
'cflags_cc': ['-fexceptions', '-Wall', '-O3'],
'xcode_settings': {
'OTHER_CFLAGS': ['-fexceptions', '-Wall', '-O3']
}
}]
}

141
index.js
View File

@@ -1,6 +1,7 @@
/*jslint node: true */
'use strict';
var Promise = require('bluebird');
var sharp = require('./build/Release/sharp');
var Sharp = function(input) {
@@ -11,19 +12,26 @@ var Sharp = function(input) {
width: -1,
height: -1,
canvas: 'c',
angle: 0,
withoutEnlargement: false,
sharpen: false,
interpolator: 'bilinear',
progressive: false,
sequentialRead: false,
quality: 80,
compressionLevel: 6,
quality: 80,
compressionLevel: 6,
output: '__jpeg'
};
if (typeof input === 'string') {
this.options.inFile = input;
this.options.fileIn = input;
} else if (typeof input ==='object' && input instanceof Buffer) {
this.options.inBuffer = input;
if (input.length > 0) {
this.options.bufferIn = input;
} else {
throw new Error('Buffer is empty');
}
} else {
throw 'Unsupported input ' + typeof input;
throw new Error('Unsupported input ' + typeof input);
}
return this;
};
@@ -49,12 +57,60 @@ Sharp.prototype.max = function() {
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;
@@ -69,7 +125,7 @@ Sharp.prototype.quality = function(quality) {
if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) {
this.options.quality = quality;
} else {
throw 'Invalid quality (1 to 100) ' + quality;
throw new Error('Invalid quality (1 to 100) ' + quality);
}
return this;
};
@@ -78,7 +134,7 @@ Sharp.prototype.compressionLevel = function(compressionLevel) {
if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) {
this.options.compressionLevel = compressionLevel;
} else {
throw 'Invalid compressionLevel (-1 to 9) ' + compressionLevel;
throw new Error('Invalid compressionLevel (-1 to 9) ' + compressionLevel);
}
return this;
};
@@ -90,7 +146,7 @@ Sharp.prototype.resize = function(width, height) {
if (!Number.isNaN(width)) {
this.options.width = width;
} else {
throw 'Invalid width ' + width;
throw new Error('Invalid width ' + width);
}
}
if (!height) {
@@ -99,25 +155,44 @@ Sharp.prototype.resize = function(width, height) {
if (!Number.isNaN(height)) {
this.options.height = height;
} else {
throw 'Invalid height ' + height;
throw new Error('Invalid height ' + height);
}
}
return this;
};
Sharp.prototype.write = function(output, callback) {
/*
Write output image data to a file
*/
Sharp.prototype.toFile = function(output, callback) {
if (!output || output.length === 0) {
callback('Invalid output');
} else {
if (this.options.inFile === output) {
callback('Cannot use same file for input and output');
var errOutputInvalid = new Error('Invalid output');
if (typeof callback === 'function') {
callback(errOutputInvalid);
} else {
this._sharp(output, callback);
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);
};
@@ -134,22 +209,28 @@ 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) {
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,
this.options.quality,
this.options.compressionLevel,
callback
);
return this;
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) {

View File

@@ -1,9 +1,10 @@
{
"name": "sharp",
"version": "0.4.2",
"version": "0.5.2",
"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,11 +32,12 @@
"buffer"
],
"dependencies": {
"nan": "^1.1.0"
"nan": "^1.2.0",
"bluebird": "^2.2.1"
},
"devDependencies": {
"imagemagick": "^0.1.3",
"imagemagick-native": "^1.0.0",
"imagemagick-native": "^1.2.2",
"gm": "^1.16.0",
"async": "^0.9.0",
"benchmark": "^1.0.0"

View File

@@ -23,13 +23,23 @@ struct resize_baton {
bool max;
VipsExtend extend;
bool sharpen;
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), crop(false), max(false), sharpen(false), progressive(false) {}
resize_baton():
buffer_in_len(0),
buffer_out_len(0),
crop(false),
max(false),
sharpen(false),
progressive(false),
without_enlargement(false) {}
};
typedef enum {
@@ -40,31 +50,34 @@ 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) {
// How many tasks are in the queue?
volatile int queue_length = 0;
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);
@@ -72,6 +85,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)
@@ -132,34 +177,47 @@ 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 = static_cast<double>(in->Xsize) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(in->Ysize) / static_cast<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>(in->Ysize) / xfactor);
baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else {
baton->width = round(static_cast<double>(in->Xsize) / yfactor);
baton->width = round(static_cast<double>(inputWidth) / yfactor);
}
}
} else if (baton->width > 0) {
// Fixed width, auto height
factor = static_cast<double>(in->Xsize) / static_cast<double>(baton->width);
baton->height = floor(static_cast<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 = static_cast<double>(in->Ysize) / static_cast<double>(baton->height);
baton->width = floor(static_cast<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) {
@@ -167,6 +225,17 @@ class ResizeWorker : public NanAsyncWorker {
}
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;
if (inputImageType == JPEG) {
@@ -186,7 +255,7 @@ class ResizeWorker : public NanAsyncWorker {
// Recalculate integral shrink and double residual
factor = std::max(factor, 1.0);
shrink = floor(factor);
residual = shrink / factor;
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)) {
@@ -209,8 +278,16 @@ class ResizeWorker : public NanAsyncWorker {
return resize_error(baton, shrunk_on_load);
}
// Recalculate residual float based on dimensions of required vs shrunk images
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunk->Xsize);
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunk->Ysize);
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 {
@@ -221,41 +298,57 @@ class ResizeWorker : public NanAsyncWorker {
}
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 (rotated->Xsize != baton->width || rotated->Ysize != baton->height) {
if (baton->crop || baton->max) {
// Crop/max
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);
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();
@@ -275,46 +368,63 @@ class ResizeWorker : public NanAsyncWorker {
}
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);
// Generate image tile cache when interlace output is required
VipsImage *cached = vips_image_new();
if (baton->progressive) {
if (vips_tilecache(colourspaced, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) {
return resize_error(baton, colourspaced);
}
} else {
vips_copy(colourspaced, &cached, NULL);
}
g_object_unref(colourspaced);
// Output
VipsImage *output = cached;
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", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, sharpened);
if (vips_jpegsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} 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", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, sharpened);
if (vips_pngsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} 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", baton->quality, NULL)) {
return resize_error(baton, sharpened);
if (vips_webpsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
}
} else if (is_jpeg(baton->file_out)) {
// Write JPEG to file
if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, sharpened);
if (vips_jpegsave(output, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} else if (is_png(baton->file_out)) {
// Write PNG to file
if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, sharpened);
if (vips_pngsave(output, baton->file_out.c_str(), "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} else if (is_webp(baton->file_out)) {
// Write WEBP to file
if (vips_webpsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, sharpened);
if (vips_webpsave(output, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
}
} 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", baton->quality, NULL)) {
return resize_error(baton, sharpened);
if (vips_tiffsave(output, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
}
} else {
(baton->err).append("Unsupported output " + baton->file_out);
}
g_object_unref(sharpened);
g_object_unref(output);
vips_thread_shutdown();
}
@@ -332,26 +442,37 @@ class ResizeWorker : public NanAsyncWorker {
}
delete baton;
callback->Call(2, argv);
// Decrement queue length
g_atomic_int_dec_and_test(&queue_length);
}
private:
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();
// 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(NanNew<String>("w"))) {
@@ -361,15 +482,25 @@ NAN_METHOD(resize) {
} else if (canvas->Equals(NanNew<String>("m"))) {
baton->max = true;
}
baton->sharpen = args[6]->BooleanValue();
baton->progressive = args[7]->BooleanValue();
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
baton->quality = args[9]->Int32Value();
baton->compressionLevel = args[10]->Int32Value();
NanCallback *callback = new NanCallback(args[11].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));
// Increment queue length
g_atomic_int_inc(&queue_length);
NanReturnUndefined();
}
@@ -386,6 +517,7 @@ NAN_METHOD(cache) {
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));
cache->Set(NanNew<String>("queue"), NanNew<Number>(queue_length));
NanReturnValue(cache);
}

BIN
tests/fixtures/Landscape_8.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -8,6 +8,10 @@ var inputJpg = path.join(__dirname, "fixtures/2569067123_aca715a2ee_o.jpg"); //
var width = 720;
var height = 480;
var timer = setInterval(function() {
console.dir(sharp.cache());
}, 100);
async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
var start = new Date().getTime();
async.times(parallelism,
@@ -28,5 +32,6 @@ async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
}
);
}, function() {
clearInterval(timer);
console.dir(sharp.cache());
});

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,3 +1,7 @@
/*jslint node: true */
/*jslint es5: true */
'use strict';
var sharp = require("../index");
var path = require("path");
var imagemagick = require("imagemagick");
@@ -12,10 +16,12 @@ 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).write(outputJpg, function(err) {
sharp(inputJpg).resize(320, 240).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -27,7 +33,7 @@ async.series([
},
// Resize to fixed width
function(done) {
sharp(inputJpg).resize(320).write(outputJpg, function(err) {
sharp(inputJpg).resize(320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -39,7 +45,7 @@ async.series([
},
// Resize to fixed height
function(done) {
sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) {
sharp(inputJpg).resize(null, 320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -51,7 +57,7 @@ async.series([
},
// Identity transform
function(done) {
sharp(inputJpg).write(outputJpg, function(err) {
sharp(inputJpg).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -63,7 +69,7 @@ async.series([
},
// Upscale
function(done) {
sharp(inputJpg).resize(3000).write(outputJpg, function(err) {
sharp(inputJpg).resize(3000).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -89,7 +95,7 @@ async.series([
},
// TIFF with dimensions known to cause rounding errors
function(done) {
sharp(inputTiff).resize(240, 320).embedBlack().write(outputJpg, function(err) {
sharp(inputTiff).resize(240, 320).embedBlack().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -100,7 +106,7 @@ async.series([
});
},
function(done) {
sharp(inputTiff).resize(240, 320).write(outputJpg, function(err) {
sharp(inputTiff).resize(240, 320).toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -112,7 +118,7 @@ async.series([
},
// Resize to max width or height considering ratio (landscape)
function(done) {
sharp(inputJpg).resize(320, 320).max().write(outputJpg, function(err) {
sharp(inputJpg).resize(320, 320).max().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -124,7 +130,7 @@ async.series([
},
// Resize to max width or height considering ratio (portrait)
function(done) {
sharp(inputTiff).resize(320, 320).max().write(outputJpg, function(err) {
sharp(inputTiff).resize(320, 320).max().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -136,7 +142,7 @@ async.series([
},
// Attempt to resize to max but only provide one dimension, so should default to crop
function(done) {
sharp(inputJpg).resize(320).max().write(outputJpg, function(err) {
sharp(inputJpg).resize(320).max().toFile(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
@@ -148,9 +154,155 @@ async.series([
},
// Attempt to output to input, should fail
function(done) {
sharp(inputJpg).write(inputJpg, function(err) {
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();
});
});
},
]);