Compare commits

...

51 Commits

Author SHA1 Message Date
Lovell Fuller
98554e919c Small doc improvements ahead of v0.6.0 2014-08-22 16:57:05 +01:00
Lovell Fuller
017bf1e905 Expose libvips interpolators #69 2014-08-22 16:50:24 +01:00
Lovell Fuller
6498fc3a9e Ignore libmagick's inconsistent GIF colour spaces 2014-08-22 14:31:34 +01:00
Lovell Fuller
8ba71c94f4 Add usage example of metadata method 2014-08-22 14:27:46 +01:00
Lovell Fuller
f2f3eb76e1 Add method for fast access to image metadata #32 2014-08-22 14:17:43 +01:00
Lovell Fuller
5fe945fca8 Add gravity support to crop #45 2014-08-21 11:01:25 +01:00
Lovell Fuller
8ef0851a49 Expose libvips' op-cache max items #76 2014-08-19 20:30:21 +01:00
Lovell Fuller
e45956db6c Correct usage examples for new Stream-based API 2014-08-18 19:34:55 +01:00
Lovell Fuller
7cc9f7e2e0 Add Xcode Command Line Tools install info #80 2014-08-18 19:11:53 +01:00
Lovell Fuller
df3903532d Initial Duplex Stream support 2014-08-07 14:00:34 +01:00
Lovell Fuller
46456c9a2a Clear cached warnings after success 2014-08-06 11:34:16 +01:00
Lovell Fuller
e98f2fc013 Small documentation improvement plus version bump 2014-07-20 22:46:26 +01:00
Lovell Fuller
7df7a505ee Merge pull request #68 from chanon/master
Pass additional info (width, height) to output callback
2014-07-20 22:22:34 +01:00
Chanon
d40bdcc6ac Sends width and height as another parameter to the callback. Fixes issue #67 2014-07-20 23:52:28 +07:00
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
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
10 changed files with 1618 additions and 410 deletions

View File

@@ -5,12 +5,12 @@ node_js:
before_install: before_install:
- sudo add-apt-repository ppa:lyrasis/precise-backports -y - sudo add-apt-repository ppa:lyrasis/precise-backports -y
- sudo apt-get update -qq - 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 - git clone https://github.com/jcupitt/libvips.git
- cd libvips - cd libvips
- git checkout 7.38 - git checkout 7.38
- ./bootstrap.sh - ./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 - make
- sudo make install - sudo make install
- sudo ldconfig - sudo ldconfig

262
README.md
View File

