Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8acb0ed5d0 | ||
|
|
430e04d894 | ||
|
|
012edb4379 | ||
|
|
11ead360a9 | ||
|
|
84a059d7e3 | ||
|
|
b1b070ae5c | ||
|
|
4eb910fec9 | ||
|
|
f0a9d82bf7 | ||
|
|
6d20a1ca81 | ||
|
|
eca2787213 | ||
|
|
8d146accf3 | ||
|
|
b635d015cd | ||
|
|
261a90c8a2 | ||
|
|
4ae22b3425 | ||
|
|
c9aa9c7723 | ||
|
|
5e0b5969da | ||
|
|
ae6d5e69b1 | ||
|
|
5ccc2bca97 | ||
|
|
46b701c85c | ||
|
|
7319533969 | ||
|
|
9a05684302 | ||
|
|
906311d403 | ||
|
|
4de9a2435f | ||
|
|
a94dd2b354 | ||
|
|
bc3311cbad | ||
|
|
7b03eb89d7 | ||
|
|
15a519ebd9 | ||
|
|
3f8e9f6487 | ||
|
|
39688371a8 | ||
|
|
e32faac17a | ||
|
|
6c96bd0d37 | ||
|
|
6b5f2028b7 | ||
|
|
276ba5228b | ||
|
|
ad7735a0a6 | ||
|
|
88edad3fae | ||
|
|
308d1971d8 | ||
|
|
6622045172 | ||
|
|
f68ba8ea57 | ||
|
|
2e427bb28a | ||
|
|
efc7504961 | ||
|
|
8118613fa0 | ||
|
|
eb6a221cee | ||
|
|
acdfe02502 | ||
|
|
2e106f8e2e | ||
|
|
10496881f1 | ||
|
|
e275f6f5dd | ||
|
|
d635c297a2 | ||
|
|
817c0a2a5a | ||
|
|
92fd34c627 | ||
|
|
43086cf134 | ||
|
|
e607bac31c | ||
|
|
f8338e7c4f | ||
|
|
8322b442e0 | ||
|
|
afc51df4d8 | ||
|
|
e3a70c1075 | ||
|
|
e3ee2b2976 | ||
|
|
cb285a6fb3 | ||
|
|
aed3ca63b3 | ||
|
|
cbcf5e0dcc | ||
|
|
59f5c2d31b | ||
|
|
d1b47ef419 | ||
|
|
0954ca6adf | ||
|
|
c9d7f43bd9 | ||
|
|
481741315d | ||
|
|
cae1dbdb89 | ||
|
|
200d5a9312 | ||
|
|
e9ca25cb45 | ||
|
|
33f24d41e7 | ||
|
|
45d5f12a63 | ||
|
|
8785ca4331 | ||
|
|
1ecdf97bdb | ||
|
|
3703ee41aa | ||
|
|
c8f023d8ba | ||
|
|
fe773733cd | ||
|
|
19bec9346e | ||
|
|
9bd335079f | ||
|
|
b6dc179551 | ||
|
|
e96fd8b9de | ||
|
|
6a2816e917 | ||
|
|
06d88de5a2 | ||
|
|
a292e1fe8e | ||
|
|
08bb35e7af | ||
|
|
0e89a5bbf2 | ||
|
|
2a0d79a78b | ||
|
|
764b57022b | ||
|
|
5f61331d1a | ||
|
|
d0e6a4c0f3 | ||
|
|
f99e42d447 | ||
|
|
9b4387be97 | ||
|
|
9c3631ecb7 | ||
|
|
31ca68fb14 | ||
|
|
d5d85a8697 | ||
|
|
ae9a8b0f57 | ||
|
|
0899252a72 | ||
|
|
16551bc058 | ||
|
|
e9d196f696 | ||
|
|
2f97d04dfa | ||
|
|
e4ca8f44ec | ||
|
|
6b3dc1e350 | ||
|
|
7ffcdb79e0 | ||
|
|
10ce7c6693 |
5
.gitignore
vendored
@@ -12,7 +12,4 @@ logs
|
||||
results
|
||||
build
|
||||
node_modules
|
||||
tests/output.jpg
|
||||
tests/output.png
|
||||
|
||||
npm-debug.log
|
||||
tests/fixtures/output.*
|
||||
|
||||
18
.npmignore
Normal file
@@ -0,0 +1,18 @@
|
||||
lib-cov
|
||||
*.seed
|
||||
*.log
|
||||
*.csv
|
||||
*.dat
|
||||
*.out
|
||||
*.pid
|
||||
*.gz
|
||||
|
||||
pids
|
||||
logs
|
||||
results
|
||||
build
|
||||
node_modules
|
||||
|
||||
.gitignore
|
||||
tests
|
||||
.travis.yml
|
||||
17
.travis.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.11"
|
||||
before_install:
|
||||
- sudo add-apt-repository ppa:lyrasis/precise-backports -y
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -qq automake gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff4-dev libexif-dev libxml2-dev swig graphicsmagick libmagick++-dev
|
||||
- git clone https://github.com/jcupitt/libvips.git
|
||||
- cd libvips
|
||||
- git checkout 7.38
|
||||
- ./bootstrap.sh
|
||||
- ./configure --enable-debug=no --enable-cxx=yes --without-orc --without-python
|
||||
- make
|
||||
- sudo make install
|
||||
- sudo ldconfig
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
329
README.md
@@ -1,58 +1,81 @@
|
||||
# sharp
|
||||
|
||||
_adj_
|
||||
* [Installation](https://github.com/lovell/sharp#installation)
|
||||
* [Usage examples](https://github.com/lovell/sharp#usage-examples)
|
||||
* [API](https://github.com/lovell/sharp#api)
|
||||
* [Testing](https://github.com/lovell/sharp#testing)
|
||||
* [Performance](https://github.com/lovell/sharp#performance)
|
||||
* [Licence](https://github.com/lovell/sharp#licence)
|
||||
|
||||
1. clearly defined; distinct: a sharp photographic image.
|
||||
2. quick, brisk, or spirited.
|
||||
3. shrewd or astute: a sharp bargainer.
|
||||
4. (Informal.) very stylish: a sharp dresser; a sharp jacket.
|
||||
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 JPEG and PNG images to smaller JPEG and PNG 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_ and Promises/A+ are supported.
|
||||
|
||||
The performance of JPEG resizing is typically 15x-25x 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.
|
||||
|
||||
Under the hood you'll find 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.
|
||||
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/).
|
||||
|
||||
## Prerequisites
|
||||
Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly fluent.
|
||||
|
||||
* Node.js v0.8+
|
||||
* [libvips](https://github.com/jcupitt/libvips) v7.38+
|
||||
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.
|
||||
|
||||
For the sharpest results, please compile libvips from source.
|
||||
|
||||
## Install
|
||||
## Installation
|
||||
|
||||
npm install sharp
|
||||
|
||||
## Usage
|
||||
### Prerequisites
|
||||
|
||||
var sharp = require("sharp");
|
||||
* Node.js v0.10+
|
||||
* [libvips](https://github.com/jcupitt/libvips) v7.38.5+
|
||||
|
||||
### resize(input, output, width, height, [options], callback)
|
||||
_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).
|
||||
|
||||
Scale and crop to `width` x `height` calling `callback` when complete.
|
||||
### Install libvips on Mac OS
|
||||
|
||||
`input` can either be a filename String or a Buffer. When using a filename libvips will `mmap` the file for improved performance.
|
||||
brew install homebrew/science/vips --with-webp --with-graphicsmagick
|
||||
|
||||
`output` can either be a filename String or one of `sharp.buffer.jpeg` or `sharp.buffer.png` to pass a Buffer containing image data to `callback`.
|
||||
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:
|
||||
|
||||
`width` is the Number of pixels wide the resultant image should be.
|
||||
brew link gettext --force
|
||||
|
||||
`height` is the Number of pixels high the resultant image should be.
|
||||
### Install libvips on Ubuntu Linux
|
||||
|
||||
`options` is optional, and can contain one or more of:
|
||||
#### Ubuntu 14.x
|
||||
|
||||
* `canvas` can be one of `sharp.canvas.crop`, `sharp.canvas.embedWhite` or `sharp.canvas.embedBlack`. Defaults to `sharp.canvas.crop`.
|
||||
* `sharpen` when set to true will perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
|
||||
* `progressive` when set will use progressive (interlace) scan for the output. This typically reduces performance by 30%.
|
||||
* `sequentialRead` is an advanced setting that, when set, switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
|
||||
sudo apt-get install libvips-dev
|
||||
|
||||
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data when a Buffer is requested.
|
||||
#### Ubuntu 13.x
|
||||
|
||||
### Examples
|
||||
Compiling from source is recommended:
|
||||
|
||||
sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev libexif-dev libxml2-dev swig
|
||||
git clone https://github.com/jcupitt/libvips.git
|
||||
cd libvips
|
||||
git checkout 7.38
|
||||
./bootstrap.sh
|
||||
./configure --enable-debug=no --enable-cxx=yes --without-python --without-orc
|
||||
make
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
|
||||
#### Ubuntu 12.x
|
||||
|
||||
Requires `libtiff4-dev` instead of `libtiff5-dev` and has [a bug](https://bugs.launchpad.net/ubuntu/+source/libwebp/+bug/1108731) in the libwebp package. Work around these problems by running these commands first:
|
||||
|
||||
sudo add-apt-repository ppa:lyrasis/precise-backports
|
||||
sudo apt-get update
|
||||
sudo apt-get install libtiff4-dev
|
||||
|
||||
Then follow Ubuntu 13.x instructions.
|
||||
|
||||
## Usage examples
|
||||
|
||||
```javascript
|
||||
sharp.resize("input.jpg", "output.jpg", 300, 200, function(err) {
|
||||
var sharp = require('sharp');
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp('input.jpg').resize(300, 200).toFile('output.jpg', function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
@@ -62,96 +85,238 @@ sharp.resize("input.jpg", "output.jpg", 300, 200, function(err) {
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp.resize("input.jpg", sharp.buffer.jpeg, 300, 200, {progressive: true}, function(err, buffer) {
|
||||
sharp('input.jpg').rotate().resize(null, 200).progressive().toBuffer(function(err, outputBuffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// buffer contains progressive JPEG image data
|
||||
// outputBuffer contains 200px high progressive JPEG image data, auto-rotated using EXIF Orientation tag
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp.resize("input.jpg", sharp.buffer.png, 300, 200, {sharpen: true}, function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// buffer contains sharpened PNG image data (converted from JPEG)
|
||||
sharp('input.png').rotate(180).resize(300).sharpen().quality(90).webp().then(function(outputBuffer) {
|
||||
// outputBuffer contains 300px wide, upside down, sharpened, 90% quality WebP image data
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp.resize(buffer, "output.jpg", 200, 300, {canvas: sharp.canvas.embedWhite}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// output.jpg is a 200 pixels wide and 300 pixels high image containing a scaled version
|
||||
// of the image data contained in buffer embedded on a white canvas
|
||||
sharp(inputBuffer).resize(200, 300).bicubicInterpolation().embedWhite().toFile('output.tiff').then(function() {
|
||||
// output.tiff is a 200 pixels wide and 300 pixels high image containing a bicubic scaled
|
||||
// version, embedded on a white canvas, of the image data in buffer
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp.resize("input.jpg", sharp.buffer.jpeg, 200, 300, {canvas: sharp.canvas.embedBlack}, function(err, buffer) {
|
||||
sharp('input.gif').resize(200, 300).embedBlack().webp(function(err, outputBuffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
// buffer contains JPEG image data of a 200 pixels wide and 300 pixels high image
|
||||
// containing a scaled version of input.png embedded on a black canvas
|
||||
// outputBuffer contains WebP image data of a 200 pixels wide and 300 pixels high
|
||||
// containing a scaled version, embedded on a black canvas, of input.gif
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
sharp(inputBuffer).resize(200, 200).max().jpeg().then(function(outputBuffer) {
|
||||
// outputBuffer contains JPEG image data no wider than 200 pixels and no higher
|
||||
// than 200 pixels regardless of the inputBuffer image dimensions
|
||||
});
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### sharp(input)
|
||||
|
||||
Constructor to which further methods are chained. `input` can be one of:
|
||||
|
||||
* Buffer containing JPEG, PNG or WebP image data, or
|
||||
* String containing the filename of an image, with most major formats supported.
|
||||
|
||||
### 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.
|
||||
|
||||
`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 the resized image to the exact size specified, the default behaviour.
|
||||
|
||||
### max()
|
||||
|
||||
Preserving aspect ratio, resize the image to the maximum width or height specified.
|
||||
|
||||
Both `width` and `height` must be provided via `resize` otherwise the behaviour will default to `crop`.
|
||||
|
||||
### embedWhite()
|
||||
|
||||
Embed the resized image on a white background of the exact size specified.
|
||||
|
||||
### embedBlack()
|
||||
|
||||
Embed the resized image on a black background of the exact size specified.
|
||||
|
||||
### rotate([angle])
|
||||
|
||||
Rotate the output image by either an explicit angle or auto-orient based on the EXIF `Orientation` tag. Mirroring is not supported.
|
||||
|
||||
`angle`, if present, is a Number with a value of `0`, `90`, `180` or `270`.
|
||||
|
||||
Use this method without `angle` to determine the angle from EXIF data.
|
||||
|
||||
### withoutEnlargement()
|
||||
|
||||
Do not enlarge the output image if the input image width *or* height are already less than the required dimensions.
|
||||
|
||||
This is equivalent to GraphicsMagick's `>` geometry option: "change the dimensions of the image only if its width or height exceeds the geometry specification".
|
||||
|
||||
### sharpen()
|
||||
|
||||
Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
|
||||
|
||||
### bilinearInterpolation()
|
||||
|
||||
Use [bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation) for image resizing, the default (and fastest) interpolation if none is specified.
|
||||
|
||||
### bicubicInterpolation()
|
||||
|
||||
Use [bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation) for image resizing. This typically reduces performance by 5%.
|
||||
|
||||
### nohaloInterpolation()
|
||||
|
||||
Use [Nohalo interpolation](http://eprints.soton.ac.uk/268086/) for image resizing. This typically reduces performance by a factor of 2.
|
||||
|
||||
### progressive()
|
||||
|
||||
Use progressive (interlace) scan for JPEG and PNG output. This typically reduces compression performance by 30% but results in an image that can be rendered sooner when decompressed.
|
||||
|
||||
### quality(quality)
|
||||
|
||||
The output quality to use for lossy JPEG, WebP and TIFF output formats. The default quality is `80`.
|
||||
|
||||
`quality` is a Number between 1 and 100.
|
||||
|
||||
### compressionLevel(compressionLevel)
|
||||
|
||||
An advanced setting for the _zlib_ compression level of the lossless PNG output format. The default level is `6`.
|
||||
|
||||
`compressionLevel` is a Number between -1 and 9.
|
||||
|
||||
### sequentialRead()
|
||||
|
||||
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
|
||||
|
||||
### toFile(filename, [callback])
|
||||
|
||||
`filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP and TIFF supported.
|
||||
|
||||
`callback`, if present, is called with a single argument `(err)` containing an error message, if any.
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
### toBuffer([callback])
|
||||
|
||||
Write image data to a Buffer, the format of which will match the input image. JPEG, PNG and WebP are supported.
|
||||
|
||||
`callback`, if present, gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data.
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
### jpeg([callback])
|
||||
|
||||
Write JPEG image data to a Buffer.
|
||||
|
||||
`callback`, if present, gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant JPEG image data.
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
### png([callback])
|
||||
|
||||
Write PNG image data to a Buffer.
|
||||
|
||||
`callback`, if present, gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant PNG image data.
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
### webp([callback])
|
||||
|
||||
Write WebP image data to a Buffer.
|
||||
|
||||
`callback`, if present, gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant WebP image data.
|
||||
|
||||
A Promises/A+ promise is returned when `callback` is not provided.
|
||||
|
||||
### sharp.cache([limit])
|
||||
|
||||
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.
|
||||
|
||||
Warnings such as _Application transferred too many scanlines_ are a good indicator you've set this value too low.
|
||||
|
||||
```javascript
|
||||
var stats = sharp.cache(); // { current: 98, high: 115, limit: 100 }
|
||||
sharp.cache(200); // { current: 98, high: 115, limit: 200 }
|
||||
sharp.cache(50); // { current: 49, high: 115, limit: 50 }
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
[](https://travis-ci.org/lovell/sharp)
|
||||
|
||||
npm test
|
||||
|
||||
Running the tests requires both ImageMagick and GraphicsMagick plus one of either libmagick++-dev or libgraphicsmagick++.
|
||||
|
||||
brew install imagemagick
|
||||
brew install graphicsmagick
|
||||
|
||||
sudo apt-get install imagemagick graphicsmagick libmagick++-dev
|
||||
|
||||
## Performance
|
||||
|
||||
Test environment:
|
||||
### Test environment
|
||||
|
||||
* AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3
|
||||
* libvips 7.37
|
||||
* libjpeg-turbo8 1.3.0
|
||||
* libpng 1.6.6
|
||||
* zlib1g 1.2.7
|
||||
* Intel Xeon [L5520](http://ark.intel.com/products/40201/Intel-Xeon-Processor-L5520-8M-Cache-2_26-GHz-5_86-GTs-Intel-QPI) 2.27GHz 8MB cache
|
||||
* Ubuntu 13.10
|
||||
* libvips 7.38.5
|
||||
|
||||
`-file-buffer` indicates read from file and write to buffer, `-buffer-file` indicates read from buffer and write to file etc.
|
||||
### The contenders
|
||||
|
||||
`-sharpen`, `-progressive` etc. demonstrate the negative effect of options on performance.
|
||||
* [imagemagick-native](https://github.com/mash/node-imagemagick-native) - Supports Buffers only and blocks main V8 thread whilst processing.
|
||||
* [imagemagick](https://github.com/rsms/node-imagemagick) - Supports filesystem only and "has been unmaintained for a long time".
|
||||
* [gm](https://github.com/aheckmann/gm) - Fully featured wrapper around GraphicsMagick.
|
||||
* sharp - Caching within libvips disabled to ensure a fair comparison.
|
||||
|
||||
### JPEG
|
||||
### The task
|
||||
|
||||
* imagemagick x 5.53 ops/sec ±0.62% (31 runs sampled)
|
||||
* gm-file-file x 4.10 ops/sec ±0.41% (25 runs sampled)
|
||||
* gm-file-buffer x 4.10 ops/sec ±0.36% (25 runs sampled)
|
||||
* epeg-file-file x 23.82 ops/sec ±0.18% (60 runs sampled)
|
||||
* epeg-file-buffer x 23.98 ops/sec ±0.16% (61 runs sampled)
|
||||
Decompress a 2725x2225 JPEG image, resize and crop to 720x480, then compress to JPEG.
|
||||
|
||||
* sharp-buffer-file x 20.76 ops/sec ±0.55% (54 runs sampled)
|
||||
* sharp-buffer-buffer x 20.90 ops/sec ±0.26% (54 runs sampled)
|
||||
* sharp-file-file x 91.78 ops/sec ±0.38% (88 runs sampled)
|
||||
* sharp-file-buffer x __93.05 ops/sec__ ±0.61% (76 runs sampled)
|
||||
### Results
|
||||
|
||||
* sharp-file-buffer-sharpen x 63.09 ops/sec ±5.58% (63 runs sampled)
|
||||
* sharp-file-buffer-progressive x 61.68 ops/sec ±0.53% (76 runs sampled)
|
||||
* sharp-file-buffer-sequentialRead x 60.66 ops/sec ±0.38% (75 runs sampled)
|
||||
| Module | Input | Output | Ops/sec | Speed-up |
|
||||
| :-------------------- | :----- | :----- | ------: | -------: |
|
||||
| imagemagick-native | buffer | buffer | 0.97 | 1 |
|
||||
| imagemagick | file | file | 2.49 | 2.6 |
|
||||
| gm | buffer | file | 3.72 | 3.8 |
|
||||
| gm | buffer | buffer | 3.80 | 3.9 |
|
||||
| gm | file | file | 3.67 | 3.8 |
|
||||
| gm | file | buffer | 3.67 | 3.8 |
|
||||
| sharp | buffer | file | 13.62 | 14.0 |
|
||||
| sharp | buffer | buffer | 12.43 | 12.8 |
|
||||
| sharp | file | file | 13.02 | 13.4 |
|
||||
| sharp | file | buffer | 11.15 | 11.5 |
|
||||
| sharp +sharpen | file | buffer | 10.26 | 10.6 |
|
||||
| sharp +progressive | file | buffer | 9.44 | 9.7 |
|
||||
| sharp +sequentialRead | file | buffer | 11.94 | 12.3 |
|
||||
|
||||
### PNG
|
||||
|
||||
* imagemagick x 4.27 ops/sec ±0.21% (25 runs sampled)
|
||||
* gm-file-file x 8.33 ops/sec ±0.19% (44 runs sampled)
|
||||
* gm-file-buffer x 7.45 ops/sec ±0.16% (40 runs sampled)
|
||||
|
||||
* sharp-buffer-file x 4.94 ops/sec ±118.46% (26 runs sampled)
|
||||
* sharp-buffer-buffer x 12.59 ops/sec ±0.55% (64 runs sampled)
|
||||
* sharp-file-file x 44.06 ops/sec ±6.86% (75 runs sampled)
|
||||
* sharp-file-buffer x __46.29 ops/sec__ ±0.38% (76 runs sampled)
|
||||
|
||||
* sharp-file-buffer-sharpen x 38.86 ops/sec ±0.22% (65 runs sampled)
|
||||
* sharp-file-buffer-progressive x 46.35 ops/sec ±0.20% (76 runs sampled)
|
||||
* sharp-file-buffer-sequentialRead x 29.02 ops/sec ±0.62% (72 runs sampled)
|
||||
You can expect much greater performance with caching enabled (default) and using 16+ core machines.
|
||||
|
||||
## Licence
|
||||
|
||||
Copyright 2013, 2014 Lovell Fuller
|
||||
Copyright 2013, 2014 Lovell Fuller and Pierre Inglebert
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
15
binding.gyp
@@ -2,16 +2,17 @@
|
||||
'targets': [{
|
||||
'target_name': 'sharp',
|
||||
'sources': ['src/sharp.cc'],
|
||||
'variables': {
|
||||
'PKG_CONFIG_PATH': '<!(which brew >/dev/null 2>&1 && eval $(brew --env) && echo $PKG_CONFIG_LIBDIR || true):$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/lib/pkgconfig'
|
||||
},
|
||||
'libraries': [
|
||||
'<!@(PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" pkg-config --libs vips)',
|
||||
'<!@(PKG_CONFIG_PATH="/usr/lib/pkgconfig" pkg-config --libs vips)'
|
||||
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --libs vips)'
|
||||
],
|
||||
'include_dirs': [
|
||||
'/usr/include/glib-2.0',
|
||||
'/usr/lib/glib-2.0/include',
|
||||
'/usr/lib/x86_64-linux-gnu/glib-2.0/include'
|
||||
'<!(PKG_CONFIG_PATH="<(PKG_CONFIG_PATH)" pkg-config --cflags vips glib-2.0)',
|
||||
'<!(node -e "require(\'nan\')")'
|
||||
],
|
||||
'cflags': ['-fexceptions', '-O3'],
|
||||
'cflags_cc': ['-fexceptions', '-O3']
|
||||
'cflags': ['-fexceptions', '-pedantic', '-Wall', '-O3'],
|
||||
'cflags_cc': ['-fexceptions', '-pedantic', '-Wall', '-O3']
|
||||
}]
|
||||
}
|
||||
|
||||
305
index.js
@@ -1,64 +1,241 @@
|
||||
var sharp = require("./build/Release/sharp");
|
||||
|
||||
module.exports.buffer = {
|
||||
jpeg: "__jpeg",
|
||||
png: "__png"
|
||||
};
|
||||
|
||||
module.exports.canvas = {
|
||||
crop: "c",
|
||||
embedWhite: "w",
|
||||
embedBlack: "b"
|
||||
};
|
||||
|
||||
module.exports.resize = function(input, output, width, height, options, callback) {
|
||||
"use strict";
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
} else {
|
||||
options = options || {};
|
||||
}
|
||||
if (typeof input === 'string') {
|
||||
options.inFile = input;
|
||||
} else if (typeof input ==='object' && input instanceof Buffer) {
|
||||
options.inBuffer = input;
|
||||
} else {
|
||||
callback("Unsupported input " + typeof input);
|
||||
return;
|
||||
}
|
||||
if (!output || output.length === 0) {
|
||||
callback("Invalid output");
|
||||
return;
|
||||
}
|
||||
var outWidth = Number(width);
|
||||
if (Number.isNaN(outWidth)) {
|
||||
callback("Invalid width " + width);
|
||||
return;
|
||||
}
|
||||
var outHeight = Number(height);
|
||||
if (Number.isNaN(outHeight)) {
|
||||
callback("Invalid height " + height);
|
||||
return;
|
||||
}
|
||||
var canvas = options.canvas || "c";
|
||||
if (canvas.length !== 1 || "cwb".indexOf(canvas) === -1) {
|
||||
callback("Invalid canvas " + canvas);
|
||||
return;
|
||||
}
|
||||
var sharpen = !!options.sharpen;
|
||||
var progessive = !!options.progessive;
|
||||
var sequentialRead = !!options.sequentialRead;
|
||||
sharp.resize(options.inFile, options.inBuffer, output, width, height, canvas, sharpen, progessive, sequentialRead, callback);
|
||||
};
|
||||
|
||||
/* Deprecated v0.0.x methods */
|
||||
module.exports.crop = function(input, output, width, height, sharpen, callback) {
|
||||
sharp.resize(input, output, width, height, {canvas: "c", sharpen: true}, callback);
|
||||
};
|
||||
module.exports.embedWhite = function(input, output, width, height, callback) {
|
||||
sharp.resize(input, output, width, height, {canvas: "w", sharpen: true}, callback);
|
||||
};
|
||||
module.exports.embedBlack = function(input, output, width, height, callback) {
|
||||
sharp.resize(input, output, width, height, {canvas: "b", sharpen: true}, callback);
|
||||
};
|
||||
/*jslint node: true */
|
||||
'use strict';
|
||||
|
||||
var Promise = require('bluebird');
|
||||
var sharp = require('./build/Release/sharp');
|
||||
|
||||
var Sharp = function(input) {
|
||||
if (!(this instanceof Sharp)) {
|
||||
return new Sharp(input);
|
||||
}
|
||||
this.options = {
|
||||
width: -1,
|
||||
height: -1,
|
||||
canvas: 'c',
|
||||
angle: 0,
|
||||
withoutEnlargement: false,
|
||||
sharpen: false,
|
||||
interpolator: 'bilinear',
|
||||
progressive: false,
|
||||
sequentialRead: false,
|
||||
quality: 80,
|
||||
compressionLevel: 6,
|
||||
output: '__jpeg'
|
||||
};
|
||||
if (typeof input === 'string') {
|
||||
this.options.fileIn = input;
|
||||
} else if (typeof input ==='object' && input instanceof Buffer) {
|
||||
if (input.length > 0) {
|
||||
this.options.bufferIn = input;
|
||||
} else {
|
||||
throw new Error('Buffer is empty');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unsupported input ' + typeof input);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
module.exports = Sharp;
|
||||
|
||||
Sharp.prototype.crop = function() {
|
||||
this.options.canvas = 'c';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.embedWhite = function() {
|
||||
this.options.canvas = 'w';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.embedBlack = function() {
|
||||
this.options.canvas = 'b';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.max = function() {
|
||||
this.options.canvas = 'm';
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Rotate output image by 0, 90, 180 or 270 degrees
|
||||
Auto-rotation based on the EXIF Orientation tag is represented by an angle of -1
|
||||
*/
|
||||
Sharp.prototype.rotate = function(angle) {
|
||||
if (typeof angle === 'undefined') {
|
||||
this.options.angle = -1;
|
||||
} else if (!Number.isNaN(angle) && [0, 90, 180, 270].indexOf(angle) !== -1) {
|
||||
this.options.angle = angle;
|
||||
} else {
|
||||
throw new Error('Unsupported angle (0, 90, 180, 270) ' + angle);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Do not enlarge the output if the input width *or* height are already less than the required dimensions
|
||||
This is equivalent to GraphicsMagick's ">" geometry option:
|
||||
"change the dimensions of the image only if its width or height exceeds the geometry specification"
|
||||
*/
|
||||
Sharp.prototype.withoutEnlargement = function(withoutEnlargement) {
|
||||
this.options.withoutEnlargement = (typeof withoutEnlargement === 'boolean') ? withoutEnlargement : true;
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.sharpen = function(sharpen) {
|
||||
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Use bilinear interpolation for the affine transformation (fastest, default)
|
||||
*/
|
||||
Sharp.prototype.bilinearInterpolation = function() {
|
||||
this.options.interpolator = 'bilinear';
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Use bicubic interpolation for the affine transformation
|
||||
*/
|
||||
Sharp.prototype.bicubicInterpolation = function() {
|
||||
this.options.interpolator = 'bicubic';
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Use Nohalo interpolation for the affine transformation
|
||||
*/
|
||||
Sharp.prototype.nohaloInterpolation = function() {
|
||||
this.options.interpolator = 'nohalo';
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.progressive = function(progressive) {
|
||||
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.sequentialRead = function(sequentialRead) {
|
||||
this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true;
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.quality = function(quality) {
|
||||
if (!Number.isNaN(quality) && quality >= 1 && quality <= 100) {
|
||||
this.options.quality = quality;
|
||||
} else {
|
||||
throw new Error('Invalid quality (1 to 100) ' + quality);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.compressionLevel = function(compressionLevel) {
|
||||
if (!Number.isNaN(compressionLevel) && compressionLevel >= -1 && compressionLevel <= 9) {
|
||||
this.options.compressionLevel = compressionLevel;
|
||||
} else {
|
||||
throw new Error('Invalid compressionLevel (-1 to 9) ' + compressionLevel);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Sharp.prototype.resize = function(width, height) {
|
||||
if (!width) {
|
||||
this.options.width = -1;
|
||||
} else {
|
||||
if (!Number.isNaN(width)) {
|
||||
this.options.width = width;
|
||||
} else {
|
||||
throw new Error('Invalid width ' + width);
|
||||
}
|
||||
}
|
||||
if (!height) {
|
||||
this.options.height = -1;
|
||||
} else {
|
||||
if (!Number.isNaN(height)) {
|
||||
this.options.height = height;
|
||||
} else {
|
||||
throw new Error('Invalid height ' + height);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/*
|
||||
Write output image data to a file
|
||||
*/
|
||||
Sharp.prototype.toFile = function(output, callback) {
|
||||
if (!output || output.length === 0) {
|
||||
var errOutputInvalid = new Error('Invalid output');
|
||||
if (typeof callback === 'function') {
|
||||
callback(errOutputInvalid);
|
||||
} else {
|
||||
return Promise.reject(errOutputInvalid);
|
||||
}
|
||||
} else {
|
||||
if (this.options.fileIn === output) {
|
||||
var errOutputIsInput = new Error('Cannot use same file for input and output');
|
||||
if (typeof callback === 'function') {
|
||||
callback(errOutputIsInput);
|
||||
} else {
|
||||
return Promise.reject(errOutputIsInput);
|
||||
}
|
||||
} else {
|
||||
return this._sharp(output, callback);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
// Deprecated to make way for future stream support - remove in v0.6.0
|
||||
Sharp.prototype.write = require('util').deprecate(
|
||||
Sharp.prototype.toFile,
|
||||
'.write() is deprecated and will be removed in v0.6.0. Use .toFile() instead.'
|
||||
);
|
||||
|
||||
Sharp.prototype.toBuffer = function(callback) {
|
||||
return this._sharp('__input', callback);
|
||||
};
|
||||
|
||||
Sharp.prototype.jpeg = function(callback) {
|
||||
return this._sharp('__jpeg', callback);
|
||||
};
|
||||
|
||||
Sharp.prototype.png = function(callback) {
|
||||
return this._sharp('__png', callback);
|
||||
};
|
||||
|
||||
Sharp.prototype.webp = function(callback) {
|
||||
return this._sharp('__webp', callback);
|
||||
};
|
||||
|
||||
/*
|
||||
Invoke the C++ image processing pipeline
|
||||
Supports callback and promise variants
|
||||
*/
|
||||
Sharp.prototype._sharp = function(output, callback) {
|
||||
if (typeof callback === 'function') {
|
||||
// I like callbacks
|
||||
sharp.resize(this.options, output, callback);
|
||||
return this;
|
||||
} else {
|
||||
// I like promises
|
||||
var options = this.options;
|
||||
return new Promise(function(resolve, reject) {
|
||||
sharp.resize(options, output, function(err, data) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.cache = function(limit) {
|
||||
if (Number.isNaN(limit)) {
|
||||
limit = null;
|
||||
}
|
||||
return sharp.cache(limit);
|
||||
};
|
||||
|
||||
34
package.json
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"name": "sharp",
|
||||
"version": "0.1.1",
|
||||
"author": "Lovell Fuller",
|
||||
"description": "High performance module to resize JPEG and PNG images using the libvips image processing library",
|
||||
"version": "0.5.1",
|
||||
"author": "Lovell Fuller <npm@lovell.info>",
|
||||
"contributors": [
|
||||
"Pierre Inglebert <pierre.inglebert@gmail.com>",
|
||||
"Jonathan Ong <jonathanrichardong@gmail.com>"
|
||||
],
|
||||
"description": "High performance Node.js module to resize JPEG, PNG and WebP images using the libvips library",
|
||||
"scripts": {
|
||||
"test": "node tests/perf.js"
|
||||
"test": "node tests/unit && node tests/perf"
|
||||
},
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -14,24 +18,32 @@
|
||||
"keywords": [
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp",
|
||||
"tiff",
|
||||
"gif",
|
||||
"resize",
|
||||
"thumbnail",
|
||||
"sharpen",
|
||||
"crop",
|
||||
"embed",
|
||||
"libvips",
|
||||
"vips",
|
||||
"fast",
|
||||
"buffer"
|
||||
],
|
||||
"dependencies": {
|
||||
"nan": "^1.2.0",
|
||||
"bluebird": "^2.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"imagemagick": "*",
|
||||
"gm": "*",
|
||||
"epeg": "*",
|
||||
"async": "*",
|
||||
"benchmark": "*"
|
||||
"imagemagick": "^0.1.3",
|
||||
"imagemagick-native": "^1.1.1",
|
||||
"gm": "^1.16.0",
|
||||
"async": "^0.9.0",
|
||||
"benchmark": "^1.0.0"
|
||||
},
|
||||
"license": "Apache 2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
"node": ">=0.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
637
src/sharp.cc
@@ -5,6 +5,8 @@
|
||||
#include <string.h>
|
||||
#include <vips/vips.h>
|
||||
|
||||
#include "nan.h"
|
||||
|
||||
using namespace v8;
|
||||
using namespace node;
|
||||
|
||||
@@ -15,40 +17,64 @@ struct resize_baton {
|
||||
std::string file_out;
|
||||
void* buffer_out;
|
||||
size_t buffer_out_len;
|
||||
int width;
|
||||
int height;
|
||||
int width;
|
||||
int height;
|
||||
bool crop;
|
||||
bool max;
|
||||
VipsExtend extend;
|
||||
bool sharpen;
|
||||
bool progessive;
|
||||
std::string interpolator;
|
||||
bool progressive;
|
||||
bool without_enlargement;
|
||||
VipsAccess access_method;
|
||||
int quality;
|
||||
int compressionLevel;
|
||||
int angle;
|
||||
std::string err;
|
||||
Persistent<Function> callback;
|
||||
|
||||
resize_baton(): buffer_in_len(0), buffer_out_len(0) {}
|
||||
resize_baton():
|
||||
buffer_in_len(0),
|
||||
buffer_out_len(0),
|
||||
crop(false),
|
||||
max(false),
|
||||
sharpen(false),
|
||||
progressive(false),
|
||||
without_enlargement(false) {}
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
JPEG,
|
||||
PNG
|
||||
PNG,
|
||||
WEBP,
|
||||
TIFF,
|
||||
MAGICK
|
||||
} ImageType;
|
||||
|
||||
unsigned char MARKER_JPEG[] = {0xff, 0xd8};
|
||||
unsigned char MARKER_PNG[] = {0x89, 0x50};
|
||||
unsigned char const MARKER_JPEG[] = {0xff, 0xd8};
|
||||
unsigned char const MARKER_PNG[] = {0x89, 0x50};
|
||||
unsigned char const MARKER_WEBP[] = {0x52, 0x49};
|
||||
|
||||
bool ends_with(std::string const &str, std::string const &end) {
|
||||
static bool ends_with(std::string const &str, std::string const &end) {
|
||||
return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
|
||||
}
|
||||
|
||||
bool is_jpeg(std::string const &str) {
|
||||
return ends_with(str, ".jpg") || ends_with(str, ".jpeg");
|
||||
static bool is_jpeg(std::string const &str) {
|
||||
return ends_with(str, ".jpg") || ends_with(str, ".jpeg") || ends_with(str, ".JPG") || ends_with(str, ".JPEG");
|
||||
}
|
||||
|
||||
bool is_png(std::string const &str) {
|
||||
return ends_with(str, ".png");
|
||||
static bool is_png(std::string const &str) {
|
||||
return ends_with(str, ".png") || ends_with(str, ".PNG");
|
||||
}
|
||||
|
||||
void resize_error(resize_baton *baton, VipsImage *unref) {
|
||||
static bool is_webp(std::string const &str) {
|
||||
return ends_with(str, ".webp") || ends_with(str, ".WEBP");
|
||||
}
|
||||
|
||||
static bool is_tiff(std::string const &str) {
|
||||
return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF");
|
||||
}
|
||||
|
||||
static void resize_error(resize_baton *baton, VipsImage *unref) {
|
||||
(baton->err).append(vips_error_buffer());
|
||||
vips_error_clear();
|
||||
g_object_unref(unref);
|
||||
@@ -56,237 +82,434 @@ void resize_error(resize_baton *baton, VipsImage *unref) {
|
||||
return;
|
||||
}
|
||||
|
||||
void resize_async(uv_work_t *work) {
|
||||
resize_baton* baton = static_cast<resize_baton*>(work->data);
|
||||
|
||||
ImageType inputImageType = JPEG;
|
||||
VipsImage *in = vips_image_new();
|
||||
if (baton->buffer_in_len > 1) {
|
||||
if (memcmp(MARKER_JPEG, baton->buffer_in, 2) == 0) {
|
||||
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
|
||||
return resize_error(baton, in);
|
||||
/*
|
||||
Calculate the angle of rotation for the output image.
|
||||
In order of priority:
|
||||
1. Use explicitly requested angle (supports 90, 180, 270)
|
||||
2. Use input image EXIF Orientation header (does not support mirroring)
|
||||
3. Otherwise default to zero, i.e. no rotation
|
||||
*/
|
||||
static VipsAngle calc_rotation(int const angle, VipsImage const *input) {
|
||||
VipsAngle rotate = VIPS_ANGLE_0;
|
||||
if (angle == -1) {
|
||||
const char *exif;
|
||||
if (!vips_image_get_string(input, "exif-ifd0-Orientation", &exif)) {
|
||||
if (exif[0] == 0x36) { // "6"
|
||||
rotate = VIPS_ANGLE_90;
|
||||
} else if (exif[0] == 0x33) { // "3"
|
||||
rotate = VIPS_ANGLE_180;
|
||||
} else if (exif[0] == 0x38) { // "8"
|
||||
rotate = VIPS_ANGLE_270;
|
||||
}
|
||||
} else if(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 (is_jpeg(baton->file_in)) {
|
||||
if (vips_jpegload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
|
||||
return resize_error(baton, in);
|
||||
}
|
||||
} else if (is_png(baton->file_in)) {
|
||||
inputImageType = PNG;
|
||||
if (vips_pngload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
|
||||
return resize_error(baton, in);
|
||||
}
|
||||
} else {
|
||||
resize_error(baton, in);
|
||||
(baton->err).append("Unsupported input " + baton->file_in);
|
||||
return;
|
||||
}
|
||||
|
||||
double xfactor = static_cast<double>(in->Xsize) / std::max(baton->width, 1);
|
||||
double yfactor = static_cast<double>(in->Ysize) / std::max(baton->height, 1);
|
||||
double factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
|
||||
factor = std::max(factor, 1.0);
|
||||
int shrink = floor(factor);
|
||||
double residual = shrink / factor;
|
||||
|
||||
// Try to use libjpeg shrink-on-load
|
||||
int shrink_on_load = 1;
|
||||
if (inputImageType == JPEG) {
|
||||
if (shrink >= 8) {
|
||||
factor = residual * shrink / 8;
|
||||
shrink_on_load = 8;
|
||||
shrink = floor(factor);
|
||||
residual = shrink / factor;
|
||||
} else if (shrink >= 4) {
|
||||
factor = residual * shrink / 4;
|
||||
shrink_on_load = 4;
|
||||
shrink = floor(factor);
|
||||
residual = shrink / factor;
|
||||
} else if (shrink >= 2) {
|
||||
factor = residual * shrink / 2;
|
||||
shrink_on_load = 2;
|
||||
shrink = floor(factor);
|
||||
residual = shrink / factor;
|
||||
if (angle == 90) {
|
||||
rotate = VIPS_ANGLE_90;
|
||||
} else if (angle == 180) {
|
||||
rotate = VIPS_ANGLE_180;
|
||||
} else if (angle == 270) {
|
||||
rotate = VIPS_ANGLE_270;
|
||||
}
|
||||
if (shrink_on_load > 1) {
|
||||
g_object_unref(in);
|
||||
in = vips_image_new();
|
||||
if (baton->buffer_in_len > 1) {
|
||||
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "shrink", shrink_on_load, NULL)) {
|
||||
}
|
||||
return rotate;
|
||||
}
|
||||
|
||||
class ResizeWorker : public NanAsyncWorker {
|
||||
public:
|
||||
ResizeWorker(NanCallback *callback, resize_baton *baton)
|
||||
: NanAsyncWorker(callback), baton(baton) {}
|
||||
~ResizeWorker() {}
|
||||
|
||||
void Execute () {
|
||||
// Input
|
||||
ImageType inputImageType = JPEG;
|
||||
VipsImage *in = vips_image_new();
|
||||
if (baton->buffer_in_len > 1) {
|
||||
if (memcmp(MARKER_JPEG, baton->buffer_in, 2) == 0) {
|
||||
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
|
||||
return resize_error(baton, in);
|
||||
}
|
||||
} 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 {
|
||||
if (vips_jpegload((baton->file_in).c_str(), &in, "shrink", shrink_on_load, NULL)) {
|
||||
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 {
|
||||
resize_error(baton, in);
|
||||
(baton->err).append("Unsupported input file " + baton->file_in);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get input image width and height
|
||||
int inputWidth = in->Xsize;
|
||||
int inputHeight = in->Ysize;
|
||||
|
||||
// Calculate angle of rotation, to be carried out later
|
||||
VipsAngle rotation = calc_rotation(baton->angle, in);
|
||||
if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
|
||||
// Swap input output width and height when rotating by 90 or 270 degrees
|
||||
int swap = inputWidth;
|
||||
inputWidth = inputHeight;
|
||||
inputHeight = swap;
|
||||
}
|
||||
|
||||
// Scaling calculations
|
||||
double factor;
|
||||
if (baton->width > 0 && baton->height > 0) {
|
||||
// Fixed width and height
|
||||
double xfactor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
|
||||
double yfactor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
|
||||
factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
|
||||
// if max is set, we need to compute the real size of the thumb image
|
||||
if (baton->max) {
|
||||
if (xfactor > yfactor) {
|
||||
baton->height = round(static_cast<double>(inputHeight) / xfactor);
|
||||
} else {
|
||||
baton->width = round(static_cast<double>(inputWidth) / yfactor);
|
||||
}
|
||||
}
|
||||
} else if (baton->width > 0) {
|
||||
// Fixed width, auto height
|
||||
factor = static_cast<double>(inputWidth) / static_cast<double>(baton->width);
|
||||
baton->height = floor(static_cast<double>(inputHeight) / factor);
|
||||
} else if (baton->height > 0) {
|
||||
// Fixed height, auto width
|
||||
factor = static_cast<double>(inputHeight) / static_cast<double>(baton->height);
|
||||
baton->width = floor(static_cast<double>(inputWidth) / factor);
|
||||
} else {
|
||||
// Identity transform
|
||||
factor = 1;
|
||||
baton->width = inputWidth;
|
||||
baton->height = inputHeight;
|
||||
}
|
||||
int shrink = floor(factor);
|
||||
if (shrink < 1) {
|
||||
shrink = 1;
|
||||
}
|
||||
double residual = static_cast<double>(shrink) / factor;
|
||||
|
||||
// Do not enlarge the output if the input width *or* height are already less than the required dimensions
|
||||
if (baton->without_enlargement) {
|
||||
if (inputWidth < baton->width || inputHeight < baton->height) {
|
||||
factor = 1;
|
||||
shrink = 1;
|
||||
residual = 0;
|
||||
baton->width = inputWidth;
|
||||
baton->height = inputHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use libjpeg shrink-on-load
|
||||
int shrink_on_load = 1;
|
||||
if (inputImageType == JPEG) {
|
||||
if (shrink >= 8) {
|
||||
factor = factor / 8;
|
||||
shrink_on_load = 8;
|
||||
} else if (shrink >= 4) {
|
||||
factor = factor / 4;
|
||||
shrink_on_load = 4;
|
||||
} else if (shrink >= 2) {
|
||||
factor = factor / 2;
|
||||
shrink_on_load = 2;
|
||||
}
|
||||
}
|
||||
VipsImage *shrunk_on_load = vips_image_new();
|
||||
if (shrink_on_load > 1) {
|
||||
// Recalculate integral shrink and double residual
|
||||
factor = std::max(factor, 1.0);
|
||||
shrink = floor(factor);
|
||||
residual = static_cast<double>(shrink) / factor;
|
||||
// Reload input using shrink-on-load
|
||||
if (baton->buffer_in_len > 1) {
|
||||
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
|
||||
return resize_error(baton, in);
|
||||
}
|
||||
} else {
|
||||
if (vips_jpegload((baton->file_in).c_str(), &shrunk_on_load, "shrink", shrink_on_load, NULL)) {
|
||||
return resize_error(baton, in);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vips_copy(in, &shrunk_on_load, NULL);
|
||||
}
|
||||
}
|
||||
g_object_unref(in);
|
||||
|
||||
VipsImage *shrunk = vips_image_new();
|
||||
if (shrink > 1) {
|
||||
// Use vips_shrink with the integral reduction
|
||||
if (vips_shrink(in, &shrunk, shrink, shrink, NULL)) {
|
||||
return resize_error(baton, in);
|
||||
VipsImage *shrunk = vips_image_new();
|
||||
if (shrink > 1) {
|
||||
// Use vips_shrink with the integral reduction
|
||||
if (vips_shrink(shrunk_on_load, &shrunk, shrink, shrink, NULL)) {
|
||||
return resize_error(baton, shrunk_on_load);
|
||||
}
|
||||
// Recalculate residual float based on dimensions of required vs shrunk images
|
||||
double shrunkWidth = shrunk->Xsize;
|
||||
double shrunkHeight = shrunk->Ysize;
|
||||
if (rotation == VIPS_ANGLE_90 || rotation == VIPS_ANGLE_270) {
|
||||
// Swap input output width and height when rotating by 90 or 270 degrees
|
||||
int swap = shrunkWidth;
|
||||
shrunkWidth = shrunkHeight;
|
||||
shrunkHeight = swap;
|
||||
}
|
||||
double residualx = static_cast<double>(baton->width) / static_cast<double>(shrunkWidth);
|
||||
double residualy = static_cast<double>(baton->height) / static_cast<double>(shrunkHeight);
|
||||
if (baton->crop || baton->max) {
|
||||
residual = std::max(residualx, residualy);
|
||||
} else {
|
||||
residual = std::min(residualx, residualy);
|
||||
}
|
||||
} else {
|
||||
vips_copy(shrunk_on_load, &shrunk, NULL);
|
||||
}
|
||||
} else {
|
||||
vips_copy(in, &shrunk, NULL);
|
||||
}
|
||||
g_object_unref(in);
|
||||
g_object_unref(shrunk_on_load);
|
||||
|
||||
// Use vips_affine with the remaining float part using bilinear interpolation
|
||||
VipsImage *affined = vips_image_new();
|
||||
if (residual > 0) {
|
||||
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) {
|
||||
return resize_error(baton, shrunk);
|
||||
// Use vips_affine with the remaining float part
|
||||
VipsImage *affined = vips_image_new();
|
||||
if (residual != 0) {
|
||||
// Create interpolator - "bilinear" (default), "bicubic" or "nohalo"
|
||||
VipsInterpolate *interpolator = vips_interpolate_new(baton->interpolator.c_str());
|
||||
// Perform affine transformation
|
||||
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", interpolator, NULL)) {
|
||||
g_object_unref(interpolator);
|
||||
return resize_error(baton, shrunk);
|
||||
}
|
||||
g_object_unref(interpolator);
|
||||
} else {
|
||||
vips_copy(shrunk, &affined, NULL);
|
||||
}
|
||||
} else {
|
||||
vips_copy(shrunk, &affined, NULL);
|
||||
}
|
||||
g_object_unref(shrunk);
|
||||
g_object_unref(shrunk);
|
||||
|
||||
VipsImage *canvased = vips_image_new();
|
||||
if (affined->Xsize != baton->width && affined->Ysize != baton->height) {
|
||||
if (baton->crop && affined->Xsize != baton->width && affined->Ysize != baton->height) {
|
||||
// Crop
|
||||
int width = std::min(affined->Xsize, baton->width);
|
||||
int height = std::min(affined->Ysize, baton->height);
|
||||
int left = (affined->Xsize - width + 1) / 2;
|
||||
int top = (affined->Ysize - height + 1) / 2;
|
||||
if (vips_extract_area(affined, &canvased, left, top, width, height, NULL)) {
|
||||
// Rotate
|
||||
VipsImage *rotated = vips_image_new();
|
||||
if (rotation != VIPS_ANGLE_0) {
|
||||
if (vips_rot(affined, &rotated, rotation, NULL)) {
|
||||
return resize_error(baton, affined);
|
||||
}
|
||||
} else {
|
||||
// Embed
|
||||
int left = (baton->width - affined->Xsize) / 2;
|
||||
int top = (baton->height - affined->Ysize) / 2;
|
||||
if (vips_embed(affined, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) {
|
||||
return resize_error(baton, affined);
|
||||
vips_copy(affined, &rotated, NULL);
|
||||
}
|
||||
g_object_unref(affined);
|
||||
|
||||
// Crop/embed
|
||||
VipsImage *canvased = vips_image_new();
|
||||
if (rotated->Xsize != baton->width || rotated->Ysize != baton->height) {
|
||||
if (baton->crop || baton->max) {
|
||||
// Crop/max
|
||||
int width = std::min(rotated->Xsize, baton->width);
|
||||
int height = std::min(rotated->Ysize, baton->height);
|
||||
int left = (rotated->Xsize - width + 1) / 2;
|
||||
int top = (rotated->Ysize - height + 1) / 2;
|
||||
if (vips_extract_area(rotated, &canvased, left, top, width, height, NULL)) {
|
||||
return resize_error(baton, rotated);
|
||||
}
|
||||
} else {
|
||||
// Embed
|
||||
int left = (baton->width - rotated->Xsize) / 2;
|
||||
int top = (baton->height - rotated->Ysize) / 2;
|
||||
if (vips_embed(rotated, &canvased, left, top, baton->width, baton->height, "extend", baton->extend, NULL)) {
|
||||
return resize_error(baton, rotated);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
vips_copy(rotated, &canvased, NULL);
|
||||
}
|
||||
} else {
|
||||
vips_copy(affined, &canvased, NULL);
|
||||
}
|
||||
g_object_unref(affined);
|
||||
g_object_unref(rotated);
|
||||
|
||||
// Mild sharpen
|
||||
VipsImage *sharpened = vips_image_new();
|
||||
if (baton->sharpen) {
|
||||
INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3,
|
||||
-1, -1, -1,
|
||||
-1, 32, -1,
|
||||
-1, -1, -1);
|
||||
sharpen->scale = 24;
|
||||
if (im_conv(canvased, sharpened, sharpen)) {
|
||||
return resize_error(baton, canvased);
|
||||
// Mild sharpen
|
||||
VipsImage *sharpened = vips_image_new();
|
||||
if (baton->sharpen) {
|
||||
VipsImage *sharpen = vips_image_new_matrixv(3, 3,
|
||||
-1.0, -1.0, -1.0,
|
||||
-1.0, 32.0, -1.0,
|
||||
-1.0, -1.0, -1.0);
|
||||
vips_image_set_double(sharpen, "scale", 24);
|
||||
if (vips_conv(canvased, &sharpened, sharpen, NULL)) {
|
||||
g_object_unref(sharpen);
|
||||
return resize_error(baton, canvased);
|
||||
}
|
||||
g_object_unref(sharpen);
|
||||
} else {
|
||||
vips_copy(canvased, &sharpened, NULL);
|
||||
}
|
||||
} else {
|
||||
vips_copy(canvased, &sharpened, NULL);
|
||||
}
|
||||
g_object_unref(canvased);
|
||||
g_object_unref(canvased);
|
||||
|
||||
if (baton->file_out == "__jpeg") {
|
||||
// Write JPEG to buffer
|
||||
if (vips_jpegsave_buffer(canvased, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
|
||||
return resize_error(baton, canvased);
|
||||
}
|
||||
} else if (baton->file_out == "__png") {
|
||||
// Write PNG to buffer
|
||||
if (vips_pngsave_buffer(canvased, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
|
||||
return resize_error(baton, canvased);
|
||||
}
|
||||
} else if (is_jpeg(baton->file_out)) {
|
||||
// Write JPEG to file
|
||||
if (vips_jpegsave(canvased, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
|
||||
return resize_error(baton, canvased);
|
||||
}
|
||||
} else if (is_png(baton->file_out)) {
|
||||
// Write PNG to file
|
||||
if (vips_pngsave(canvased, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
|
||||
return resize_error(baton, canvased);
|
||||
}
|
||||
} else {
|
||||
(baton->err).append("Unsupported output " + baton->file_out);
|
||||
}
|
||||
g_object_unref(canvased);
|
||||
vips_thread_shutdown();
|
||||
}
|
||||
// Always convert to sRGB colour space
|
||||
VipsImage *colourspaced = vips_image_new();
|
||||
vips_colourspace(sharpened, &colourspaced, VIPS_INTERPRETATION_sRGB, NULL);
|
||||
g_object_unref(sharpened);
|
||||
|
||||
void resize_async_after(uv_work_t *work, int status) {
|
||||
HandleScope scope;
|
||||
|
||||
resize_baton *baton = static_cast<resize_baton*>(work->data);
|
||||
|
||||
Handle<Value> argv[2] = { Null(), Null() };
|
||||
if (!baton->err.empty()) {
|
||||
// Error
|
||||
argv[0] = String::New(baton->err.data(), baton->err.size());
|
||||
} else if (baton->buffer_out_len > 0) {
|
||||
// Buffer
|
||||
Buffer *slowBuffer = Buffer::New(baton->buffer_out_len);
|
||||
memcpy(Buffer::Data(slowBuffer), baton->buffer_out, baton->buffer_out_len);
|
||||
Local<Object> globalObj = Context::GetCurrent()->Global();
|
||||
Local<Function> bufferConstructor = Local<Function>::Cast(globalObj->Get(String::New("Buffer")));
|
||||
Handle<Value> constructorArgs[3] = { slowBuffer->handle_, v8::Integer::New(baton->buffer_out_len), v8::Integer::New(0) };
|
||||
argv[1] = bufferConstructor->NewInstance(3, constructorArgs);
|
||||
g_free(baton->buffer_out);
|
||||
// Output
|
||||
if (baton->file_out == "__jpeg" || (baton->file_out == "__input" && inputImageType == JPEG)) {
|
||||
// Write JPEG to buffer
|
||||
if (vips_jpegsave_buffer(colourspaced, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
|
||||
return resize_error(baton, colourspaced);
|
||||
}
|
||||
} else if (baton->file_out == "__png" || (baton->file_out == "__input" && inputImageType == PNG)) {
|
||||
// Write PNG to buffer
|
||||
if (vips_pngsave_buffer(colourspaced, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
|
||||
return resize_error(baton, colourspaced);
|
||||
}
|
||||
} else if (baton->file_out == "__webp" || (baton->file_out == "__input" && inputImageType == WEBP)) {
|
||||
// Write WEBP to buffer
|
||||
if (vips_webpsave_buffer(colourspaced, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", baton->quality, NULL)) {
|
||||
return resize_error(baton, colourspaced);
|
||||
}
|
||||
} else if (is_jpeg(baton->file_out)) {
|
||||
// Write JPEG to file
|
||||
if (vips_jpegsave(colourspaced, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, "optimize_coding", TRUE, "interlace", baton->progressive, NULL)) {
|
||||
return resize_error(baton, colourspaced);
|
||||
}
|
||||
} else if (is_png(baton->file_out)) {
|
||||
// Write PNG to file
|
||||
if (vips_pngsave(colourspaced, baton->file_out.c_str(), "strip", TRUE, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) {
|
||||
return resize_error(baton, colourspaced);
|
||||
}
|
||||
} else if (is_webp(baton->file_out)) {
|
||||
// Write WEBP to file
|
||||
if (vips_webpsave(colourspaced, baton->file_out.c_str(), "strip", TRUE, "Q", baton->quality, NULL)) {
|
||||
return resize_error(baton, colourspaced);
|
||||
}
|
||||
} else if (is_tiff(baton->file_out)) {
|
||||
// Write TIFF to file
|
||||
if (vips_tiffsave(colourspaced, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", baton->quality, NULL)) {
|
||||
return resize_error(baton, colourspaced);
|
||||
}
|
||||
} else {
|
||||
(baton->err).append("Unsupported output " + baton->file_out);
|
||||
}
|
||||
g_object_unref(colourspaced);
|
||||
vips_thread_shutdown();
|
||||
}
|
||||
|
||||
baton->callback->Call(Context::GetCurrent()->Global(), 2, argv);
|
||||
baton->callback.Dispose();
|
||||
delete baton;
|
||||
delete work;
|
||||
}
|
||||
void HandleOKCallback () {
|
||||
NanScope();
|
||||
|
||||
Handle<Value> resize(const Arguments& args) {
|
||||
HandleScope scope;
|
||||
|
||||
Handle<Value> argv[2] = { NanNull(), NanNull() };
|
||||
if (!baton->err.empty()) {
|
||||
// Error
|
||||
argv[0] = NanNew<String>(baton->err.data(), baton->err.size());
|
||||
} else if (baton->buffer_out_len > 0) {
|
||||
// Buffer
|
||||
argv[1] = NanNewBufferHandle((char *)baton->buffer_out, baton->buffer_out_len);
|
||||
g_free(baton->buffer_out);
|
||||
}
|
||||
delete baton;
|
||||
callback->Call(2, argv);
|
||||
}
|
||||
|
||||
private:
|
||||
resize_baton* baton;
|
||||
};
|
||||
|
||||
/*
|
||||
resize(options, output, callback)
|
||||
*/
|
||||
NAN_METHOD(resize) {
|
||||
NanScope();
|
||||
|
||||
// V8 objects are converted to non-V8 types held in the baton struct
|
||||
resize_baton *baton = new resize_baton;
|
||||
baton->file_in = *String::Utf8Value(args[0]->ToString());
|
||||
if (args[1]->IsObject()) {
|
||||
Local<Object> buffer = args[1]->ToObject();
|
||||
baton->buffer_in = Buffer::Data(buffer);
|
||||
baton->buffer_in_len = Buffer::Length(buffer);
|
||||
}
|
||||
baton->file_out = *String::Utf8Value(args[2]->ToString());
|
||||
baton->width = args[3]->Int32Value();
|
||||
baton->height = args[4]->Int32Value();
|
||||
Local<String> canvas = args[5]->ToString();
|
||||
if (canvas->Equals(String::NewSymbol("c"))) {
|
||||
baton->crop = true;
|
||||
} else if (canvas->Equals(String::NewSymbol("w"))) {
|
||||
baton->crop = false;
|
||||
baton->extend = VIPS_EXTEND_WHITE;
|
||||
} else if (canvas->Equals(String::NewSymbol("b"))) {
|
||||
baton->crop = false;
|
||||
baton->extend = VIPS_EXTEND_BLACK;
|
||||
}
|
||||
baton->sharpen = args[6]->BooleanValue();
|
||||
baton->progessive = args[7]->BooleanValue();
|
||||
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
|
||||
baton->callback = Persistent<Function>::New(Local<Function>::Cast(args[9]));
|
||||
Local<Object> options = args[0]->ToObject();
|
||||
|
||||
uv_work_t *work = new uv_work_t;
|
||||
work->data = baton;
|
||||
uv_queue_work(uv_default_loop(), work, resize_async, (uv_after_work_cb)resize_async_after);
|
||||
return scope.Close(Undefined());
|
||||
// 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);
|
||||
}
|
||||
// Output image dimensions
|
||||
baton->width = options->Get(NanNew<String>("width"))->Int32Value();
|
||||
baton->height = options->Get(NanNew<String>("height"))->Int32Value();
|
||||
// Canvas options
|
||||
Local<String> canvas = options->Get(NanNew<String>("canvas"))->ToString();
|
||||
if (canvas->Equals(NanNew<String>("c"))) {
|
||||
baton->crop = true;
|
||||
} else if (canvas->Equals(NanNew<String>("w"))) {
|
||||
baton->extend = VIPS_EXTEND_WHITE;
|
||||
} else if (canvas->Equals(NanNew<String>("b"))) {
|
||||
baton->extend = VIPS_EXTEND_BLACK;
|
||||
} else if (canvas->Equals(NanNew<String>("m"))) {
|
||||
baton->max = true;
|
||||
}
|
||||
// Other options
|
||||
baton->sharpen = options->Get(NanNew<String>("sharpen"))->BooleanValue();
|
||||
baton->interpolator = *String::Utf8Value(options->Get(NanNew<String>("interpolator"))->ToString());
|
||||
baton->progressive = options->Get(NanNew<String>("progressive"))->BooleanValue();
|
||||
baton->without_enlargement = options->Get(NanNew<String>("withoutEnlargement"))->BooleanValue();
|
||||
baton->access_method = options->Get(NanNew<String>("sequentialRead"))->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
|
||||
baton->quality = options->Get(NanNew<String>("quality"))->Int32Value();
|
||||
baton->compressionLevel = options->Get(NanNew<String>("compressionLevel"))->Int32Value();
|
||||
baton->angle = options->Get(NanNew<String>("angle"))->Int32Value();
|
||||
// Output filename or __format for Buffer
|
||||
baton->file_out = *String::Utf8Value(args[1]->ToString());
|
||||
|
||||
// Join queue for worker thread
|
||||
NanCallback *callback = new NanCallback(args[2].As<v8::Function>());
|
||||
NanAsyncQueueWorker(new ResizeWorker(callback, baton));
|
||||
NanReturnUndefined();
|
||||
}
|
||||
|
||||
NAN_METHOD(cache) {
|
||||
NanScope();
|
||||
|
||||
// Set cache limit
|
||||
if (args[0]->IsInt32()) {
|
||||
vips_cache_set_max_mem(args[0]->Int32Value() * 1048576);
|
||||
}
|
||||
|
||||
// Get cache statistics
|
||||
Local<Object> cache = NanNew<Object>();
|
||||
cache->Set(NanNew<String>("current"), NanNew<Number>(vips_tracked_get_mem() / 1048576));
|
||||
cache->Set(NanNew<String>("high"), NanNew<Number>(vips_tracked_get_mem_highwater() / 1048576));
|
||||
cache->Set(NanNew<String>("limit"), NanNew<Number>(vips_cache_get_max_mem() / 1048576));
|
||||
NanReturnValue(cache);
|
||||
}
|
||||
|
||||
static void at_exit(void* arg) {
|
||||
HandleScope scope;
|
||||
NanScope();
|
||||
vips_shutdown();
|
||||
}
|
||||
|
||||
extern "C" void init(Handle<Object> target) {
|
||||
HandleScope scope;
|
||||
NanScope();
|
||||
vips_init("");
|
||||
AtExit(at_exit);
|
||||
NODE_SET_METHOD(target, "resize", resize);
|
||||
};
|
||||
NODE_SET_METHOD(target, "cache", cache);
|
||||
}
|
||||
|
||||
NODE_MODULE(sharp, init);
|
||||
NODE_MODULE(sharp, init)
|
||||
|
||||
|
Before Width: | Height: | Size: 813 KiB After Width: | Height: | Size: 813 KiB |
BIN
tests/fixtures/4.webp
vendored
Normal file
|
After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
BIN
tests/fixtures/Crash_test.gif
vendored
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
tests/fixtures/G31D.TIF
vendored
Normal file
BIN
tests/fixtures/Landscape_8.jpg
vendored
Normal file
|
After Width: | Height: | Size: 138 KiB |
32
tests/parallel.js
Executable file
@@ -0,0 +1,32 @@
|
||||
var sharp = require("../index");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var assert = require("assert");
|
||||
var async = require("async");
|
||||
|
||||
var inputJpg = path.join(__dirname, "fixtures/2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
|
||||
var width = 720;
|
||||
var height = 480;
|
||||
|
||||
async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
|
||||
var start = new Date().getTime();
|
||||
async.times(parallelism,
|
||||
function(id, callback) {
|
||||
sharp(inputJpg).resize(width, height).toBuffer(function(err, buffer) {
|
||||
buffer = null;
|
||||
callback(err, new Date().getTime() - start);
|
||||
});
|
||||
},
|
||||
function(err, ids) {
|
||||
assert(!err);
|
||||
assert(ids.length === parallelism);
|
||||
var mean = ids.reduce(function(a, b) {
|
||||
return a + b;
|
||||
}) / ids.length;
|
||||
console.log(parallelism + " parallel calls: fastest=" + ids[0] + "ms slowest=" + ids[ids.length - 1] + "ms mean=" + mean + "ms");
|
||||
next();
|
||||
}
|
||||
);
|
||||
}, function() {
|
||||
console.dir(sharp.cache());
|
||||
});
|
||||
313
tests/perf.js
@@ -1,25 +1,39 @@
|
||||
var sharp = require("../index");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var imagemagick = require("imagemagick");
|
||||
var imagemagickNative = require("imagemagick-native");
|
||||
var gm = require("gm");
|
||||
var epeg = require("epeg");
|
||||
var async = require("async");
|
||||
var assert = require("assert");
|
||||
var Benchmark = require("benchmark");
|
||||
|
||||
var inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
|
||||
var outputJpg = __dirname + "/output.jpg";
|
||||
var fixturesPath = path.join(__dirname, "fixtures");
|
||||
|
||||
var inputPng = __dirname + "/50020484-00001.png"; // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
||||
var outputPng = __dirname + "/output.png";
|
||||
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
|
||||
var outputJpg = path.join(fixturesPath, "output.jpg");
|
||||
|
||||
var inputPng = path.join(fixturesPath, "50020484-00001.png"); // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
|
||||
var outputPng = path.join(fixturesPath, "output.png");
|
||||
|
||||
var inputWebp = path.join(fixturesPath, "4.webp"); // http://www.gstatic.com/webp/gallery/4.webp
|
||||
var outputWebp = path.join(fixturesPath, "output.webp");
|
||||
|
||||
var inputTiff = path.join(fixturesPath, "G31D.TIF"); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
|
||||
var outputTiff = path.join(fixturesPath, "output.tiff");
|
||||
|
||||
var inputGif = path.join(fixturesPath, "Crash_test.gif"); // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif
|
||||
|
||||
var width = 720;
|
||||
var height = 480;
|
||||
|
||||
// Disable libvips cache to ensure tests are as fair as they can be
|
||||
sharp.cache(0);
|
||||
|
||||
async.series({
|
||||
jpeg: function(callback) {
|
||||
var inputJpgBuffer = fs.readFileSync(inputJpg);
|
||||
(new Benchmark.Suite("jpeg")).add("imagemagick", {
|
||||
(new Benchmark.Suite("jpeg")).add("imagemagick-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagick.resize({
|
||||
@@ -36,6 +50,41 @@ async.series({
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("imagemagick-native-buffer-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagickNative.convert({
|
||||
srcData: inputJpgBuffer,
|
||||
quality: 80,
|
||||
width: width,
|
||||
height: height,
|
||||
format: 'JPEG'
|
||||
});
|
||||
deferred.resolve();
|
||||
}
|
||||
}).add("gm-buffer-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(inputJpgBuffer).resize(width, height).quality(80).write(outputJpg, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("gm-buffer-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(inputJpgBuffer).resize(width, height).quality(80).toBuffer(function (err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("gm-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
@@ -59,25 +108,10 @@ async.series({
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("epeg-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
var image = new epeg.Image({path: inputJpg});
|
||||
image.downsize(width, height, 80).saveTo(outputJpg);
|
||||
deferred.resolve();
|
||||
}
|
||||
}).add("epeg-file-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
var image = new epeg.Image({path: inputJpg});
|
||||
var buffer = image.downsize(width, height, 80).process();
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
}).add("sharp-buffer-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpgBuffer, outputJpg, width, height, function(err) {
|
||||
sharp(inputJpgBuffer).resize(width, height).toFile(outputJpg, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -88,7 +122,7 @@ async.series({
|
||||
}).add("sharp-buffer-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpgBuffer, sharp.buffer.jpeg, width, height, function(err, buffer) {
|
||||
sharp(inputJpgBuffer).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -100,7 +134,7 @@ async.series({
|
||||
}).add("sharp-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpg, outputJpg, width, height, function(err) {
|
||||
sharp(inputJpg).resize(width, height).toFile(outputJpg, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -111,7 +145,7 @@ async.series({
|
||||
}).add("sharp-file-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, function(err, buffer) {
|
||||
sharp(inputJpg).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -120,10 +154,42 @@ async.series({
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-promise", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).toBuffer().then(function(buffer) {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-sharpen", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, {sharpen: true}, function(err, buffer) {
|
||||
sharp(inputJpg).resize(width, height).sharpen().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-bicubic", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).bicubicInterpolation().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-nohalo", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(width, height).nohaloInterpolation().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -135,7 +201,19 @@ async.series({
|
||||
}).add("sharp-file-buffer-progressive", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, {progressive: true}, function(err, buffer) {
|
||||
sharp(inputJpg).resize(width, height).progressive().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).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 {
|
||||
@@ -147,7 +225,7 @@ async.series({
|
||||
}).add("sharp-file-buffer-sequentialRead", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, {sequentialRead: true}, function(err, buffer) {
|
||||
sharp(inputJpg).resize(width, height).sequentialRead().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -164,7 +242,7 @@ async.series({
|
||||
},
|
||||
png: function(callback) {
|
||||
var inputPngBuffer = fs.readFileSync(inputPng);
|
||||
(new Benchmark.Suite("png")).add("imagemagick", {
|
||||
(new Benchmark.Suite("png")).add("imagemagick-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagick.resize({
|
||||
@@ -180,6 +258,17 @@ async.series({
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("imagemagick-native-buffer-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagickNative.convert({
|
||||
srcData: inputPngBuffer,
|
||||
width: width,
|
||||
height: height,
|
||||
format: 'PNG'
|
||||
});
|
||||
deferred.resolve();
|
||||
}
|
||||
}).add("gm-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
@@ -206,7 +295,7 @@ async.series({
|
||||
}).add("sharp-buffer-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPngBuffer, outputPng, width, height, function(err) {
|
||||
sharp(inputPngBuffer).resize(width, height).toFile(outputPng, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -217,7 +306,7 @@ async.series({
|
||||
}).add("sharp-buffer-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPngBuffer, sharp.buffer.png, width, height, function(err, buffer) {
|
||||
sharp(inputPngBuffer).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -229,7 +318,7 @@ async.series({
|
||||
}).add("sharp-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPng, outputPng, width, height, function(err) {
|
||||
sharp(inputPng).resize(width, height).toFile(outputPng, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -240,7 +329,7 @@ async.series({
|
||||
}).add("sharp-file-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPng, sharp.buffer.png, width, height, function(err, buffer) {
|
||||
sharp(inputPng).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -252,7 +341,7 @@ async.series({
|
||||
}).add("sharp-file-buffer-sharpen", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPng, sharp.buffer.png, width, height, {sharpen: true}, function(err, buffer) {
|
||||
sharp(inputPng).resize(width, height).sharpen().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -264,19 +353,7 @@ async.series({
|
||||
}).add("sharp-file-buffer-progressive", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPng, sharp.buffer.png, width, height, {progressive: true}, function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-sequentialRead", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp.resize(inputPng, sharp.buffer.png, width, height, {sequentialRead: true}, function(err, buffer) {
|
||||
sharp(inputPng).resize(width, height).progressive().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
@@ -290,10 +367,148 @@ async.series({
|
||||
}).on("complete", function() {
|
||||
callback(null, this.filter("fastest").pluck("name"));
|
||||
}).run();
|
||||
}
|
||||
},
|
||||
webp: function(callback) {
|
||||
var inputWebpBuffer = fs.readFileSync(inputWebp);
|
||||
(new Benchmark.Suite("webp")).add("sharp-buffer-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputWebpBuffer).resize(width, height).toFile(outputWebp, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-buffer-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputWebpBuffer).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputWebp).resize(width, height).toFile(outputWebp, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputWebp).resize(width, height).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-buffer-sharpen", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputWebp).resize(width, height).sharpen().toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on("cycle", function(event) {
|
||||
console.log("webp " + String(event.target));
|
||||
}).on("complete", function() {
|
||||
callback(null, this.filter("fastest").pluck("name"));
|
||||
}).run();
|
||||
},
|
||||
tiff: function(callback) {
|
||||
(new Benchmark.Suite("tiff")).add("sharp-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputTiff).resize(width, height).toFile(outputTiff, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-file-sharpen", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputTiff).resize(width, height).sharpen().toFile(outputTiff, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on("cycle", function(event) {
|
||||
console.log("tiff " + String(event.target));
|
||||
}).on("complete", function() {
|
||||
callback(null, this.filter("fastest").pluck("name"));
|
||||
}).run();
|
||||
},
|
||||
gif: function(callback) {
|
||||
(new Benchmark.Suite("gif")).add("sharp-file-file", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputGif).resize(width, height).toFile(outputTiff, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-file-sharpen", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputGif).resize(width, height).sharpen().toFile(outputTiff, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp-file-file-sequentialRead", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputGif).sequentialRead().resize(width, height).toFile(outputTiff, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on("cycle", function(event) {
|
||||
console.log("gif " + String(event.target));
|
||||
}).on("complete", function() {
|
||||
callback(null, this.filter("fastest").pluck("name"));
|
||||
}).run();
|
||||
}
|
||||
}, function(err, results) {
|
||||
assert(!err, err);
|
||||
Object.keys(results).forEach(function(format) {
|
||||
assert.strictEqual("sharp", results[format].toString().substr(0, 5), "sharp was slower than " + results[format] + " for " + format);
|
||||
if (results[format].toString().substr(0, 5) !== "sharp") {
|
||||
console.log("sharp was slower than " + results[format] + " for " + format);
|
||||
}
|
||||
});
|
||||
console.dir(sharp.cache());
|
||||
});
|
||||
|
||||
68
tests/random.js
Executable file
@@ -0,0 +1,68 @@
|
||||
var sharp = require("../index");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var imagemagick = require("imagemagick");
|
||||
var gm = require("gm");
|
||||
var async = require("async");
|
||||
var assert = require("assert");
|
||||
var Benchmark = require("benchmark");
|
||||
|
||||
var fixturesPath = path.join(__dirname, "fixtures");
|
||||
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
|
||||
var outputJpg = path.join(fixturesPath, "output.jpg");
|
||||
|
||||
var min = 320;
|
||||
var max = 960;
|
||||
|
||||
var randomDimension = function() {
|
||||
return Math.random() * (max - min) + min;
|
||||
};
|
||||
|
||||
new Benchmark.Suite("random").add("imagemagick", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
imagemagick.resize({
|
||||
srcPath: inputJpg,
|
||||
dstPath: outputJpg,
|
||||
quality: 0.8,
|
||||
width: randomDimension(),
|
||||
height: randomDimension()
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("gm", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
gm(inputJpg).resize(randomDimension(), randomDimension()).quality(80).toBuffer(function (err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).add("sharp", {
|
||||
defer: true,
|
||||
fn: function(deferred) {
|
||||
sharp(inputJpg).resize(randomDimension(), randomDimension()).toBuffer(function(err, buffer) {
|
||||
if (err) {
|
||||
throw err;
|
||||
} else {
|
||||
assert.notStrictEqual(null, buffer);
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on("cycle", function(event) {
|
||||
console.log(String(event.target));
|
||||
}).on("complete", function() {
|
||||
var winner = this.filter("fastest").pluck("name");
|
||||
assert.strictEqual("sharp", String(winner), "sharp was slower than " + winner);
|
||||
console.dir(sharp.cache());
|
||||
}).run();
|
||||
308
tests/unit.js
Executable file
@@ -0,0 +1,308 @@
|
||||
/*jslint node: true */
|
||||
/*jslint es5: true */
|
||||
'use strict';
|
||||
|
||||
var sharp = require("../index");
|
||||
var path = require("path");
|
||||
var imagemagick = require("imagemagick");
|
||||
var assert = require("assert");
|
||||
var async = require("async");
|
||||
|
||||
var fixturesPath = path.join(__dirname, "fixtures");
|
||||
|
||||
var inputJpg = path.join(fixturesPath, "2569067123_aca715a2ee_o.jpg"); // http://www.flickr.com/photos/grizdave/2569067123/
|
||||
var outputJpg = path.join(fixturesPath, "output.jpg");
|
||||
|
||||
var inputTiff = path.join(fixturesPath, "G31D.TIF"); // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm
|
||||
var outputTiff = path.join(fixturesPath, "output.tiff");
|
||||
|
||||
var inputJpgWithExif = path.join(fixturesPath, "Landscape_8.jpg"); // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg
|
||||
|
||||
async.series([
|
||||
// Resize with exact crop
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(240, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Resize to fixed width
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(261, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Resize to fixed height
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(null, 320).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(391, features.width);
|
||||
assert.strictEqual(320, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Identity transform
|
||||
function(done) {
|
||||
sharp(inputJpg).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(2725, features.width);
|
||||
assert.strictEqual(2225, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Upscale
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(3000).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(3000, features.width);
|
||||
assert.strictEqual(2449, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Quality
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).quality(70).jpeg(function(err, buffer70) {
|
||||
if (err) throw err;
|
||||
sharp(inputJpg).resize(320, 240).jpeg(function(err, buffer80) {
|
||||
if (err) throw err;
|
||||
sharp(inputJpg).resize(320, 240).quality(90).jpeg(function(err, buffer90) {
|
||||
assert(buffer70.length < buffer80.length);
|
||||
assert(buffer80.length < buffer90.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
// TIFF with dimensions known to cause rounding errors
|
||||
function(done) {
|
||||
sharp(inputTiff).resize(240, 320).embedBlack().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(240, features.width);
|
||||
assert.strictEqual(320, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
function(done) {
|
||||
sharp(inputTiff).resize(240, 320).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(240, features.width);
|
||||
assert.strictEqual(320, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Resize to max width or height considering ratio (landscape)
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 320).max().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(261, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Resize to max width or height considering ratio (portrait)
|
||||
function(done) {
|
||||
sharp(inputTiff).resize(320, 320).max().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(243, features.width);
|
||||
assert.strictEqual(320, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Attempt to resize to max but only provide one dimension, so should default to crop
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320).max().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(261, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Attempt to output to input, should fail
|
||||
function(done) {
|
||||
sharp(inputJpg).toFile(inputJpg, function(err) {
|
||||
assert(!!err);
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Rotate by 90 degrees, respecting output input size
|
||||
function(done) {
|
||||
sharp(inputJpg).rotate(90).resize(320, 240).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(240, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Input image has Orientation EXIF tag but do not rotate output
|
||||
function(done) {
|
||||
sharp(inputJpgWithExif).resize(320).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(426, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Input image has Orientation EXIF tag value of 8 (270 degrees), auto-rotate
|
||||
function(done) {
|
||||
sharp(inputJpgWithExif).rotate().resize(320).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(240, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Attempt to auto-rotate using image that has no EXIF
|
||||
function(done) {
|
||||
sharp(inputJpg).rotate().resize(320).toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(261, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Rotate to an invalid angle, should fail
|
||||
function(done) {
|
||||
var fail = false;
|
||||
try {
|
||||
sharp(inputJpg).rotate(1);
|
||||
fail = true;
|
||||
} catch (e) {}
|
||||
assert(!fail);
|
||||
done();
|
||||
},
|
||||
// Do not enlarge the output if the input width is already less than the output width
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(2800).withoutEnlargement().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(2725, features.width);
|
||||
assert.strictEqual(2225, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Do not enlarge the output if the input height is already less than the output height
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(null, 2300).withoutEnlargement().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(2725, features.width);
|
||||
assert.strictEqual(2225, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Promises/A+
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).toFile(outputJpg).then(function() {
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(240, features.height);
|
||||
done();
|
||||
});
|
||||
}).catch(function(err) {
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
// Empty Buffer, should fail
|
||||
function(done) {
|
||||
var fail = false;
|
||||
try {
|
||||
sharp(new Buffer(0));
|
||||
fail = true;
|
||||
} catch (e) {}
|
||||
assert(!fail);
|
||||
done();
|
||||
},
|
||||
// Check colour space conversion occurs from TIFF to WebP (this used to segfault)
|
||||
function(done) {
|
||||
sharp(inputTiff).webp().then(function() {
|
||||
done();
|
||||
});
|
||||
},
|
||||
// Interpolation: bilinear
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).bilinearInterpolation().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(240, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Interpolation: bicubic
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).bicubicInterpolation().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(240, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
// Interpolation: nohalo
|
||||
function(done) {
|
||||
sharp(inputJpg).resize(320, 240).nohaloInterpolation().toFile(outputJpg, function(err) {
|
||||
if (err) throw err;
|
||||
imagemagick.identify(outputJpg, function(err, features) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(320, features.width);
|
||||
assert.strictEqual(240, features.height);
|
||||
done();
|
||||
});
|
||||
});
|
||||
},
|
||||
]);
|
||||