@@ -9,15 +9,17 @@
The typical use case for this high speed Node.js module is to convert large images of many formats to smaller, web-friendly JPEG, PNG and WebP images of varying dimensions. The typical use case for this high speed Node.js module is to convert large 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.
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. Memory usage is kept to a minimum, no child processes are spawned, everything remains non-blocking thanks to _libuv_ and Promises/A+ are supported.
This module supports reading and writing JPEG, PNG and WebP images to and from Streams, Buffer objects and the filesystem. It also supports reading images of many other types from the filesystem via libmagick++ or libgraphicsmagick++ if present.
When generating JPEG output all metadata is removed and Huffman tables optimised without having to use separate command line tools like [jpegoptim](https://github.com/tjko/jpegoptim) and [jpegtran](http://jpegclub.org/jpegtran/). When generating JPEG output all metadata is removed and Huffman tables optimised without having to use separate command line tools like [jpegoptim](https://github.com/tjko/jpegoptim) and [jpegtran](http://jpegclub.org/jpegtran/).
Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent. Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent.
This module is powered by the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by John Cupitt. This module is powered by the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by [John Cupitt](https://github.com/jcupitt).
## Installation ## Installation
@@ -28,12 +30,16 @@ This module is powered by the blazingly fast [libvips](https://github.com/jcupit
* Node.js v0.10+ * Node.js v0.10+
* [libvips](https://github.com/jcupitt/libvips) v7.38.5+ * [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 ### Install libvips on Mac OS
brew install homebrew/science/vips --with-webp --with-graphicsmagick brew install homebrew/science/vips --with-webp --with-graphicsmagick
A missing or incorrectly configured _Xcode Command Line Tools_ installation [can lead](https://github.com/lovell/sharp/issues/80) to a `library not found for -ljpeg` error. If so, please try:
xcode-select --install
The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp/issues/9) to a `library not found for -lintl` error. If so, please try: The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp/issues/9) to a `library not found for -lintl` error. If so, please try:
brew link gettext --force brew link gettext --force
@@ -48,19 +54,19 @@ The _gettext_ dependency of _libvips_ [can lead](https://github.com/lovell/sharp
Compiling from source is recommended: 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 git clone https://github.com/jcupitt/libvips.git
cd libvips cd libvips
git checkout 7.38 git checkout 7.38
./bootstrap.sh ./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 make
sudo make install sudo make install
sudo ldconfig sudo ldconfig
#### Ubuntu 12.x #### 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 add-apt-repository ppa:lyrasis/precise-backports
sudo apt-get update sudo apt-get update
@@ -68,6 +74,10 @@ Requires `libtiff4-dev` instead of `libtiff5-dev` and has [a bug](https://bugs.l
Then follow Ubuntu 13.x instructions. 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 ## Usage examples
```javascript ```javascript
@@ -75,7 +85,7 @@ var sharp = require('sharp');
``` ```
```javascript ```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) { if (err) {
throw err; throw err;
} }
@@ -85,35 +95,74 @@ sharp('input.jpg').resize(300, 200).write('output.jpg', function(err) {
``` ```
```javascript ```javascript
sharp('input.jpg').resize(null, 200).progressive().toBuffer(function(err, outputBuffer) { var transformer = sharp().resize(300, 200).crop(sharp.gravity.north);
if (err) { readableStream.pipe(transformer).pipe(writableStream);
throw err; // Read image data from readableStream, resize and write image data to writableStream
} ```
// outputBuffer contains progressive JPEG image data, 200 pixels high
```javascript
var image = sharp(inputJpg);
image.metadata(function(err, metadata) {
image.resize(metadata.width / 2).webp().toBuffer(function(err, outputBuffer, info) {
// outputBuffer contains a WebP image half the width and height of the original JPEG
});
}); });
``` ```
```javascript ```javascript
sharp('input.png').resize(300).sharpen().quality(90).webp(function(err, outputBuffer) { var pipeline = sharp()
if (err) { .rotate()
throw err; .resize(null, 200)
} .progressive()
// outputBuffer contains 300 pixels wide, sharpened, 90% quality WebP image data .toBuffer(function(err, outputBuffer, info) {
}); if (err) {
throw err;
}
// outputBuffer contains 200px high progressive JPEG image data,
// auto-rotated using EXIF Orientation tag
// info.width and info.height contain the dimensions of the resized image
});
readableStream.pipe(pipeline);
``` ```
```javascript ```javascript
sharp(inputBuffer).resize(200, 300).embedWhite().write('output.tiff', function(err) { sharp('input.png')
if (err) { .rotate(180)
throw err; .resize(300)
} .sharpen()
// output.tiff is a 200 pixels wide and 300 pixels high image containing a scaled .quality(90)
// version, embedded on a white canvas, of the image data in buffer .webp()
}); .toBuffer()
.then(function(outputBuffer) {
// outputBuffer contains 300px wide, upside down, sharpened,
// 90% quality WebP image data
});
``` ```
```javascript ```javascript
sharp('input.gif').resize(200, 300).embedBlack().webp(function(err, outputBuffer) { http.createServer(function(request, response) {
response.writeHead(200, {'Content-Type': 'image/webp'});
sharp('input.jpg').rotate().resize(200).webp().pipe(response);
}).listen(8000);
// Create HTTP server that always returns auto-rotated 'input.jpg',
// resized to 200 pixels wide, in WebP format
```
```javascript
sharp(inputBuffer)
.resize(200, 300)
.interpolateWith(sharp.interpolator.nohalo)
.embedWhite()
.toFile('output.tiff')
.then(function() {
// output.tiff is a 200 pixels wide and 300 pixels high image
// containing a bicubic scaled version, embedded on a white canvas,
// of the image data in inputBuffer
});
```
```javascript
sharp('input.gif').resize(200, 300).embedBlack().webp().toBuffer(function(err, outputBuffer) {
if (err) { if (err) {
throw err; throw err;
} }
@@ -123,10 +172,7 @@ sharp('input.gif').resize(200, 300).embedBlack().webp(function(err, outputBuffer
``` ```
```javascript ```javascript
sharp(inputBuffer).resize(200, 200).max().jpeg(function(err, outputBuffer) { sharp(inputBuffer).resize(200, 200).max().jpeg().toBuffer().then(function(outputBuffer) {
if (err) {
throw err;
}
// outputBuffer contains JPEG image data no wider than 200 pixels and no higher // outputBuffer contains JPEG image data no wider than 200 pixels and no higher
// than 200 pixels regardless of the inputBuffer image dimensions // than 200 pixels regardless of the inputBuffer image dimensions
}); });
@@ -134,105 +180,183 @@ sharp(inputBuffer).resize(200, 200).max().jpeg(function(err, outputBuffer) {
## API ## API
### sharp(input) ### Input methods
Constructor to which further methods are chained. `input` can be one of: #### sharp([input])
Constructor to which further methods are chained. `input`, if present, can be one of:
* Buffer containing JPEG, PNG or WebP image data, or * Buffer containing JPEG, PNG or WebP image data, or
* String containing the filename of an image, with most major formats supported. * String containing the filename of an image, with most major formats supported.
### resize(width, [height]) The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class.
Scale to `width` x `height`. By default, the resized image is cropped to the exact size specified. JPEG, PNG or WebP format image data can be streamed into the object when `input` is not provided.
JPEG, PNG or WebP format image data can be streamed out from this object.
#### metadata([callback])
Fast access to image metadata without decoding any compressed image data.
`callback`, if present, gets the arguments `(err, metadata)` where `metadata` has the attributes:
* `format`: Name of decoder to be used to decompress image data e.g. `jpeg`, `png`, `webp` (for file-based input additionally `tiff` and `magick`)
* `width`: Number of pixels wide
* `height`: Number of pixels high
* `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L502)
* `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK
* `orientation`: Number value of the EXIF Orientation header, if present
A Promises/A+ promise is returned when `callback` is not provided.
#### sequentialRead()
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
### Image transformation options
#### resize(width, [height])
Scale output to `width` x `height`. By default, the resized image is cropped to the exact size specified.
`width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height. `width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
`height` is the Number of pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width. `height` is the Number of pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width.
### crop() #### crop([gravity])
Crop the resized image to the exact size specified, the default behaviour. Crop the resized image to the exact size specified, the default behaviour.
### max() `gravity`, if present, is an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north`.
Possible values are `north`, `east`, `south`, `west`, `center` and `centre`. The default gravity is `center`/`centre`.
#### max()
Preserving aspect ratio, resize the image to the maximum width or height specified. 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`. Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
### embedWhite() #### embedWhite()
Embed the resized image on a white background of the exact size specified. Embed the resized image on a white background of the exact size specified.
### embedBlack() #### embedBlack()
Embed the resized image on a black background of the exact size specified. Embed the resized image on a black background of the exact size specified.
### sharpen() #### 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%. Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
### progressive() #### interpolateWith(interpolator)
Use the given interpolator for image resizing, where `interpolator` is an attribute of the `sharp.interpolator` Object e.g. `sharp.interpolator.bicubic`.
Possible interpolators, in order of performance, are:
* `bilinear`: Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation), the default (and fastest) interpolation.
* `bicubic`: Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation), which typically reduces performance by 5%.
* `vertexSplitQuadraticBasisSpline`: Use [VSQBS interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/vsqbs.cpp#L48), which prevents "staircasing" when enlarging and typically reduces performance by 5%.
* `locallyBoundedBicubic`: Use [LBB interpolation](https://github.com/jcupitt/libvips/blob/master/libvips/resample/lbb.cpp#L100), which prevents some "[acutance](http://en.wikipedia.org/wiki/Acutance)" and typically reduces performance by a factor of 2.
* `nohalo`: Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/), which prevents acutance and typically reduces performance by a factor of 3.
### Output options
#### progressive()
Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed. 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) #### quality(quality)
The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`. 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. `quality` is a Number between 1 and 100.
### compressionLevel(compressionLevel) #### compressionLevel(compressionLevel)
An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`. 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. `compressionLevel` is a Number between -1 and 9.
### sequentialRead() #### jpeg()
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. Use JPEG format for the output image.
### write(filename, callback) #### png()
Use PNG format for the output image.
#### webp()
Use WebP format for the output image.
### Output methods
#### toFile(filename, [callback])
`filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP and TIFF supported. `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 two arguments `(err, info)` where:
### jpeg(callback) * `err` contains an error message, if any
* `info` contains the final resized image dimensions in its `width` and `height` properties
Write JPEG image data to a Buffer. A Promises/A+ promise is returned when `callback` is not provided.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant JPEG image data. #### toBuffer([callback])
### png(callback) Write image data to a Buffer, the format of which will match the input image by default. JPEG, PNG and WebP are supported.
Write PNG image data to a Buffer. `callback`, if present, gets three arguments `(err, buffer, info)` where:
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant PNG image data. * `err` is an error message, if any
* `buffer` is the resultant image data
* `info` contains the final resized image dimensions in its `width` and `height` properties
### webp(callback) A Promises/A+ promise is returned when `callback` is not provided.
Write WebP image data to a Buffer. ### Utility methods
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant WebP image data. #### sharp.cache([memory], [items])
### toBuffer(callback) If `memory` or `items` are provided, set the limits of _libvips'_ operation cache.
Write image data to a Buffer, the format of which will match the input image. JPEG, PNG and WebP are supported. * `memory` is the maximum memory in MB to use for this cache, with a default value of 100
* `items` is the maximum number of operations to cache, with a default value of 500
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data.
### sharp.cache([limit])
If `limit` is provided, set the (soft) limit of _libvips_ working/cache memory to this value in MB. The default value is 100.
This method always returns cache statistics, useful for determining how much working memory is required for a particular task. This method always returns cache statistics, useful for determining how much working memory is required for a particular task.
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: 75, high: 99, memory: 100, items: 500 }
sharp.cache(200); // { current: 75, high: 99, memory: 200, items: 500 }
sharp.cache(50, 200); // { current: 49, high: 99, memory: 50, items: 200}
```
#### sharp.counters()
Provides access to internal task counters.
* `queue` is the number of tasks this module has queued waiting for _libuv_ to provide a worker thread from its pool.
* `process` is the number of resize tasks currently being processed.
```javascript ```javascript
var stats = sharp.cache(); // { current: 98, high: 115, limit: 100 } var counters = sharp.counters(); // { queue: 2, process: 4 }
sharp.cache(200); // { current: 98, high: 115, limit: 200 }
sharp.cache(50); // { current: 49, high: 115, limit: 50 }
``` ```
## Testing ## Testing
@@ -289,7 +413,7 @@ You can expect much greater performance with caching enabled (default) and using
## Licence ## Licence
Copyright 2013, 2014 Lovell Fuller and Pierre Inglebert Copyright 2013, 2014 Lovell Fuller, Pierre Inglebert, Jonathan Ong and Chanon Sajjamanochai
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -2,18 +2,20 @@
'targets': [{ 'targets': [{
'target_name': 'sharp', 'target_name': 'sharp',
'sources': ['src/sharp.cc'], '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': [ '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': [ 'include_dirs': [
'/usr/local/include/glib-2.0', '<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips 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',
'<!(node -e "require(\'nan\')")' '<!(node -e "require(\'nan\')")'
], ],
'cflags': ['-fexceptions', '-pedantic', '-Wall', '-O3'], 'cflags': ['-fexceptions', '-Wall', '-O3'],
'cflags_cc': ['-fexceptions', '-pedantic', '-Wall', '-O3'] 'cflags_cc': ['-std=c++0x', '-fexceptions', '-Wall', '-O3'],
'xcode_settings': {
'OTHER_CFLAGS': ['-std=c++11', '-fexceptions', '-Wall', '-O3']
}
}] }]
} }

352
index.js
View File

@@ -1,36 +1,91 @@
/*jslint node: true */ /*jslint node: true */
'use strict'; 'use strict';
var util = require('util');
var stream = require('stream');
var Promise = require('bluebird');
var sharp = require('./build/Release/sharp'); var sharp = require('./build/Release/sharp');
var Sharp = function(input) { var Sharp = function(input) {
if (!(this instanceof Sharp)) { if (!(this instanceof Sharp)) {
return new Sharp(input); return new Sharp(input);
} }
stream.Duplex.call(this);
this.options = { this.options = {
width: -1, width: -1,
height: -1, height: -1,
canvas: 'c', canvas: 'c',
gravity: 0,
angle: 0,
withoutEnlargement: false,
sharpen: false, sharpen: false,
interpolator: 'bilinear',
progressive: false, progressive: false,
sequentialRead: false, sequentialRead: false,
quality: 80, quality: 80,
compressionLevel: 6, compressionLevel: 6,
output: '__jpeg' streamIn: false,
streamOut: false,
output: '__input'
}; };
if (typeof input === 'string') { if (typeof input === 'string') {
this.options.inFile = input; // input=file
} else if (typeof input ==='object' && input instanceof Buffer) { this.options.fileIn = input;
this.options.inBuffer = input; } else if (typeof input === 'object' && input instanceof Buffer) {
// input=buffer
if (input.length > 0) {
this.options.bufferIn = input;
} else {
throw new Error('Buffer is empty');
}
} else { } else {
throw 'Unsupported input ' + typeof input; // input=stream
this.options.streamIn = true;
} }
return this; return this;
}; };
module.exports = Sharp; module.exports = Sharp;
util.inherits(Sharp, stream.Duplex);
Sharp.prototype.crop = function() { /*
Handle incoming chunk on Writable Stream
*/
Sharp.prototype._write = function(chunk, encoding, callback) {
if (this.options.streamIn) {
if (typeof chunk === 'object' || chunk instanceof Buffer) {
if (typeof this.options.bufferIn === 'undefined') {
// Create new Buffer
this.options.bufferIn = new Buffer(chunk.length);
chunk.copy(this.options.bufferIn);
} else {
// Append to existing Buffer
this.options.bufferIn = Buffer.concat(
[this.options.bufferIn, chunk],
this.options.bufferIn.length + chunk.length
);
}
callback();
} else {
callback(new Error('Non-Buffer data on Writable Stream'));
}
} else {
callback(new Error('Unexpected data on Writable Stream'));
}
};
// Crop this part of the resized image (Center/Centre, North, East, South, West)
module.exports.gravity = {'center': 0, 'centre': 0, 'north': 1, 'east': 2, 'south': 3, 'west': 4};
Sharp.prototype.crop = function(gravity) {
this.options.canvas = 'c'; this.options.canvas = 'c';
if (typeof gravity !== 'undefined') {
// Is this a supported gravity?
if (!Number.isNaN(gravity) && gravity >= 0 && gravity <= 4) {
this.options.gravity = gravity;
} else {
throw new Error('Unsupported crop gravity ' + gravity);
}
}
return this; return this;
}; };
@@ -49,12 +104,64 @@ Sharp.prototype.max = function() {
return this; 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) { Sharp.prototype.sharpen = function(sharpen) {
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true; this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
return this; return this;
}; };
/*
Set the interpolator to use for the affine transformation
*/
module.exports.interpolator = {
bilinear: 'bilinear',
bicubic: 'bicubic',
nohalo: 'nohalo',
locallyBoundedBicubic: 'lbb',
vertexSplitQuadraticBasisSpline: 'vsqbs'
};
Sharp.prototype.interpolateWith = function(interpolator) {
this.options.interpolator = interpolator;
return this;
};
/*
Deprecated interpolation methods, to be removed in v0.7.0
*/
Sharp.prototype.bilinearInterpolation = util.deprecate(function() {
return this.interpolateWith(module.exports.interpolator.bilinear);
}, 'bilinearInterpolation() is deprecated, use interpolateWith(sharp.interpolator.bilinear) instead');
Sharp.prototype.bicubicInterpolation = util.deprecate(function() {
return this.interpolateWith(module.exports.interpolator.bicubic);
}, 'bicubicInterpolation() is deprecated, use interpolateWith(sharp.interpolator.bicubic) instead');
Sharp.prototype.nohaloInterpolation = util.deprecate(function() {
return this.interpolateWith(module.exports.interpolator.nohalo);
}, 'nohaloInterpolation() is deprecated, use interpolateWith(sharp.interpolator.nohalo) instead');
Sharp.prototype.progressive = function(progressive) { Sharp.prototype.progressive = function(progressive) {
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true; this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
return this; return this;
@@ -69,7 +176,7 @@ Sharp.prototype.quality = function(quality) {
if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) { if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) {
this.options.quality = quality; this.options.quality = quality;
} else { } else {
throw 'Invalid quality (1 to 100) ' + quality; throw new Error('Invalid quality (1 to 100) ' + quality);
} }
return this; return this;
}; };
@@ -78,7 +185,7 @@ Sharp.prototype.compressionLevel = function(compressionLevel) {
if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) { if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) {
this.options.compressionLevel = compressionLevel; this.options.compressionLevel = compressionLevel;
} else { } else {
throw 'Invalid compressionLevel (-1 to 9) ' + compressionLevel; throw new Error('Invalid compressionLevel (-1 to 9) ' + compressionLevel);
} }
return this; return this;
}; };
@@ -90,7 +197,7 @@ Sharp.prototype.resize = function(width, height) {
if (!Number.isNaN(width)) { if (!Number.isNaN(width)) {
this.options.width = width; this.options.width = width;
} else { } else {
throw 'Invalid width ' + width; throw new Error('Invalid width ' + width);
} }
} }
if (!height) { if (!height) {
@@ -99,58 +206,209 @@ Sharp.prototype.resize = function(width, height) {
if (!Number.isNaN(height)) { if (!Number.isNaN(height)) {
this.options.height = height; this.options.height = height;
} else { } else {
throw 'Invalid height ' + height; throw new Error('Invalid height ' + height);
} }
} }
return this; 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) { if (!output || output.length === 0) {
throw 'Invalid output'; var errOutputInvalid = new Error('Invalid output');
if (typeof callback === 'function') {
callback(errOutputInvalid);
} else {
return Promise.reject(errOutputInvalid);
}
} else { } else {
this._sharp(output, callback); if (this.options.fileIn === output) {
var errOutputIsInput = new Error('Cannot use same file for input and output');
if (typeof callback === 'function') {
callback(errOutputIsInput);
} else {
return Promise.reject(errOutputIsInput);
}
} else {
this.options.output = output;
return this._sharp(callback);
}
} }
return this; return this;
}; };
Sharp.prototype.toBuffer = function(callback) { Sharp.prototype.toBuffer = function(callback) {
return this._sharp('__input', callback); return this._sharp(callback);
}; };
Sharp.prototype.jpeg = function(callback) { Sharp.prototype.jpeg = function() {
return this._sharp('__jpeg', callback); this.options.output = '__jpeg';
}; if (arguments.length > 0) {
console.error('Use of the jpeg() method with a callback is deprecated in 0.6.x and will be removed in 0.7.x');
Sharp.prototype.png = function(callback) { console.error('Please add toFile(), toBuffer() or Stream methods e.g. pipe() for JPEG output');
return this._sharp('__png', callback); this._sharp(arguments);
}; }
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,
this.options.quality,
this.options.compressionLevel,
callback
);
return this; return this;
}; };
module.exports.cache = function(limit) { Sharp.prototype.png = function() {
if (Number.isNaN(limit)) { this.options.output = '__png';
limit = null; if (arguments.length > 0) {
console.error('Use of the png() method with a callback is deprecated in 0.6.x and will be removed in 0.7.x');
console.error('Please add toFile(), toBuffer() or Stream methods e.g. pipe() for PNG output');
this._sharp(arguments);
} }
return sharp.cache(limit); return this;
};
Sharp.prototype.webp = function() {
this.options.output = '__webp';
if (arguments.length > 0) {
console.error('Use of the webp() method with a callback is deprecated in 0.6.x and will be removed in 0.7.x');
console.error('Please add toFile(), toBuffer() or Stream methods e.g. pipe() for WebP output');
this._sharp(arguments);
}
return this;
};
/*
Used by a Writable Stream to notify that it is ready for data
*/
Sharp.prototype._read = function() {
if (!this.options.streamOut) {
this.options.streamOut = true;
this._sharp();
}
};
/*
Invoke the C++ image processing pipeline
Supports callback, stream and promise variants
*/
Sharp.prototype._sharp = function(callback) {
var that = this;
if (typeof callback === 'function') {
// output=file/buffer
if (this.options.streamIn) {
// output=file/buffer, input=stream
this.on('finish', function() {
sharp.resize(that.options, callback);
});
} else {
// output=file/buffer, input=file/buffer
sharp.resize(this.options, callback);
}
return this;
} else if (this.options.streamOut) {
// output=stream
if (this.options.streamIn) {
// output=stream, input=stream
this.on('finish', function() {
sharp.resize(that.options, function(err, data) {
if (err) throw err;
that.push(data);
that.push(null);
});
});
} else {
// output=stream, input=file/buffer
sharp.resize(this.options, function(err, data) {
if (err) throw err;
that.push(data);
that.push(null);
});
}
return this;
} else {
// output=promise
if (this.options.streamIn) {
// output=promise, input=stream
return new Promise(function(resolve, reject) {
that.on('finish', function() {
sharp.resize(that.options, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
});
} else {
// output=promise, input=file/buffer
return new Promise(function(resolve, reject) {
sharp.resize(that.options, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
}
};
/*
Reads the image header and returns metadata
Supports callback, stream and promise variants
*/
Sharp.prototype.metadata = function(callback) {
var that = this;
if (typeof callback === 'function') {
if (this.options.streamIn) {
this.on('finish', function() {
sharp.metadata(that.options, callback);
});
} else {
sharp.metadata(this.options, callback);
}
return this;
} else {
if (this.options.streamIn) {
return new Promise(function(resolve, reject) {
that.on('finish', function() {
sharp.metadata(that.options, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
});
} else {
return new Promise(function(resolve, reject) {
sharp.metadata(that.options, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
}
};
/*
Get and set cache memory and item limits
*/
module.exports.cache = function(memory, items) {
if (Number.isNaN(memory)) {
memory = null;
}
if (Number.isNaN(items)) {
items = null;
}
return sharp.cache(memory, items);
};
/*
Get internal counters
*/
module.exports.counters = function() {
return sharp.counters();
}; };

View File

@@ -1,9 +1,11 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.4.0", "version": "0.6.0",
"author": "Lovell Fuller <npm@lovell.info>", "author": "Lovell Fuller <npm@lovell.info>",
"contributors": [ "contributors": [
"Pierre Inglebert <pierre.inglebert@gmail.com>" "Pierre Inglebert <pierre.inglebert@gmail.com>",
"Jonathan Ong <jonathanrichardong@gmail.com>",
"Chanon Sajjamanochai <chanon.s@gmail.com>"
], ],
"description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library", "description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library",
"scripts": { "scripts": {
@@ -28,16 +30,18 @@
"libvips", "libvips",
"vips", "vips",
"fast", "fast",
"buffer" "buffer",
"stream"
], ],
"dependencies": { "dependencies": {
"nan": "^1.0.0" "nan": "^1.3.0",
"bluebird": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"imagemagick": "^0.1.3", "imagemagick": "^0.1.3",
"imagemagick-native": "git://github.com/mash/node-imagemagick-native.git#master", "imagemagick-native": "^1.2.2",
"gm": "^1.16.0", "gm": "^1.16.0",
"async": "^0.8.0", "async": "^0.9.0",
"benchmark": "^1.0.0" "benchmark": "^1.0.0"
}, },
"license": "Apache 2.0", "license": "Apache 2.0",

View File

@@ -3,6 +3,7 @@
#include <math.h> #include <math.h>
#include <string> #include <string>
#include <string.h> #include <string.h>
#include <tuple>
#include <vips/vips.h> #include <vips/vips.h>
#include "nan.h" #include "nan.h"
@@ -14,25 +15,38 @@ struct resize_baton {
std::string file_in; std::string file_in;
void* buffer_in; void* buffer_in;
size_t buffer_in_len; size_t buffer_in_len;
std::string file_out; std::string output;
void* buffer_out; void* buffer_out;
size_t buffer_out_len; size_t buffer_out_len;
int width; int width;
int height; int height;
bool crop; bool crop;
int gravity;
bool max; bool max;
VipsExtend extend; VipsExtend extend;
bool sharpen; bool sharpen;
std::string interpolator;
bool progressive; bool progressive;
bool without_enlargement;
VipsAccess access_method; VipsAccess access_method;
int quality; int quality;
int compressionLevel; int compressionLevel;
int angle;
std::string err; 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),
gravity(0),
max(false),
sharpen(false),
progressive(false),
without_enlargement(false) {}
}; };
typedef enum { typedef enum {
UNKNOWN,
JPEG, JPEG,
PNG, PNG,
WEBP, WEBP,
@@ -40,31 +54,36 @@ typedef enum {
MAGICK MAGICK
} ImageType; } ImageType;
unsigned char MARKER_JPEG[] = {0xff, 0xd8}; unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
unsigned char MARKER_PNG[] = {0x89, 0x50}; unsigned char const MARKER_PNG[] = {0x89, 0x50};
unsigned char MARKER_WEBP[] = {0x52, 0x49}; 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 counter_queue = 0;
// How many tasks are being processed?
volatile int counter_process = 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); 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"); 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"); 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"); 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"); 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()); (baton->err).append(vips_error_buffer());
vips_error_clear(); vips_error_clear();
g_object_unref(unref); g_object_unref(unref);
@@ -72,94 +91,329 @@ void resize_error(resize_baton *baton, VipsImage *unref) {
return; return;
} }
class ResizeWorker : public NanAsyncWorker { /*
public: Calculate the angle of rotation for the output image.
ResizeWorker(NanCallback *callback, resize_baton *baton) In order of priority:
: NanAsyncWorker(callback), baton(baton) {} 1. Use explicitly requested angle (supports 90, 180, 270)
~ResizeWorker() {} 2. Use input image EXIF Orientation header (does not support mirroring)
3. Otherwise default to zero, i.e. no rotation
*/
static VipsAngle
sharp_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;
}
void Execute () { /*
// Input Calculate the (left, top) coordinates of the output image
ImageType inputImageType = JPEG; within the input image, applying the given gravity.
VipsImage *in = vips_image_new(); */
static std::tuple<int, int>
sharp_calc_crop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const gravity) {
int left = 0;
int top = 0;
switch (gravity) {
case 1: // North
left = (inWidth - outWidth + 1) / 2;
break;
case 2: // East
left = inWidth - outWidth;
top = (inHeight - outHeight + 1) / 2;
break;
case 3: // South
left = (inWidth - outWidth + 1) / 2;
top = inHeight - outHeight;
break;
case 4: // West
top = (inHeight - outHeight + 1) / 2;
break;
default: // Centre
left = (inWidth - outWidth + 1) / 2;
top = (inHeight - outHeight + 1) / 2;
}
return std::make_tuple(left, top);
}
/*
Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP.
Returns the ImageType detected, if any.
*/
static ImageType
sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const length, VipsAccess const access) {
ImageType imageType = UNKNOWN;
if (memcmp(MARKER_JPEG, buffer, 2) == 0) {
if (!vips_jpegload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = JPEG;
}
} else if(memcmp(MARKER_PNG, buffer, 2) == 0) {
if (!vips_pngload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = PNG;
}
} else if(memcmp(MARKER_WEBP, buffer, 2) == 0) {
if (!vips_webpload_buffer(buffer, length, image, "access", access, NULL)) {
imageType = WEBP;
}
}
return imageType;
}
/*
Initialise a VipsImage from a file.
Returns the ImageType detected, if any.
*/
static ImageType
sharp_init_image_from_file(VipsImage **image, char const *file, VipsAccess const access) {
ImageType imageType = UNKNOWN;
if (vips_foreign_is_a("jpegload", file)) {
if (!vips_jpegload(file, image, "access", access, NULL)) {
imageType = JPEG;
}
} else if (vips_foreign_is_a("pngload", file)) {
if (!vips_pngload(file, image, "access", access, NULL)) {
imageType = PNG;
}
} else if (vips_foreign_is_a("webpload", file)) {
if (!vips_webpload(file, image, "access", access, NULL)) {
imageType = WEBP;
}
} else if (vips_foreign_is_a("tiffload", file)) {
if (!vips_tiffload(file, image, "access", access, NULL)) {
imageType = TIFF;
}
} else if(vips_foreign_is_a("magickload", file)) {
if (!vips_magickload(file, image, "access", access, NULL)) {
imageType = MAGICK;
}
}
return imageType;
}
// Metadata
struct metadata_baton {
// Input
std::string file_in;
void* buffer_in;
size_t buffer_in_len;
// Output
std::string format;
int width;
int height;
std::string space;
int channels;
int orientation;
std::string err;
metadata_baton():
buffer_in_len(0),
orientation(0) {}
};
class MetadataWorker : public NanAsyncWorker {
public:
MetadataWorker(NanCallback *callback, metadata_baton *baton) : NanAsyncWorker(callback), baton(baton) {}
~MetadataWorker() {}
void Execute() {
// Decrement queued task counter
g_atomic_int_dec_and_test(&counter_queue);
ImageType imageType = UNKNOWN;
VipsImage *image = vips_image_new();
if (baton->buffer_in_len > 1) { if (baton->buffer_in_len > 1) {
if (memcmp(MARKER_JPEG, baton->buffer_in, 2) == 0) { // From buffer
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) { imageType = sharp_init_image_from_buffer(&image, baton->buffer_in, baton->buffer_in_len, VIPS_ACCESS_RANDOM);
return resize_error(baton, in); if (imageType == UNKNOWN) {
} (baton->err).append("Input buffer contains unsupported image format");
} else if(memcmp(MARKER_PNG, baton->buffer_in, 2) == 0) {
inputImageType = PNG;
if (vips_pngload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if(memcmp(MARKER_WEBP, baton->buffer_in, 2) == 0) {
inputImageType = WEBP;
if (vips_webpload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else {
resize_error(baton, in);
(baton->err).append("Unsupported input buffer");
return;
}
} else if (vips_foreign_is_a("jpegload", baton->file_in.c_str())) {
if (vips_jpegload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if (vips_foreign_is_a("pngload", baton->file_in.c_str())) {
inputImageType = PNG;
if (vips_pngload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if (vips_foreign_is_a("webpload", baton->file_in.c_str())) {
inputImageType = WEBP;
if (vips_webpload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if (vips_foreign_is_a("tiffload", baton->file_in.c_str())) {
inputImageType = TIFF;
if (vips_tiffload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if(vips_foreign_is_a("magickload", (baton->file_in).c_str())) {
inputImageType = MAGICK;
if (vips_magickload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
} }
} else { } else {
resize_error(baton, in); // From file
(baton->err).append("Unsupported input file " + baton->file_in); imageType = sharp_init_image_from_file(&image, baton->file_in.c_str(), VIPS_ACCESS_RANDOM);
return; if (imageType == UNKNOWN) {
(baton->err).append("File is of an unsupported image format");
}
}
if (imageType != UNKNOWN) {
// Image type
switch (imageType) {
case JPEG: baton->format = "jpeg"; break;
case PNG: baton->format = "png"; break;
case WEBP: baton->format = "webp"; break;
case TIFF: baton->format = "tiff"; break;
case MAGICK: baton->format = "magick"; break;
case UNKNOWN: default: baton->format = "";
}
// VipsImage attributes
baton->width = image->Xsize;
baton->height = image->Ysize;
baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type);
baton->channels = image->Bands;
// EXIF Orientation
const char *exif;
if (!vips_image_get_string(image, "exif-ifd0-Orientation", &exif)) {
baton->orientation = atoi(&exif[0]);
}
}
// Clean up
g_object_unref(image);
vips_error_clear();
vips_thread_shutdown();
}
void HandleOKCallback () {
NanScope();
Handle<Value> argv[2] = { NanNull(), NanNull() };
if (!baton->err.empty()) {
// Error
argv[0] = NanNew<String>(baton->err.data(), baton->err.size());
} else {
// Metadata Object
Local<Object> info = NanNew<Object>();
info->Set(NanNew<String>("format"), NanNew<String>(baton->format));
info->Set(NanNew<String>("width"), NanNew<Number>(baton->width));
info->Set(NanNew<String>("height"), NanNew<Number>(baton->height));
info->Set(NanNew<String>("space"), NanNew<String>(baton->space));
info->Set(NanNew<String>("channels"), NanNew<Number>(baton->channels));
if (baton->orientation > 0) {
info->Set(NanNew<String>("orientation"), NanNew<Number>(baton->orientation));
}
argv[1] = info;
}
delete baton;
// Return to JavaScript
callback->Call(2, argv);
}
private:
metadata_baton* baton;
};
/*
metadata(options, callback)
*/
NAN_METHOD(metadata) {
NanScope();
// V8 objects are converted to non-V8 types held in the baton struct
metadata_baton *baton = new metadata_baton;
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);
}
// Join queue for worker thread
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
NanAsyncQueueWorker(new MetadataWorker(callback, baton));
// Increment queued task counter
g_atomic_int_inc(&counter_queue);
NanReturnUndefined();
}
// Resize
class ResizeWorker : public NanAsyncWorker {
public:
ResizeWorker(NanCallback *callback, resize_baton *baton) : NanAsyncWorker(callback), baton(baton) {}
~ResizeWorker() {}
void Execute() {
// Decrement queued task counter
g_atomic_int_dec_and_test(&counter_queue);
// Increment processing task counter
g_atomic_int_inc(&counter_process);
// Input
ImageType inputImageType = UNKNOWN;
VipsImage *in = vips_image_new();
if (baton->buffer_in_len > 1) {
// From buffer
inputImageType = sharp_init_image_from_buffer(&in, baton->buffer_in, baton->buffer_in_len, baton->access_method);
if (inputImageType == UNKNOWN) {
(baton->err).append("Input buffer contains unsupported image format");
}
} else {
// From file
inputImageType = sharp_init_image_from_file(&in, baton->file_in.c_str(), baton->access_method);
if (inputImageType == UNKNOWN) {
(baton->err).append("File is of an unsupported image format");
}
}
if (inputImageType == UNKNOWN) {
return resize_error(baton, in);
}
// 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 = sharp_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 // Scaling calculations
double factor; double factor;
if (baton->width > 0 && baton->height > 0) { if (baton->width > 0 && baton->height > 0) {
// Fixed width and height // Fixed width and height
double xfactor = static_cast<double>(in->Xsize) / static_cast<double>(baton->width); double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
double yfactor = static_cast<double>(in->Ysize) / static_cast<double>(baton->height); double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor); 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 max is set, we need to compute the real size of the thumb image
if (baton->max) { if (baton->max) {
if (xfactor > yfactor) { if (xfactor > yfactor) {
baton->height = round(static_cast<double>(in->Ysize) / xfactor); baton->height = round(static_cast<double>(inputHeight) / xfactor);
} else { } else {
baton->width = round(static_cast<double>(in->Xsize) / yfactor); baton->width = round(static_cast<double>(inputWidth) / yfactor);
} }
} }
} else if (baton->width > 0) { } else if (baton->width > 0) {
// Fixed width, auto height // Fixed width, auto height
factor = static_cast<double>(in->Xsize) / static_cast<double>(baton->width); factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
baton->height = floor(static_cast<double>(in->Ysize) / factor); baton->height = floor(static_cast<double>(inputHeight) / factor);
} else if (baton->height > 0) { } else if (baton->height > 0) {
// Fixed height, auto width // Fixed height, auto width
factor = static_cast<double>(in->Ysize) / static_cast<double>(baton->height); factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
baton->width = floor(static_cast<double>(in->Xsize) / factor); baton->width = floor(static_cast<double>(inputWidth) / factor);
} else { } else {
// Identity transform // Identity transform
factor = 1; factor = 1;
baton->width = in->Xsize; baton->width = inputWidth;
baton->height = in->Ysize; baton->height = inputHeight;
} }
int shrink = floor(factor); int shrink = floor(factor);
if (shrink < 1) { if (shrink < 1) {
@@ -167,6 +421,17 @@ class ResizeWorker : public NanAsyncWorker {
} }
double residual = static_cast<double>(shrink) / 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 // Try to use libjpeg shrink-on-load
int shrink_on_load = 1; int shrink_on_load = 1;
if (inputImageType == JPEG) { if (inputImageType == JPEG) {
@@ -186,7 +451,7 @@ class ResizeWorker : public NanAsyncWorker {
// Recalculate integral shrink and double residual // Recalculate integral shrink and double residual
factor = std::max(factor, 1.0); factor = std::max(factor, 1.0);
shrink = floor(factor); shrink = floor(factor);
residual = shrink / factor; residual = static_cast<double>(shrink) / factor;
// Reload input using shrink-on-load // Reload input using shrink-on-load
if (baton->buffer_in_len > 1) { 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)) { if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
@@ -209,9 +474,17 @@ class ResizeWorker : public NanAsyncWorker {
return resize_error(baton, shrunk_on_load); return resize_error(baton, shrunk_on_load);
} }
// Recalculate residual float based on dimensions of required vs shrunk images // Recalculate residual float based on dimensions of required vs shrunk images
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunk->Xsize); double shrunkWidth = shrunk->Xsize;
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunk->Ysize); double shrunkHeight = shrunk->Ysize;
if (baton->crop) { 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); residual = std::max(residualx, residualy);
} else { } else {
residual = std::min(residualx, residualy); residual = std::min(residualx, residualy);
@@ -221,41 +494,58 @@ class ResizeWorker : public NanAsyncWorker {
} }
g_object_unref(shrunk_on_load); 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(); VipsImage *affined = vips_image_new();
if (residual != 0) { 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); return resize_error(baton, shrunk);
} }
g_object_unref(interpolator);
} else { } else {
vips_copy(shrunk, &affined, NULL); vips_copy(shrunk, &affined, NULL);
} }
g_object_unref(shrunk); 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 // Crop/embed
VipsImage *canvased = vips_image_new(); 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) { if (baton->crop || baton->max) {
// Crop/max // Crop/max
int width = std::min(affined->Xsize, baton->width); int left;
int height = std::min(affined->Ysize, baton->height); int top;
int left = (affined->Xsize - width + 1) / 2; std::tie(left, top) = sharp_calc_crop(rotated->Xsize, rotated->Ysize, baton->width, baton->height, baton->gravity);
int top = (affined->Ysize - height + 1) / 2; int width = std::min(rotated->Xsize, baton->width);
if (vips_extract_area(affined, &canvased, left, top, width, height, NULL)) { int height = std::min(rotated->Ysize, baton->height);
return resize_error(baton, affined); if (vips_extract_area(rotated, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, rotated);
} }
} else { } else {
// Embed // Embed
int left = (baton->width - affined->Xsize) / 2; int left = (baton->width - rotated->Xsize) / 2;
int top = (baton->height - affined->Ysize) / 2; int top = (baton->height - rotated->Ysize) / 2;
if (vips_embed(affined, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) { if (vips_embed(rotated, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) {
return resize_error(baton, affined); return resize_error(baton, rotated);
} }
} }
} else { } else {
vips_copy(affined, &canvased, NULL); vips_copy(rotated, &canvased, NULL);
} }
g_object_unref(affined); g_object_unref(rotated);
// Mild sharpen // Mild sharpen
VipsImage *sharpened = vips_image_new(); VipsImage *sharpened = vips_image_new();
@@ -275,121 +565,196 @@ class ResizeWorker : public NanAsyncWorker {
} }
g_object_unref(canvased); g_object_unref(canvased);
// Output // Always convert to sRGB colour space
if (baton->file_out == "__jpeg" || (baton->file_out == "__input" && inputImageType == JPEG)) { VipsImage *colourspaced = vips_image_new();
// Write JPEG to buffer vips_colourspace(sharpened, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL);
if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) { g_object_unref(sharpened);
return resize_error(baton, sharpened);
} // Generate image tile cache when interlace output is required
} else if (baton->file_out == "__png" || (baton->file_out == "__input" && inputImageType == PNG)) { VipsImage *cached = vips_image_new();
// Write PNG to buffer if (baton->progressive) {
if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) { if (vips_tilecache(colourspaced, &cached, "threaded", TRUE, "persistent", TRUE, "max_tiles", -1, NULL)) {
return resize_error(baton, sharpened); 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", baton->quality, NULL)) {
return resize_error(baton, sharpened);
}
} 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);
}
} 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);
}
} 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);
}
} 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);
} }
} else { } else {
(baton->err).append("Unsupported output " + baton->file_out); vips_copy(colourspaced, &cached, NULL);
} }
g_object_unref(sharpened); g_object_unref(colourspaced);
// Output
VipsImage *output = cached;
if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == JPEG)) {
// Write JPEG to buffer
if (vips_jpegsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == PNG)) {
// Write PNG to buffer
if (vips_pngsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == WEBP)) {
// Write WEBP to buffer
if (vips_webpsave_buffer(output, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
}
} else if (is_jpeg(baton->output)) {
// Write JPEG to file
if (vips_jpegsave(output, baton->output.c_str(), "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} else if (is_png(baton->output)) {
// Write PNG to file
if (vips_pngsave(output, baton->output.c_str(), "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
return resize_error(baton, output);
}
} else if (is_webp(baton->output)) {
// Write WEBP to file
if (vips_webpsave(output, baton->output.c_str(), "strip", TRUE, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
}
} else if (is_tiff(baton->output)) {
// Write TIFF to file
if (vips_tiffsave(output, baton->output.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
return resize_error(baton, output);
}
} else {
(baton->err).append("Unsupported output " + baton->output);
}
g_object_unref(output);
// Clean up libvips' per-request data and threads
vips_error_clear();
vips_thread_shutdown(); vips_thread_shutdown();
} }
void HandleOKCallback () { void HandleOKCallback () {
NanScope(); NanScope();
Handle<Value> argv[2] = { NanNull(), NanNull() }; Handle<Value> argv[3] = { NanNull(), NanNull(), NanNull() };
if (!baton->err.empty()) { if (!baton->err.empty()) {
// Error // Error
argv[0] = NanNew<String>(baton->err.data(), baton->err.size()); argv[0] = NanNew<String>(baton->err.data(), baton->err.size());
} else if (baton->buffer_out_len > 0) { } else {
// Buffer // Info Object
argv[1] = NanNewBufferHandle((char *)baton->buffer_out, baton->buffer_out_len); Local<Object> info = NanNew<Object>();
g_free(baton->buffer_out); info->Set(NanNew<String>("width"), NanNew<Number>(baton->width));
info->Set(NanNew<String>("height"), NanNew<Number>(baton->height));
if (baton->buffer_out_len > 0) {
// Buffer
argv[1] = NanNewBufferHandle((char *)baton->buffer_out, baton->buffer_out_len);
g_free(baton->buffer_out);
argv[2] = info;
} else {
// File
argv[1] = info;
}
} }
delete baton; delete baton;
callback->Call(2, argv);
// Decrement processing task counter
g_atomic_int_dec_and_test(&counter_process);
// Return to JavaScript
callback->Call(3, argv);
} }
private: private:
resize_baton* baton; resize_baton* baton;
}; };
/*
resize(options, output, callback)
*/
NAN_METHOD(resize) { NAN_METHOD(resize) {
NanScope(); NanScope();
// V8 objects are converted to non-V8 types held in the baton struct
resize_baton *baton = new resize_baton; resize_baton *baton = new resize_baton;
baton->file_in = *String::Utf8Value(args[0]->ToString()); Local<Object> options = args[0]->ToObject();
if (args[1]->IsObject()) {
Local<Object> buffer = args[1]->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_len = Buffer::Length(buffer);
baton->buffer_in = Buffer::Data(buffer); baton->buffer_in = Buffer::Data(buffer);
} }
baton->file_out = *String::Utf8Value(args[2]->ToString()); // Output image dimensions
baton->width = args[3]->Int32Value(); baton->width = options->Get(NanNew<String>("width"))->Int32Value();
baton->height = args[4]->Int32Value(); baton->height = options->Get(NanNew<String>("height"))->Int32Value();
Local<String> canvas = args[5]->ToString(); // Canvas options
if (canvas->Equals(NanSymbol("c"))) { Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
if (canvas->Equals(NanNew<String>("c"))) {
baton->crop = true; baton->crop = true;
} else if (canvas->Equals(NanSymbol("w"))) { } else if (canvas->Equals(NanNew<String>("w"))) {
baton->extend = VIPS_EXTEND_WHITE; baton->extend = VIPS_EXTEND_WHITE;
} else if (canvas->Equals(NanSymbol("b"))) { } else if (canvas->Equals(NanNew<String>("b"))) {
baton->extend = VIPS_EXTEND_BLACK; baton->extend = VIPS_EXTEND_BLACK;
} else if (canvas->Equals(NanSymbol("m"))) { } else if (canvas->Equals(NanNew<String>("m"))) {
baton->crop = true;
baton->max = true; baton->max = true;
} }
baton->sharpen = args[6]->BooleanValue(); // Other options
baton->progressive = args[7]->BooleanValue(); baton->gravity = options->Get(NanNew<String>("gravity"))->Int32Value();
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM; baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
baton->quality = args[9]->Int32Value(); baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
baton->compressionLevel = args[10]->Int32Value(); baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
baton->without_enlargement = options->Get(NanNew<String>("withoutEnlargement"))->BooleanValue();
NanCallback *callback = new NanCallback(args[11].As<v8::Function>()); 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->output = *String::Utf8Value(options->Get(NanNew<String>("output"))->ToString());
// Join queue for worker thread
NanCallback *callback = new NanCallback(args[1].As<v8::Function>());
NanAsyncQueueWorker(new ResizeWorker(callback, baton)); NanAsyncQueueWorker(new ResizeWorker(callback, baton));
// Increment queued task counter
g_atomic_int_inc(&counter_queue);
NanReturnUndefined(); NanReturnUndefined();
} }
/*
Get and set cache memory and item limits
*/
NAN_METHOD(cache) { NAN_METHOD(cache) {
NanScope(); NanScope();
// Set cache limit // Set cache memory limit
if (args[0]->IsInt32()) { if (args[0]->IsInt32()) {
vips_cache_set_max_mem(args[0]->Int32Value() * 1048576); vips_cache_set_max_mem(args[0]->Int32Value() * 1048576);
} }
// Set cache items limit
if (args[1]->IsInt32()) {
vips_cache_set_max(args[1]->Int32Value());
}
// Get cache statistics // Get cache statistics
Local<Object> cache = NanNew<Object>(); Local<Object> cache = NanNew<Object>();
cache->Set(NanSymbol("current"), NanNew<Number>(vips_tracked_get_mem() / 1048576)); cache->Set(NanNew<String>("current"), NanNew<Number>(vips_tracked_get_mem() / 1048576));
cache->Set(NanSymbol("high"), NanNew<Number>(vips_tracked_get_mem_highwater() / 1048576)); cache->Set(NanNew<String>("high"), NanNew<Number>(vips_tracked_get_mem_highwater() / 1048576));
cache->Set(NanSymbol("limit"), NanNew<Number>(vips_cache_get_max_mem() / 1048576)); cache->Set(NanNew<String>("memory"), NanNew<Number>(vips_cache_get_max_mem() / 1048576));
cache->Set(NanNew<String>("items"), NanNew<Number>(vips_cache_get_max()));
NanReturnValue(cache); NanReturnValue(cache);
} }
/*
Get internal counters (queued tasks, processing tasks)
*/
NAN_METHOD(counters) {
NanScope();
Local<Object> counters = NanNew<Object>();
counters->Set(NanNew<String>("queue"), NanNew<Number>(counter_queue));
counters->Set(NanNew<String>("process"), NanNew<Number>(counter_process));
NanReturnValue(counters);
}
static void at_exit(void* arg) { static void at_exit(void* arg) {
NanScope(); NanScope();
vips_shutdown(); vips_shutdown();
@@ -399,8 +764,14 @@ extern "C" void init(Handle<Object> target) {
NanScope(); NanScope();
vips_init(""); vips_init("");
AtExit(at_exit); AtExit(at_exit);
// Set libvips operation cache limits
vips_cache_set_max_mem(100 * 1048576); // 100 MB
vips_cache_set_max(500); // 500 operations
// Methods available to JavaScript
NODE_SET_METHOD(target, "metadata", metadata);
NODE_SET_METHOD(target, "resize", resize); NODE_SET_METHOD(target, "resize", resize);
NODE_SET_METHOD(target, "cache", cache); NODE_SET_METHOD(target, "cache", cache);
NODE_SET_METHOD(target, "counters", counters);
} }
NODE_MODULE(sharp, init) NODE_MODULE(sharp, init)

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 width = 720;
var height = 480; 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) { async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
var start = new Date().getTime(); var start = new Date().getTime();
async.times(parallelism, async.times(parallelism,
@@ -28,5 +32,6 @@ async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
} }
); );
}, function() { }, function() {
clearInterval(timer);
console.dir(sharp.cache()); console.dir(sharp.cache());
}); });

View File

@@ -111,7 +111,7 @@ async.series({
}).add("sharp-buffer-file", { }).add("sharp-buffer-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpgBuffer).resize(width, height).write(outputJpg, function(err) { sharp(inputJpgBuffer).resize(width, height).toFile(outputJpg, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -134,7 +134,7 @@ async.series({
}).add("sharp-file-file", { }).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputJpg).resize(width, height).write(outputJpg, function(err) { sharp(inputJpg).resize(width, height).toFile(outputJpg, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -142,6 +142,17 @@ async.series({
} }
}); });
} }
}).add("sharp-stream-stream", {
defer: true,
fn: function(deferred) {
var readable = fs.createReadStream(inputJpg);
var writable = fs.createWriteStream(outputJpg);
writable.on('finish', function() {
deferred.resolve();
});
var pipeline = sharp().resize(width, height);
readable.pipe(pipeline).pipe(writable);
}
}).add("sharp-file-buffer", { }).add("sharp-file-buffer", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
@@ -154,6 +165,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", { }).add("sharp-file-buffer-sharpen", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
@@ -166,6 +185,54 @@ async.series({
} }
}); });
} }
}).add("sharp-file-buffer-bicubic", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.bicubic).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-nohalo", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.nohalo).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-locallyBoundedBicubic", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.locallyBoundedBicubic).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-vertexSplitQuadraticBasisSpline", {
defer: true,
fn: function(deferred) {
sharp(inputJpg).resize(width, height).interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline).toBuffer(function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-progressive", { }).add("sharp-file-buffer-progressive", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
@@ -178,6 +245,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", { }).add("sharp-file-buffer-sequentialRead", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
@@ -251,7 +330,7 @@ async.series({
}).add("sharp-buffer-file", { }).add("sharp-buffer-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputPngBuffer).resize(width, height).write(outputPng, function(err) { sharp(inputPngBuffer).resize(width, height).toFile(outputPng, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -274,7 +353,7 @@ async.series({
}).add("sharp-file-file", { }).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputPng).resize(width, height).write(outputPng, function(err) { sharp(inputPng).resize(width, height).toFile(outputPng, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -318,18 +397,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) { }).on("cycle", function(event) {
console.log(" png " + String(event.target)); console.log(" png " + String(event.target));
}).on("complete", function() { }).on("complete", function() {
@@ -341,7 +408,7 @@ async.series({
(new Benchmark.Suite("webp")).add("sharp-buffer-file", { (new Benchmark.Suite("webp")).add("sharp-buffer-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputWebpBuffer).resize(width, height).write(outputWebp, function(err) { sharp(inputWebpBuffer).resize(width, height).toFile(outputWebp, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -364,7 +431,7 @@ async.series({
}).add("sharp-file-file", { }).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputWebp).resize(width, height).write(outputWebp, function(err) { sharp(inputWebp).resize(width, height).toFile(outputWebp, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -396,18 +463,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) { }).on("cycle", function(event) {
console.log("webp " + String(event.target)); console.log("webp " + String(event.target));
}).on("complete", function() { }).on("complete", function() {
@@ -418,7 +473,7 @@ async.series({
(new Benchmark.Suite("tiff")).add("sharp-file-file", { (new Benchmark.Suite("tiff")).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputTiff).resize(width, height).write(outputTiff, function(err) { sharp(inputTiff).resize(width, height).toFile(outputTiff, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -429,18 +484,7 @@ async.series({
}).add("sharp-file-file-sharpen", { }).add("sharp-file-file-sharpen", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputTiff).resize(width, height).sharpen().write(outputTiff, function(err) { sharp(inputTiff).resize(width, height).sharpen().toFile(outputTiff, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-file-sequentialRead", {
defer: true,
fn: function(deferred) {
sharp(inputTiff).sequentialRead().resize(width, height).write(outputTiff, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -458,7 +502,7 @@ async.series({
(new Benchmark.Suite("gif")).add("sharp-file-file", { (new Benchmark.Suite("gif")).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp(inputGif).resize(width, height).write(outputTiff, function(err) { sharp(inputGif).resize(width, height).toFile(outputTiff, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -469,7 +513,7 @@ async.series({
}).add("sharp-file-file-sharpen", { }).add("sharp-file-file-sharpen", {
defer: true, defer: true,
fn: function(deferred) { 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) { if (err) {
throw err; throw err;
} else { } else {
@@ -480,7 +524,7 @@ async.series({
}).add("sharp-file-file-sequentialRead", { }).add("sharp-file-file-sequentialRead", {
defer: true, defer: true,
fn: function(deferred) { 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) { if (err) {
throw err; throw err;
} else { } else {

View File

@@ -1,6 +1,10 @@
/*jslint node: true */
/*jslint es5: true */
'use strict';
var sharp = require("../index"); var sharp = require("../index");
var fs = require("fs");
var path = require("path"); var path = require("path");
var imagemagick = require("imagemagick");
var assert = require("assert"); var assert = require("assert");
var async = require("async"); var async = require("async");
@@ -12,74 +16,74 @@ 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 inputTiff = path.join(fixturesPath, "G31D.TIF"); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
var outputTiff = path.join(fixturesPath, "output.tiff"); var outputTiff = path.join(fixturesPath, "output.tiff");
var inputJpgWithExif = path.join(fixturesPath, "Landscape_8.jpg"); // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
var inputPng = path.join(fixturesPath, "50020484-00001.png"); // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
var inputWebp = path.join(fixturesPath, "4.webp"); // http://www.gstatic.com/webp/gallery/4.webp
var inputGif = path.join(fixturesPath, "Crash_test.gif"); // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
// Ensure cache limits can be set
sharp.cache(0); // Disable
sharp.cache(50, 500); // 50MB, 500 items
async.series([ async.series([
// Resize with exact crop // Resize with exact crop
function(done) { function(done) {
sharp(inputJpg).resize(320, 240).write(outputJpg, function(err) { sharp(inputJpg).resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(320, info.width);
assert.strictEqual(320, features.width); assert.strictEqual(240, info.height);
assert.strictEqual(240, features.height); done();
done();
});
}); });
}, },
// Resize to fixed width // Resize to fixed width
function(done) { function(done) {
sharp(inputJpg).resize(320).write(outputJpg, function(err) { sharp(inputJpg).resize(320).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(320, info.width);
assert.strictEqual(320, features.width); assert.strictEqual(261, info.height);
assert.strictEqual(261, features.height); done();
done();
});
}); });
}, },
// Resize to fixed height // Resize to fixed height
function(done) { function(done) {
sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) { sharp(inputJpg).resize(null, 320).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(391, info.width);
assert.strictEqual(391, features.width); assert.strictEqual(320, info.height);
assert.strictEqual(320, features.height); done();
done();
});
}); });
}, },
// Identity transform // Identity transform
function(done) { function(done) {
sharp(inputJpg).write(outputJpg, function(err) { sharp(inputJpg).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(2725, info.width);
assert.strictEqual(2725, features.width); assert.strictEqual(2225, info.height);
assert.strictEqual(2225, features.height); done();
done();
});
}); });
}, },
// Upscale // Upscale
function(done) { function(done) {
sharp(inputJpg).resize(3000).write(outputJpg, function(err) { sharp(inputJpg).resize(3000).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(3000, info.width);
assert.strictEqual(3000, features.width); assert.strictEqual(2449, info.height);
assert.strictEqual(2449, features.height); done();
done();
});
}); });
}, },
// Quality // Quality
function(done) { function(done) {
sharp(inputJpg).resize(320, 240).quality(70).jpeg(function(err, buffer70) { sharp(inputJpg).resize(320, 240).quality(70).toBuffer(function(err, buffer70) {
if (err) throw err; if (err) throw err;
sharp(inputJpg).resize(320, 240).jpeg(function(err, buffer80) { sharp(inputJpg).resize(320, 240).toBuffer(function(err, buffer80) {
if (err) throw err; if (err) throw err;
sharp(inputJpg).resize(320, 240).quality(90).jpeg(function(err, buffer90) { sharp(inputJpg).resize(320, 240).quality(90).toBuffer(function(err, buffer90) {
assert(buffer70.length < buffer80.length); assert(buffer70.length < buffer80.length);
assert(buffer80.length < buffer90.length); assert(buffer80.length < buffer90.length);
done(); done();
@@ -89,61 +93,457 @@ async.series([
}, },
// TIFF with dimensions known to cause rounding errors // TIFF with dimensions known to cause rounding errors
function(done) { function(done) {
sharp(inputTiff).resize(240, 320).embedBlack().write(outputJpg, function(err) { sharp(inputTiff).resize(240, 320).embedBlack().jpeg().toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(240, info.width);
assert.strictEqual(240, features.width); assert.strictEqual(320, info.height);
assert.strictEqual(320, features.height); done();
done();
});
}); });
}, },
function(done) { function(done) {
sharp(inputTiff).resize(240, 320).write(outputJpg, function(err) { sharp(inputTiff).resize(240, 320).jpeg().toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(240, info.width);
assert.strictEqual(240, features.width); assert.strictEqual(320, info.height);
assert.strictEqual(320, features.height); done();
done();
});
}); });
}, },
// Resize to max width or height considering ratio (landscape) // Resize to max width or height considering ratio (landscape)
function(done) { function(done) {
sharp(inputJpg).resize(320, 320).max().write(outputJpg, function(err) { sharp(inputJpg).resize(320, 320).max().toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(320, info.width);
assert.strictEqual(320, features.width); assert.strictEqual(261, info.height);
assert.strictEqual(261, features.height); done();
done();
});
}); });
}, },
// Resize to max width or height considering ratio (portrait) // Resize to max width or height considering ratio (portrait)
function(done) { function(done) {
sharp(inputTiff).resize(320, 320).max().write(outputJpg, function(err) { sharp(inputTiff).resize(320, 320).max().jpeg().toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
if (err) throw err; assert.strictEqual(243, info.width);
assert.strictEqual(243, features.width); assert.strictEqual(320, info.height);
assert.strictEqual(320, features.height); done();
done();
});
}); });
}, },
// Attempt to resize to max but only provide one dimension, so should default to crop // Attempt to resize to max but only provide one dimension, so should default to crop
function(done) { function(done) {
sharp(inputJpg).resize(320).max().write(outputJpg, function(err) { sharp(inputJpg).resize(320).max().toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) { assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
},
// Attempt to output to input, should fail
function(done) {
sharp(inputJpg).toFile(inputJpg, function(err) {
assert(!!err);
done();
});
},
// Rotate by 90 degrees, respecting output input size
function(done) {
sharp(inputJpg).rotate(90).resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Input image has Orientation EXIF tag but do not rotate output
function(done) {
sharp(inputJpgWithExif).resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(426, info.height);
done();
});
},
// Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate
function(done) {
sharp(inputJpgWithExif).rotate().resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Attempt to auto-rotate using image that has no EXIF
function(done) {
sharp(inputJpg).rotate().resize(320).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(261, info.height);
done();
});
},
// Rotate to an invalid angle, should fail
function(done) {
var fail = false;
try {
sharp(inputJpg).rotate(1);
fail = true;
} catch (e) {}
assert(!fail);
done();
},
// Do not enlarge the output if the input width is already less than the output width
function(done) {
sharp(inputJpg).resize(2800).withoutEnlargement().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
},
// Do not enlarge the output if the input height is already less than the output height
function(done) {
sharp(inputJpg).resize(null, 2300).withoutEnlargement().toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(2725, info.width);
assert.strictEqual(2225, info.height);
done();
});
},
// Promises/A+
function(done) {
sharp(inputJpg).resize(320, 240).toBuffer().then(function(data) {
sharp(data).toBuffer(function(err, data, info) {
if (err) throw err; if (err) throw err;
assert.strictEqual(320, features.width); assert.strictEqual(true, data.length > 0);
assert.strictEqual(261, features.height); assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
}).catch(function(err) {
throw err;
});
},
// Empty Buffer, should fail
function(done) {
var fail = false;
try {
sharp(new Buffer(0));
fail = true;
} catch (e) {}
assert(!fail);
done();
},
// Check colour space conversion occurs from TIFF to WebP (this used to segfault)
function(done) {
sharp(inputTiff).webp().toBuffer().then(function() {
done();
});
},
// Interpolation: bilinear
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.bilinear).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Interpolation: bicubic
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.bicubic).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Interpolation: nohalo
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.nohalo).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Interpolation: locally bounded bicubic (LBB)
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.locallyBoundedBicubic).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// Interpolation: vertex split quadratic basis spline (VSQBS)
function(done) {
sharp(inputJpg).resize(320, 240).interpolateWith(sharp.interpolator.vertexSplitQuadraticBasisSpline).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
},
// File-Stream
function(done) {
var writable = fs.createWriteStream(outputJpg);
writable.on('finish', function() {
sharp(outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(outputJpg);
done(); done();
}); });
}); });
sharp(inputJpg).resize(320, 240).pipe(writable);
},
// Buffer-Stream
function(done) {
var inputJpgBuffer = fs.readFileSync(inputJpg);
var writable = fs.createWriteStream(outputJpg);
writable.on('finish', function() {
sharp(outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(outputJpg);
done();
});
});
sharp(inputJpgBuffer).resize(320, 240).pipe(writable);
},
// Stream-File
function(done) {
var readable = fs.createReadStream(inputJpg);
var pipeline = sharp().resize(320, 240).toFile(outputJpg, function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(outputJpg);
done();
});
readable.pipe(pipeline);
},
// Stream-Buffer
function(done) {
var readable = fs.createReadStream(inputJpg);
var pipeline = sharp().resize(320, 240).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
done();
});
readable.pipe(pipeline);
},
// Stream-Stream
function(done) {
var readable = fs.createReadStream(inputJpg);
var writable = fs.createWriteStream(outputJpg);
writable.on('finish', function() {
sharp(outputJpg).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(320, info.width);
assert.strictEqual(240, info.height);
fs.unlinkSync(outputJpg);
done();
});
});
var pipeline = sharp().resize(320, 240);
readable.pipe(pipeline).pipe(writable);
},
// Crop, gravity=north
function(done) {
sharp(inputJpg).resize(320, 80).crop(sharp.gravity.north).toFile(path.join(fixturesPath, 'output.gravity-north.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
},
// Crop, gravity=east
function(done) {
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.east).toFile(path.join(fixturesPath, 'output.gravity-east.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// Crop, gravity=south
function(done) {
sharp(inputJpg).resize(320, 80).crop(sharp.gravity.south).toFile(path.join(fixturesPath, 'output.gravity-south.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
},
// Crop, gravity=west
function(done) {
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.west).toFile(path.join(fixturesPath, 'output.gravity-west.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// Crop, gravity=center
function(done) {
sharp(inputJpg).resize(320, 80).crop(sharp.gravity.center).toFile(path.join(fixturesPath, 'output.gravity-center.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(320, info.width);
assert.strictEqual(80, info.height);
done();
});
},
// Crop, gravity=centre
function(done) {
sharp(inputJpg).resize(80, 320).crop(sharp.gravity.centre).toFile(path.join(fixturesPath, 'output.gravity-centre.jpg'), function(err, info) {
if (err) throw err;
assert.strictEqual(80, info.width);
assert.strictEqual(320, info.height);
done();
});
},
// Metadata - JPEG
function(done) {
sharp(inputJpg).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual('undefined', typeof metadata.orientation);
done();
});
},
// Metadata - JPEG with EXIF
function(done) {
sharp(inputJpgWithExif).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(450, metadata.width);
assert.strictEqual(600, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
assert.strictEqual(8, metadata.orientation);
done();
});
},
// Metadata - TIFF
function(done) {
sharp(inputTiff).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('tiff', metadata.format);
assert.strictEqual(2464, metadata.width);
assert.strictEqual(3248, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
done();
});
},
// Metadata - PNG
function(done) {
sharp(inputPng).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('png', metadata.format);
assert.strictEqual(2809, metadata.width);
assert.strictEqual(2074, metadata.height);
assert.strictEqual('b-w', metadata.space);
assert.strictEqual(1, metadata.channels);
done();
});
},
// Metadata - WebP
function(done) {
sharp(inputWebp).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('webp', metadata.format);
assert.strictEqual(1024, metadata.width);
assert.strictEqual(772, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
done();
});
},
// Metadata - GIF (via libmagick)
function(done) {
sharp(inputGif).metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('magick', metadata.format);
assert.strictEqual(800, metadata.width);
assert.strictEqual(533, metadata.height);
assert.strictEqual(3, metadata.channels);
done();
});
},
// Metadata - Promise
function(done) {
sharp(inputJpg).metadata().then(function(metadata) {
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
done();
});
},
// Metadata - Stream
function(done) {
var readable = fs.createReadStream(inputJpg);
var pipeline = sharp().metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
done();
});
readable.pipe(pipeline);
},
// Get metadata then resize to half width
function(done) {
var image = sharp(inputJpg);
image.metadata(function(err, metadata) {
if (err) throw err;
assert.strictEqual('jpeg', metadata.format);
assert.strictEqual(2725, metadata.width);
assert.strictEqual(2225, metadata.height);
assert.strictEqual('srgb', metadata.space);
assert.strictEqual(3, metadata.channels);
image.resize(metadata.width / 2).toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual(true, data.length > 0);
assert.strictEqual(1362, info.width);
assert.strictEqual(1112, info.height);
done();
});
});
},
// Verify internal counters
function(done) {
var counters = sharp.counters();
assert.strictEqual(0, counters.queue);
assert.strictEqual(0, counters.process);
done();
} }
]); ]);